QA How-To
Testing GraphQL subscriptions (2026)
Learn testing GraphQL subscriptions with graphql-ws, event and authorization matrices, reconnect, ordering, cleanup, observability, and interview answers.
26 min read | 3,523 words
TL;DR
Testing GraphQL subscriptions requires more than connecting a WebSocket. Validate the GraphQL operation and every emitted result, then test event filtering, identity boundaries, lifecycle, reconnect, delivery guarantees, backpressure, and cleanup against the deployed transport.
Key Takeaways
- Separate transport connection, GraphQL operation, event routing, and committed business state in every test oracle.
- Document the actual WebSocket, SSE, or vendor transport instead of assuming GraphQL mandates one protocol.
- Wait for operation readiness before publishing, because connection acknowledgment alone can hide race conditions.
- Test positive delivery and bounded silence for nonmatching, unauthorized, and cross-tenant events.
- Define ordering, duplication, replay, resume, and reconnect guarantees before asserting reliability.
- Verify unsubscribe, socket close, failure, and deployment cleanup release listeners, buffers, and tasks.
Testing GraphQL subscriptions means validating a long-lived stream, not just a successful WebSocket connection. QA must prove subscription document validation, authentication, authorization, event filtering, payload shape, ordering, duplication policy, cancellation, reconnect behavior, resource cleanup, and consistency with the system of record.
GraphQL defines subscription execution semantics, but the transport is a separate contract. A product may use the graphql-transport-ws WebSocket subprotocol, GraphQL over Server-Sent Events, or a managed service protocol. Record the actual handshake and message rules before automating tests. This guide uses the maintained graphql-ws library for a runnable WebSocket example and keeps the broader strategy transport-aware.
TL;DR
| Layer | Minimum assertion |
|---|---|
| Schema | Subscription field, arguments, return type, nullability, and deprecations match the contract |
| Transport | Negotiated protocol, connection acknowledgment, message sequence, close behavior, and keepalive are correct |
| Identity | Invalid identity is rejected, valid identity is bound to operations, and expiry or revocation follows policy |
| Event routing | Only matching tenant, resource, topic, and filter events reach the subscriber |
| Payload | Every next result follows GraphQL data and errors semantics and requested field selection |
| Lifecycle | Unsubscribe, socket close, reconnect, and server shutdown release listeners and resources |
| Reliability | Ordering, duplicate, gap, resume, and replay behavior is explicitly tested against the product guarantee |
1. Build the Testing GraphQL Subscriptions Mental Model
A GraphQL subscription operation creates a response stream. Each event is executed against the selected fields and produces an execution result. The first useful distinction is between connection, operation, event, and business state. A socket can be open while the subscription operation was rejected. An operation can be accepted while no matching event arrives. A payload can arrive while the underlying business transaction later rolls back.
Model four layers:
- Transport lifecycle: connect, negotiate, initialize, acknowledge, ping or pong, close, and reconnect.
- GraphQL operation: parse, validate, authorize, start, emit
next, returnerror, andcomplete. - Event infrastructure: publish, route, filter, serialize, retry, partition, and fan out.
- Business truth: committed entity state, ordering rule, visibility, and audit evidence.
Keep IDs separate. The WebSocket connection has an identity, each active operation has a protocol ID, the domain event has an event ID, and the business object has its own ID and version. Log and assert the right one. Reusing id without context makes failures almost impossible to diagnose.
The GraphQL API testing guide explains documents, variables, nullability, and response envelopes for queries and mutations. Subscription testing reuses those rules for every emitted result, then adds time, lifecycle, and transport risk.
2. Identify the Schema, Transport, and Delivery Contract
Start from the schema. Inventory every field on Subscription, its arguments, defaults, return type, nullability, directives, deprecations, and authorization rule. Save representative operation documents with explicit names. Include minimum field selection, normal client selection, fragments, aliases, variables, and invalid cases.
Then document transport facts instead of assuming WebSockets:
- URL and
ws://orwss://, or HTTP endpoint for SSE. - WebSocket subprotocol, commonly
graphql-transport-wsforgraphql-ws. - Connection initialization payload and acknowledgment timeout.
- Authentication source and refresh behavior.
- Ping, pong, application keepalive, idle timeout, and proxy timeout.
- Maximum connections and active operations per identity.
- Close codes and public error shapes.
- Retry, resume, replay, and deduplication guarantees.
GraphQL itself does not promise durable event delivery. Ask whether the product offers at-most-once, at-least-once, replay from a cursor, best effort, or no explicit guarantee. Define ordering scope: global, tenant, topic, object, partition, or none. Exact-once wording deserves scrutiny because reconnects and distributed failures usually require idempotent consumers even when infrastructure deduplicates.
Record event-to-payload mapping. Does a subscription emit after database commit, after a queue accepts the event, or before a transaction completes? Can one business action produce multiple events? Does filtering happen before or after authorization? These answers determine the oracle and race tests.
3. Compare WebSocket, SSE, and Polling Test Surfaces
Transport choice changes operational tests while GraphQL field and authorization rules remain similar.
| Dimension | WebSocket subscription | GraphQL over SSE | Polling fallback |
|---|---|---|---|
| Direction | Full-duplex connection | Server-to-client event stream over HTTP | Repeated request-response |
| Multiplexing | Often many operations per socket | Protocol and mode dependent | One operation per request |
| Browser auth | Cookies or connection initialization payload | Normal HTTP credentials and cookies | Normal HTTP credentials and cookies |
| Main lifecycle risks | Subprotocol, init, keepalive, close, reconnect | Proxy buffering, HTTP timeout, cancellation | Interval, overlap, cache, missed changes |
| Cancellation | Protocol complete and socket lifecycle |
Abort or connection close | Stop timer or abort request |
| Scaling focus | Connection count and operations per socket | Long HTTP streams and intermediaries | Request volume and backend load |
| Useful diagnostics | Frames, close code, connection and operation IDs | HTTP headers, event frames, disconnect | Request logs, cursor, response version |
Do not force WebSocket expectations onto SSE. An HTTP stream has status and headers before events, while a WebSocket upgrade negotiates a subprotocol. Likewise, do not describe polling as a subscription. It may be a valid fallback with different freshness and load guarantees.
Test the deployment path, including CDN, ingress, load balancer, service mesh, and idle timeouts. A local socket that survives ten minutes says little about a production proxy configured for a shorter idle period. Keepalive frames must be frequent enough for intermediaries but not so aggressive that they create unnecessary load.
TLS matters. Production browser clients should use wss:// from HTTPS pages. Verify certificates, hostnames, and mixed-content behavior. Test allowed origins and cookie scope at the handshake, especially when browser sessions authenticate the connection.
4. Design a Subscription Event Test Matrix
Create controlled actors, resources, and events. For a subscription such as orderStatusChanged(orderId: ID!), seed order A owned by user A and order B owned by user B. Subscribe A to order A, publish a matching change, and require exactly the documented payload. Then publish a nonmatching event and require silence within a bounded observation window.
A practical matrix covers:
- Matching event for the authorized resource.
- Same event type for another resource.
- Same resource in another tenant.
- Event before the operation is acknowledged or known active.
- Event immediately after subscription starts.
- Multiple events with increasing object versions.
- Duplicate event ID according to deduplication policy.
- Out-of-order publish according to ordering scope.
- Deleted, archived, or permission-revoked resource.
- Event that causes a nullable and non-null resolver failure.
- Unsubscribe followed by another matching publish.
- Disconnect during an event and reconnect.
Use a subscription-ready signal. Connection acknowledgment proves the connection initialized, not necessarily that a specific operation installed its event listener. Some protocols and libraries do not provide an operation acknowledgment by default. For deterministic tests, add an approved test hook, server metric, or domain handshake that confirms the listener before publishing.
Avoid arbitrary long sleeps. Wait on explicit lifecycle events and use short, bounded timeouts only for negative silence assertions. If a test fails, report whether it never subscribed, never published, misrouted, failed execution, lost transport, or timed out waiting for the client.
5. Validate Handshake and Protocol Message Sequences
For graphql-transport-ws, verify the client offers and server accepts the correct WebSocket subprotocol. Opening a generic WebSocket is not enough. Capture frames and require the documented sequence: connection initialization, connection acknowledgment, operation subscribe, zero or more next messages, then error or complete according to the operation outcome.
Test malformed and out-of-order messages in a dedicated protocol suite. Examples include subscribe before initialization when initialization is required, repeated initialization, duplicate active operation IDs, unknown message types, malformed JSON, and completion for an unknown operation. Assert the documented close code or operation error. Keep these tests at low volume because some cases intentionally close connections.
Differentiate operation errors from connection errors. A GraphQL validation error for one subscription should not necessarily kill unrelated operations on the same socket. An invalid authentication payload may close the connection and terminate all operations. The chosen behavior must match the server library and application contract.
Ping and pong can exist at WebSocket and application-protocol layers. Record which layer the system uses. Test a healthy exchange, a missed response, delayed response, and payload echo only if specified. Client retry should avoid synchronized reconnect storms through bounded backoff and jitter.
Close behavior deserves exact assertions. Test normal client disposal, server graceful shutdown, policy violation, authentication rejection, idle timeout, deployment restart, and abrupt network loss. Record close code, reason exposure, reconnect eligibility, and whether active operation listeners are released.
6. Automate Testing GraphQL Subscriptions with graphql-ws
This local Node.js test starts a GraphQL WebSocket server, subscribes with the maintained graphql-ws client, receives two execution results, and verifies completion. It uses real GraphQL execution and the graphql-transport-ws subprotocol. Install current compatible releases with npm install -D graphql graphql-ws ws @graphql-tools/schema, save as subscriptions.test.mjs, and run node --test subscriptions.test.mjs.
import assert from "node:assert/strict";
import http from "node:http";
import { after, before, test } from "node:test";
import { makeExecutableSchema } from "@graphql-tools/schema";
import { createClient } from "graphql-ws";
import { useServer } from "graphql-ws/use/ws";
import { WebSocket, WebSocketServer } from "ws";
const typeDefs = `
type Query { health: String! }
type CounterEvent { value: Int!, sequence: Int! }
type Subscription { counter(limit: Int!): CounterEvent! }
`;
const resolvers = {
Query: { health: () => "ok" },
Subscription: {
counter: {
subscribe: async function* (_root, { limit }) {
for (let sequence = 1; sequence <= limit; sequence += 1) {
yield { counter: { value: sequence * 10, sequence } };
}
}
}
}
};
const schema = makeExecutableSchema({ typeDefs, resolvers });
let httpServer;
let wsServer;
let serverCleanup;
let client;
let url;
before(async () => {
httpServer = http.createServer();
wsServer = new WebSocketServer({ server: httpServer, path: "/graphql" });
serverCleanup = useServer({ schema }, wsServer);
await new Promise((resolve) => httpServer.listen(0, "127.0.0.1", resolve));
const address = httpServer.address();
url = `ws://127.0.0.1:${address.port}/graphql`;
client = createClient({ url, webSocketImpl: WebSocket });
});
after(async () => {
await client.dispose();
await serverCleanup.dispose();
await new Promise((resolve) => httpServer.close(resolve));
});
test("delivers selected fields in sequence and completes", async () => {
const results = [];
await new Promise((resolve, reject) => {
client.subscribe(
{
query: `subscription Counter($limit: Int!) {
counter(limit: $limit) { value sequence }
}`,
operationName: "Counter",
variables: { limit: 2 }
},
{
next: (result) => results.push(result),
error: reject,
complete: resolve
}
);
});
assert.deepEqual(results, [
{ data: { counter: { value: 10, sequence: 1 } } },
{ data: { counter: { value: 20, sequence: 2 } } }
]);
});
This test intentionally uses a deterministic async generator, so it proves GraphQL execution, framing, field selection, sequence, and completion without a broker. Add separate integration tests with the real publisher and event infrastructure. If your installed package uses different runtime requirements, choose mutually compatible current releases rather than copying an old transport library.
7. Test Authentication, Authorization, and Tenant Isolation
Authentication can occur during the HTTP upgrade, connection initialization, each subscribe operation, or a combination. Browser WebSocket APIs do not allow arbitrary custom upgrade headers, so cookie sessions or an initialization payload are common. Document where the token lives, whether it is logged, and how the server validates issuer, audience, expiry, and revocation.
Test no credential, malformed credential, expired token, wrong audience, valid user, disabled user, and token expiry after connection. If authentication is checked only once, define what happens to an existing subscription when access is revoked. High-risk products may revalidate per event or close affected operations on policy change.
Authorization belongs at operation and event level. A user allowed to subscribe to orderChanged must not receive every tenant's order event. Test user A and B with matching resource fixtures, then publish both events while both are connected. Assert positive delivery and negative silence in each direction.
Filters are not authorization. A client-supplied tenantId or userId must not expand visibility. Resolve tenant and ownership from the trusted principal and server-side relationships. Test omitted, altered, duplicate, and unauthorized filter arguments. Verify aliases or multiple operations on one socket cannot reuse another operation's authorization context.
Origin validation matters for cookie-authenticated browser sockets. Test trusted origin, foreign origin, same-site sibling origin, null, and missing Origin according to policy. Pair this with the testing for CSRF guide, especially for cross-site WebSocket hijacking risk.
8. Verify Filtering, Selection Sets, Nullability, and Errors
Publish events that sit just inside and outside every filter boundary. Test exact ID, list membership, status, time window, text filter, and combinations. Filters should use event and authoritative resource data, not trust client claims. When resource visibility changes after publication but before delivery, the policy must define whether to suppress or redact the event.
Each emitted payload follows normal GraphQL field selection. Subscribe with minimum and expanded selections, fragments, aliases, interface or union conditions, deprecated fields, and variables. Ensure unselected sensitive fields never appear. Generated event objects can still leak through extensions, debug errors, or serialized internal properties if custom code bypasses GraphQL execution.
Test resolver failure at different nullability levels. A nullable field error can return partial data plus errors. A non-null field failure propagates to the nearest nullable parent and can null the subscription payload branch. The stream may continue or terminate according to implementation and error class. Capture the actual contract and assert it explicitly.
Distinguish protocol error from a next payload containing GraphQL execution errors. Validation failures commonly produce an operation error before the stream begins. Field execution failures can arrive in results after events. Client code must handle both rather than treating every HTTP-style success assumption as sufficient.
Avoid asserting entire error messages when they contain implementation details. Prefer stable extension codes, response paths, safe messages, and stream lifecycle. Ensure errors do not reveal another tenant's object, topic name, broker partition, stack trace, or resolver source.
9. Test Ordering, Duplicates, Gaps, Replay, and Reconnect
Reliability tests start with a written guarantee. If ordering is per aggregate, publish version 7 then 8 for the same object and require that order. Do not demand global order across unrelated partitions unless the architecture promises it. Include event ID, object version, and server time as separate concepts.
For duplicates, publish the same event ID twice or simulate broker redelivery in a test environment. The server may deduplicate, or the client may be expected to handle at-least-once delivery. Assert the documented layer. Business consumers should remain idempotent where duplicate delivery is possible.
For gaps, disconnect the client between known events, publish while offline, and reconnect. A best-effort subscription may miss the event and require a query refresh. A resumable stream may accept a cursor and replay. Test valid, stale, future, unauthorized, and malformed cursors, retention expiry, and duplicate overlap at the resume boundary.
Reconnect behavior must avoid silently creating two live operations. Force an abrupt socket close, observe retry policy, confirm reauthentication, and count server listeners. Publish one event after recovery and assert one delivery unless replay explicitly adds another. Dispose the client and require listener count to return to baseline.
Client applications should reconcile subscription events with query state. An event may contain only an ID or stale snapshot. Test the cache update or refetch logic, optimistic mutation overlap, version comparison, and page visibility changes. The GraphQL vs REST for testers guide helps compare these state and error contracts across API styles.
10. Test Cancellation, Backpressure, and Resource Cleanup
Unsubscribe is a functional and operational requirement. Start an operation, confirm it is active, dispose it, publish another matching event, and require no delivery. Verify the server removes broker listeners, timers, database cursors, and per-operation context. Closing the whole socket must clean up every active operation.
Test client cancellation while a resolver is waiting, after an event is published, during payload serialization, and during server shutdown. Async generators should receive cancellation through their return or abort path. Instrument listener and task counts in a test build. Heap growth after repeated subscribe-unsubscribe cycles is a useful leak signal, but measure under controlled conditions.
Backpressure appears when publishers are faster than a client or network. Define buffer limits, drop policy, disconnect policy, and metrics. In a load environment, slow one consumer, publish a bounded burst, and observe memory, queue depth, latency, dropped events, and impact on healthy consumers. Do not run unbounded publishers.
Set limits for connections per identity, active operations per connection, query cost, payload size, event rate, and idle duration. Test just below and above each boundary. Rejection must not harm existing authorized streams or expose global capacity.
Graceful deployment should stop accepting new operations, drain or close existing streams according to policy, and let clients reconnect without a storm. Verify close signals, retry jitter, listener cleanup, and readiness behavior. Kubernetes or serverless platform time limits belong in the test plan because long-lived connections interact with rollout and scaling differently from ordinary requests.
11. Validate Observability and Diagnose Subscription Failures
Logs and metrics should let QA locate a failure without recording secrets or full private payloads. Useful fields include connection ID, operation ID, normalized operation name or hash, safe principal reference, tenant, event type, event ID, object version, decision, delivery result, close code, and correlation ID.
Track connection attempts, active connections, active operations, initialization failures, authorization denials, event publish count, matched subscriber count, delivery latency, serialization errors, buffer depth, drops, reconnects, and close codes. Keep user IDs and object IDs out of high-cardinality metric labels.
Trace one test event across transaction commit, outbox, broker publish, consumer, filter, GraphQL execution, transport send, and client receipt. If the business action succeeded but no payload arrived, this trace identifies the missing segment. Use a synthetic event ID safe for test evidence.
Client diagnostics should distinguish connection acknowledged, operation active, first event received, normal complete, operation error, socket close, and retry. A single connected UI status is misleading. Expose enough state in test builds to make readiness deterministic.
Alerting needs context. A reconnect spike may reflect a deployment, proxy timeout, certificate issue, auth expiry, or regional network event. A drop spike may reflect a slow subscriber or undersized buffer. Pair service metrics with gateway and broker signals before assigning root cause.
12. Build a Production-Grade Subscription Test Pyramid
Unit tests should cover filter predicates, authorization policy, event-to-payload mapping, idempotency, cursor parsing, and cleanup hooks. Use plain async iterators and injected clocks so edge cases are fast and deterministic.
Schema and contract tests should validate stored client operations against the current schema, detect breaking nullability or field changes, and check complexity limits. Protocol tests should exercise message sequences, close codes, and malformed frames against the actual server adapter.
Integration tests should include the real database transaction boundary, outbox or publisher, broker, subscription service, and GraphQL execution. Use unique topic or tenant fixtures and explicit readiness. End-to-end tests should use the same transport library as production clients and traverse the deployed proxy.
Load and resilience tests belong in a controlled environment. Model realistic connection duration, operations per socket, publish distribution, slow consumers, rolling restarts, broker interruption, and regional latency. Report percentiles and resource ceilings from your own environment rather than copying generic benchmark numbers.
A compact release gate can include valid delivery, nonmatching silence, cross-tenant denial, GraphQL error semantics, unsubscribe cleanup, and one reconnect case. Run deeper soak, backpressure, and failure tests on a scheduled cadence and before architecture changes.
For broader real-time protocol coverage, use the WebSocket testing guide. It complements GraphQL operation assertions with frame, proxy, network, and connection-level checks.
Interview Questions and Answers
Q: How is testing GraphQL subscriptions different from testing queries?
A query produces one response, while a subscription creates a long-lived response stream. I test connection and operation lifecycle, event routing, multiple payloads, silence for nonmatching events, cancellation, reconnect, ordering, and cleanup in addition to normal GraphQL validation.
Q: Does GraphQL require WebSockets for subscriptions?
No. GraphQL specifies subscription execution semantics but does not require one transport. Products may use WebSockets, SSE, or vendor protocols, so I document and test the deployed transport contract.
Q: What does a WebSocket connection acknowledgment prove?
It proves the connection initialization was accepted under that protocol. It does not necessarily prove a particular subscription operation installed its listener. Deterministic tests need an operation-ready signal before publishing.
Q: How do you test subscription authorization?
I create two users and resources, subscribe each to allowed data, and publish matching and cross-tenant events. I require positive delivery and bounded negative silence. I also test permission revocation during an active stream.
Q: How do you test missed or duplicate events?
I begin with the documented delivery guarantee. Then I simulate duplicate IDs, disconnect gaps, and resume boundaries, asserting server deduplication or client idempotency at the promised layer. I do not assume exact-once delivery.
Q: How do you avoid flaky subscription tests?
I wait for explicit connection and operation readiness, use unique fixtures, publish after readiness, and apply short bounded timeouts only to silence cases. Logs separate subscribe, publish, match, execute, send, and receive stages.
Q: What should happen when a subscription resolver field fails?
The result follows GraphQL nullability and error propagation. A payload may contain partial data and errors, while validation can produce an operation-level error before streaming. Whether the stream continues is part of the server contract and must be tested.
Q: How do you test resource cleanup?
I instrument listener, task, and operation counts. After unsubscribe or socket close, I publish another event, require no delivery, and confirm counts return to baseline. I repeat cycles under a bounded soak to detect leaks.
Common Mistakes
- Treating an open socket or connection acknowledgment as a successful active subscription.
- Assuming all GraphQL subscriptions use the same WebSocket protocol.
- Publishing before the operation listener is ready and masking the race with long sleeps.
- Asserting only received events and never proving nonmatching or cross-tenant silence.
- Ignoring GraphQL
errors, partial data, null propagation, and selection sets for each event. - Requiring global order or exact-once delivery without an architectural guarantee.
- Reconnecting without checking duplicate active operations and listener leaks.
- Testing locally while ignoring proxy idle timeouts, TLS, load balancers, and deployment restarts.
- Authenticating only the connection and never defining expiry or revocation behavior.
- Running unbounded backpressure or connection tests in a shared environment.
- Logging tokens, initialization payloads, or complete private event bodies.
Conclusion
Testing GraphQL subscriptions requires a layered oracle: protocol state, GraphQL operation state, event routing, and committed business state. Define the transport and delivery guarantees, wait for real operation readiness, test matching and nonmatching events, and verify lifecycle cleanup under cancellation and reconnect.
Start with one subscription and two tenants. Automate a valid delivery, a cross-tenant silence case, an unsubscribe cleanup check, and one reconnect scenario using the production transport. Once those are deterministic, expand into nullability, replay, backpressure, proxy behavior, and resilience.
Interview Questions and Answers
How does testing subscriptions differ from testing GraphQL queries?
Subscriptions create a long-lived response stream, while a query normally produces one result. I add lifecycle, multiple events, filtering, silence, cancellation, reconnect, reliability, and cleanup to normal GraphQL document and payload assertions.
Does GraphQL prescribe WebSockets?
No. GraphQL specifies subscription execution but leaves transport to implementations and related protocols. I identify whether the product uses WebSockets, SSE, or a vendor transport before designing frame tests.
What does connection acknowledgment mean?
It means the transport connection initialization was accepted. It does not always prove that a particular subscription operation is active. Tests need an operation-ready signal before publishing the event.
How do you test subscription filtering?
I publish events just inside and outside each filter boundary with unique IDs. I require matching delivery and bounded silence for different resources, tenants, states, and unauthorized filters. Server-derived identity must constrain client arguments.
How do you test event ordering?
First I document the ordering scope, such as per object or partition. I publish versioned events in a controlled order and assert only the promised scope. I do not demand global order without an architectural guarantee.
How do you test duplicate events?
I redeliver the same event ID in a test environment and assert deduplication at the documented server or client layer. Where delivery is at least once, I verify the consumer remains idempotent.
How do you test reconnect behavior?
I force an abrupt close, observe bounded retry and reauthentication, and verify the operation is recreated exactly once. I publish after recovery and check replay, duplicates, gaps, listener count, and cache reconciliation.
What observability is useful for subscription testing?
I correlate connection ID, operation ID, event ID, object version, safe principal, authorization decision, publish, match, execution, send, receipt, and close code. Metrics avoid high-cardinality private labels and logs omit tokens and full payloads.
Frequently Asked Questions
Do GraphQL subscriptions always use WebSockets?
No. GraphQL defines subscription execution semantics but not one mandatory transport. A service may use `graphql-transport-ws`, SSE, or a vendor protocol, and QA should test the deployed contract.
What should a GraphQL subscription test assert?
Assert schema and document behavior, transport lifecycle, identity, authorization, event matching, payload selection, errors, ordering guarantees, cancellation, reconnect, and resource cleanup. Verify the underlying business state independently.
Why do GraphQL subscription tests become flaky?
A common race publishes before the operation listener is active, while tests rely on arbitrary sleeps. Use explicit connection and operation readiness, unique events, and bounded timeouts only for expected silence.
How do you test GraphQL subscription authorization?
Subscribe two controlled users to their allowed resources, publish matching and cross-tenant events, and assert delivery only to the authorized operation. Also test token expiry and permission revocation during the stream.
How should reconnect and replay be tested?
Disconnect between known event IDs, publish while offline, reconnect, and assert the documented best-effort, replay, or resume behavior. Check duplicates at the boundary and ensure only one live operation remains.
What is the difference between a protocol error and a GraphQL error?
A protocol or connection error affects framing or socket lifecycle. GraphQL validation may reject one operation, while field execution errors can arrive in a `next` result with partial data. Clients and tests must handle each channel.
How do you test subscription cleanup?
Unsubscribe or close the socket, publish another matching event, and require no delivery. Instrument listener, operation, buffer, and task counts so they return to baseline across repeated cycles.