Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
7bcbd92
Add UTS specs for Realtime connection lifecycle
paddybyers Mar 30, 2026
aa17d10
Fix connection test specs to close WebSocket transport when necessary
paddybyers Mar 30, 2026
0cf678c
Refactor realtime test specs: extract mock WebSocket helper and add s…
paddybyers Mar 30, 2026
e914b8d
Refactor test specs to use EXPECT_THROW instead of TRY/CATCH
paddybyers Mar 30, 2026
f7e7a6e
Use unique channel names in test specs to prevent cross-test interfer…
paddybyers Mar 30, 2026
2ad36e9
Rewrite heartbeat test specs and extend mock WebSocket helper
paddybyers Mar 30, 2026
4924343
Fix RTN15a (immediate reconnection) test approach and update skill
paddybyers Mar 30, 2026
5299cbb
Fix minor issues in channel attach and state events test specs
paddybyers Mar 30, 2026
a5438a2
Add test specs for connection state transitions and channel properties
paddybyers Mar 30, 2026
7d8cd91
Add test specs for realtime channel subscribe (RTL7/RTL8)
paddybyers Mar 30, 2026
81fec16
Add test specs for realtime channel publish (RTL6)
paddybyers Mar 30, 2026
0c6b330
Add realtime entries for stats() and time() referencing existing REST…
paddybyers Mar 30, 2026
1b7778c
Add test specs for Realtime.request() (RTN18)
paddybyers Mar 30, 2026
6622edc
Extend realtime publish tests: queued messages and state transitions
paddybyers Mar 30, 2026
0f010d4
Add test specs for RSN1-4 (connection recovery) and RTL10 (realtime h…
paddybyers Mar 30, 2026
4482d5f
Add test specs for client logging (LOG1-LOG3)
paddybyers Mar 30, 2026
156301d
Add remaining auth test specs
paddybyers Mar 30, 2026
31e3b3f
Add test specs for realtime presence (RTP)
paddybyers Mar 30, 2026
bce930b
Fix path component encoding in test specs to use encode_uri_component()
paddybyers Mar 30, 2026
15e211e
Update presence test specs and add integration tests
paddybyers Mar 30, 2026
a0df069
Update write-test-spec skill to emphasise keeping specs in sync
paddybyers Mar 30, 2026
3de2c04
Add test specs for batch presence (RSP4)
paddybyers Mar 30, 2026
dc783aa
Add test specs for token revocation (RSA10)
paddybyers Mar 30, 2026
63e36b4
Add test specs for RTL12 (channel UPDATE event handling)
paddybyers Mar 30, 2026
fed299b
Add test specs for VCDIFF delta message encoding (RTL18/RTL19)
paddybyers Mar 30, 2026
9244325
Add test specs for channel attributes, whenState, timeouts, and auto-…
paddybyers Mar 30, 2026
ec858b3
Add test specs for mutable messages (RTL22/RTL23)
paddybyers Mar 30, 2026
66b1b72
Add test specs for push admin (RSH1/RSH7)
paddybyers Mar 30, 2026
6420ec4
Fix presence test specs: server echoes, wildcard clientId, RTL13b, RT…
paddybyers Mar 30, 2026
14b7d80
Fix integration test specs based on sandbox behavior
paddybyers Mar 30, 2026
9eb85c8
Add CLOSE_CLIENT(client) to all UTS specs that create Realtime clients
paddybyers Apr 30, 2026
1803464
UTS spec corrections from ably-js audit
paddybyers May 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
932 changes: 932 additions & 0 deletions uts/.claude/skills/write-test-spec.md

Large diffs are not rendered by default.

417 changes: 417 additions & 0 deletions uts/completion-status.md

Large diffs are not rendered by default.

255 changes: 255 additions & 0 deletions uts/realtime/integration/auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
# Realtime Auth Integration Tests

Spec points: `RTC8`, `RSA8`, `RSA7`

## Test Type
Integration test against Ably sandbox

## Token Formats

Tests use JWTs generated using a third-party JWT library, signed with the app key secret using HMAC-SHA256.

## Sandbox Setup

Tests run against the Ably Sandbox at `https://sandbox-rest.ably.io`.

### App Provisioning

```pseudo
BEFORE ALL TESTS:
response = POST https://sandbox-rest.ably.io/apps
WITH body from ably-common/test-resources/test-app-setup.json

app_config = parse_json(response.body)
api_key = app_config.keys[0].key_str
app_id = app_config.app_id

AFTER ALL TESTS:
DELETE https://sandbox-rest.ably.io/apps/{app_id}
WITH Authorization: Basic {api_key}
```

---

## RTC8a - In-band reauthorization on CONNECTED client

**Spec requirement:** RTC8a - When `auth.authorize()` is called on a CONNECTED realtime client, it sends an AUTH protocol message with the new token rather than disconnecting/reconnecting.

Tests that calling authorize() on a connected client succeeds and the connection remains connected (UPDATE event, not disconnect/reconnect).

### Setup
```pseudo
auth_callback = FUNCTION(params):
RETURN generate_jwt(
key_name: extract_key_name(api_key),
key_secret: extract_key_secret(api_key),
ttl: 3600000
)

client = Realtime(options: ClientOptions(
authCallback: auth_callback,
endpoint: "sandbox",
autoConnect: false
))
```

### Test Steps
```pseudo
# Connect and wait for CONNECTED
client.connect()
AWAIT_STATE client.connection.state == CONNECTED

# Record connection ID before reauth
connection_id_before = client.connection.id

# Collect state changes during reauth
state_changes = []
subscription = client.connection.on(LISTEN state_changes.append)

# Call authorize — should send AUTH and get UPDATE, not disconnect
token = AWAIT client.auth.authorize()

# Check state after reauth
connection_id_after = client.connection.id
```

### Assertions
```pseudo
# authorize() returned a valid token
ASSERT token IS NOT NULL
ASSERT token.token IS String

# Connection remained connected — same connection ID
ASSERT connection_id_after == connection_id_before

# No state transitions occurred (UPDATE has current == previous == connected,
# so filtering for actual transitions should yield nothing)
state_transitions = state_changes.filter(c => c.current != c.previous)
ASSERT state_transitions IS EMPTY

AWAIT client.close()
```

---

## RTC8c - authorize() from INITIALIZED initiates connection

**Spec requirement:** RTC8c - When `auth.authorize()` is called on a client in INITIALIZED state, it should initiate the connection.

Tests that calling authorize() on an unconnected client triggers a connection.

### Setup
```pseudo
auth_callback = FUNCTION(params):
RETURN generate_jwt(
key_name: extract_key_name(api_key),
key_secret: extract_key_secret(api_key),
ttl: 3600000
)

client = Realtime(options: ClientOptions(
authCallback: auth_callback,
endpoint: "sandbox",
autoConnect: false
))
```

### Test Steps
```pseudo
# Client starts in INITIALIZED, no connection
ASSERT client.connection.state == INITIALIZED

# authorize() should trigger connection
token = AWAIT client.auth.authorize()

# Wait for connection to be established
AWAIT_STATE client.connection.state == CONNECTED
```

### Assertions
```pseudo
ASSERT token IS NOT NULL
ASSERT client.connection.state == CONNECTED
ASSERT client.connection.id IS NOT NULL

AWAIT client.close()
```

---

## RSA8 - Token auth on realtime connection

**Spec requirement:** RSA8 - Realtime client can connect using token authentication via an authCallback that returns JWTs.

Tests that a realtime client can connect using JWT-based token auth.

### Setup
```pseudo
auth_callback = FUNCTION(params):
RETURN generate_jwt(
key_name: extract_key_name(api_key),
key_secret: extract_key_secret(api_key),
ttl: 3600000
)

client = Realtime(options: ClientOptions(
authCallback: auth_callback,
endpoint: "sandbox",
autoConnect: false
))
```

### Test Steps
```pseudo
client.connect()
AWAIT_STATE client.connection.state == CONNECTED
```

### Assertions
```pseudo
ASSERT client.connection.state == CONNECTED
ASSERT client.connection.id IS NOT NULL
ASSERT client.connection.errorReason IS NULL

AWAIT client.close()
```

---

## RSA7 - clientId validation on realtime connection

**Spec requirement:** RSA7 - The server validates clientId consistency between token claims and connection parameters.

Tests that:
1. A JWT with a clientId allows connection with matching clientId
2. A JWT with a clientId rejects connection with mismatched clientId

### Test 1: Matching clientId succeeds

#### Setup
```pseudo
test_client_id = "test-client-" + random_id()

auth_callback = FUNCTION(params):
RETURN generate_jwt(
key_name: extract_key_name(api_key),
key_secret: extract_key_secret(api_key),
client_id: test_client_id,
ttl: 3600000
)

client = Realtime(options: ClientOptions(
authCallback: auth_callback,
clientId: test_client_id,
endpoint: "sandbox",
autoConnect: false
))
```

#### Test Steps
```pseudo
client.connect()
AWAIT_STATE client.connection.state == CONNECTED
```

#### Assertions
```pseudo
ASSERT client.connection.state == CONNECTED
ASSERT client.auth.clientId == test_client_id

AWAIT client.close()
```

### Test 2: Mismatched clientId fails

#### Setup
```pseudo
auth_callback = FUNCTION(params):
RETURN generate_jwt(
key_name: extract_key_name(api_key),
key_secret: extract_key_secret(api_key),
client_id: "token-client-id",
ttl: 3600000
)
```

#### Test Steps
```pseudo
# ClientOptions constructor should reject mismatched clientId
# The clientId in options ("wrong-client-id") doesn't match the token's clientId
# This is validated client-side per RSA7
EXPECT THROW creating Realtime(options: ClientOptions(
authCallback: auth_callback,
clientId: "wrong-client-id",
endpoint: "sandbox",
autoConnect: false
))
```

#### Assertions
```pseudo
# Note: The mismatch is detected client-side when the token is obtained.
# The exact behavior depends on implementation: it may throw during
# authorize() or during token validation. The key assertion is that
# the connection enters FAILED state with error code 40102.
```
109 changes: 109 additions & 0 deletions uts/realtime/integration/channel_history_test.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# RealtimeChannel History Integration Test

Spec points: `RTL10d`

## Test Type
Integration test against Ably Sandbox endpoint

## Sandbox Setup

Tests run against the Ably Sandbox at `https://sandbox-rest.ably.io`.

### App Provisioning

```pseudo
BEFORE ALL TESTS:
response = POST https://sandbox-rest.ably.io/apps
WITH body from ably-common/test-resources/test-app-setup.json

app_config = parse_json(response.body)
api_key = app_config.keys[0].key_str
app_id = app_config.app_id

AFTER ALL TESTS:
DELETE https://sandbox-rest.ably.io/apps/{app_id}
WITH Authorization: Basic {api_key}
```

---

## RTL10d - History contains messages published by another client

| Spec | Requirement |
|------|-------------|
| RTL10d | A test should exist that publishes messages from one client, and upon confirmation of message delivery, a history request should be made on another client to ensure all messages are available |

Tests that messages published by one Realtime client are available in the history retrieved by a separate client.

### Setup

```pseudo
channel_name = "history-RTL10d-" + random_id()

publisher = Realtime(options: ClientOptions(
key: api_key,
endpoint: "sandbox"
))

subscriber = Realtime(options: ClientOptions(
key: api_key,
endpoint: "sandbox"
))

publisher.connect()
subscriber.connect()

AWAIT_STATE publisher.connection.state == ConnectionState.connected
WITH timeout: 10 seconds
AWAIT_STATE subscriber.connection.state == ConnectionState.connected
WITH timeout: 10 seconds

pub_channel = publisher.channels.get(channel_name)
sub_channel = subscriber.channels.get(channel_name)

AWAIT pub_channel.attach()
AWAIT sub_channel.attach()
```

### Test Steps

```pseudo
# Publish messages from publisher client and await confirmation
AWAIT pub_channel.publish(name: "event1", data: "data1")
AWAIT pub_channel.publish(name: "event2", data: "data2")
AWAIT pub_channel.publish(name: "event3", data: "data3")

# Retrieve history from subscriber client
# Poll until all messages appear
history = poll_until(
condition: FUNCTION() =>
result = AWAIT sub_channel.history()
RETURN result.items.length == 3,
interval: 500ms,
timeout: 10s
)
```

### Assertions

```pseudo
ASSERT history.items.length == 3

# Default order is backwards (newest first)
ASSERT history.items[0].name == "event3"
ASSERT history.items[0].data == "data3"

ASSERT history.items[1].name == "event2"
ASSERT history.items[1].data == "data2"

ASSERT history.items[2].name == "event1"
ASSERT history.items[2].data == "data1"
```

### Cleanup

```pseudo
AFTER TEST:
publisher.close()
subscriber.close()
```
Loading
Loading