Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 5 additions & 2 deletions uts/realtime/unit/connection/connection_failures_test.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,12 @@ AWAIT_STATE client.connection.state == ConnectionState.failed
# Connection transitioned to FAILED
ASSERT client.connection.state == ConnectionState.failed

# Error reason is set
# Error reason is set. Per RSA4a2, the SDK substitutes 40171 ("no means to
# renew the token") for the server's 40142, having detected it has no way to
# renew (no key, authCallback or authUrl). Matches the proxy integration spec
# (connection_resume.md, RTN15h1 note) and error_reason_test.md (RTN25).
ASSERT client.connection.errorReason IS NOT null
ASSERT client.connection.errorReason.code == 40142
ASSERT client.connection.errorReason.code == 40171
ASSERT client.connection.errorReason.statusCode == 401
CLOSE_CLIENT(client)
```
Expand Down
106 changes: 63 additions & 43 deletions uts/realtime/unit/presence/realtime_presence_enter.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@ and `leaveClient` functions. These methods send PRESENCE ProtocolMessages to the
and handle ACK/NACK responses. Tests cover protocol message format, implicit channel
attach, connection state conditions, and error cases.

**Note on wildcard clientId:** Several tests use `clientId: "*"` (wildcard) which is
the Ably convention for clients permitted to act on behalf of any clientId via
`enterClient`/`updateClient`/`leaveClient`. Some SDKs may reject `"*"` at the
`ClientOptions` construction level. In such cases, adapt these tests to use a
concrete clientId (e.g., `"admin"`) and skip the client-side `enterClient` clientId
mismatch check (RTP15f), or configure the mock to accept any clientId.
**Note on acting on behalf of other clients:** Tests that use
`enterClient`/`updateClient`/`leaveClient` for arbitrary clientIds use an
*unidentified* client (key auth with no `clientId`), which is permitted to act
on behalf of any clientId. Per RSA7c, `"*"` is a reserved value for
`ClientOptions#clientId` and must be rejected at construction time — the
wildcard-clientId state referenced by RTP8j arises only from token auth with a
wildcard (`clientId: "*"`) token. Per RTP15f, an *identified* client calling
`enterClient` with a different clientId results in an error (client-side or via
server NACK).

---

Expand Down Expand Up @@ -280,9 +283,6 @@ CLOSE_CLIENT(client)

### Setup

Note: Some SDKs may reject wildcard clientId `"*"` at the `ClientOptions`
construction level rather than at `enter()` time. In that case, this test
validates that the error occurs at `ClientOptions` creation instead.
```pseudo
channel_name = "test-RTP8j-wild-${random_id()}"

Expand All @@ -295,10 +295,13 @@ mock_ws = MockWebSocket(
)
install_mock(mock_ws)

# Wildcard clientId
# Note: clientId "*" may not be accepted by all SDKs at construction time.
# See top-level note for alternative auth patterns (e.g., key auth without clientId).
client = Realtime(options: ClientOptions(key: "fake.key:secret", clientId: "*", autoConnect: false))
# Wildcard clientId via a wildcard token. RSA7c reserves "*" as a
# ClientOptions#clientId value (construction must reject it), so the RTP8j
# wildcard state arises from token auth with a token whose clientId is "*".
client = Realtime(options: ClientOptions(
tokenDetails: TokenDetails(token: "wildcard-token", expires: now() + 3600000, clientId: "*"),
autoConnect: false
))
channel = client.channels.get(channel_name)
```

Expand Down Expand Up @@ -542,9 +545,10 @@ mock_ws = MockWebSocket(
)
install_mock(mock_ws)

# Note: clientId "*" may not be accepted by all SDKs at construction time.
# See top-level note for alternative auth patterns (e.g., key auth without clientId).
client = Realtime(options: ClientOptions(key: "fake.key:secret", clientId: "*", autoConnect: false))
# Unidentified client (key auth, no clientId): permitted to act on behalf of
# any clientId via enterClient/updateClient/leaveClient. (RSA7c reserves "*"
# as a ClientOptions#clientId value, so it must not be used here.)
client = Realtime(options: ClientOptions(key: "fake.key:secret", autoConnect: false))
channel = client.channels.get(channel_name)
```

Expand Down Expand Up @@ -601,9 +605,10 @@ mock_ws = MockWebSocket(
)
install_mock(mock_ws)

# Note: clientId "*" may not be accepted by all SDKs at construction time.
# See top-level note for alternative auth patterns (e.g., key auth without clientId).
client = Realtime(options: ClientOptions(key: "fake.key:secret", clientId: "*", autoConnect: false))
# Unidentified client (key auth, no clientId): permitted to act on behalf of
# any clientId via enterClient/updateClient/leaveClient. (RSA7c reserves "*"
# as a ClientOptions#clientId value, so it must not be used here.)
client = Realtime(options: ClientOptions(key: "fake.key:secret", autoConnect: false))
channel = client.channels.get(channel_name)
```

Expand Down Expand Up @@ -661,9 +666,10 @@ mock_ws = MockWebSocket(
)
install_mock(mock_ws)

# Note: clientId "*" may not be accepted by all SDKs at construction time.
# See top-level note for alternative auth patterns (e.g., key auth without clientId).
client = Realtime(options: ClientOptions(key: "fake.key:secret", clientId: "*", autoConnect: false))
# Unidentified client (key auth, no clientId): permitted to act on behalf of
# any clientId via enterClient/updateClient/leaveClient. (RSA7c reserves "*"
# as a ClientOptions#clientId value, so it must not be used here.)
client = Realtime(options: ClientOptions(key: "fake.key:secret", autoConnect: false))
channel = client.channels.get(channel_name)
```

Expand Down Expand Up @@ -931,33 +937,44 @@ mock_ws = MockWebSocket(
)
install_mock(mock_ws)

# Wildcard clientId to allow both enter() and enterClient() on the same connection.
# See note in Purpose section about SDK-level wildcard validation.
# Note: clientId "*" may not be accepted by all SDKs at construction time.
# See top-level note for alternative auth patterns (e.g., key auth without clientId).
client = Realtime(options: ClientOptions(key: "fake.key:secret", clientId: "*", autoConnect: false))
channel = client.channels.get(channel_name)
# Two clients: no single clientId permits both operations on a compliant SDK.
# Per RTP8j, a wildcard (or null) clientId means plain enter() errors
# immediately; per RTP15f, an identified client's enterClient() with a
Comment on lines 938 to +942
# different clientId indicates an error. So the normally-entered member is an
# identified client, and the enterClient/leaveClient operations come from a
# second, unidentified (key-auth) client — mirroring the real-world shape of
# an end user plus a server-side process acting on behalf of other users.
client_main = Realtime(options: ClientOptions(key: "fake.key:secret", clientId: "main-user", autoConnect: false))
channel_main = client_main.channels.get(channel_name)

client_ops = Realtime(options: ClientOptions(key: "fake.key:secret", autoConnect: false))
channel_ops = client_ops.channels.get(channel_name)
```

### Test Steps
```pseudo
client.connect()
AWAIT_STATE client.connection.state == ConnectionState.connected
AWAIT channel.attach()
client_main.connect()
AWAIT_STATE client_main.connection.state == ConnectionState.connected
AWAIT channel_main.attach()

client_ops.connect()
AWAIT_STATE client_ops.connection.state == ConnectionState.connected
AWAIT channel_ops.attach()

# Normal enter for the wildcard client
AWAIT channel.presence.enter(data: "main-client")
# Normal enter for the identified main client
AWAIT channel_main.presence.enter(data: "main-client")

# enterClient for a different user
AWAIT channel.presence.enterClient("other-user", data: "other-data")
# enterClient for a different user, from the unidentified client
AWAIT channel_ops.presence.enterClient("other-user", data: "other-data")

# leaveClient for the other user
AWAIT channel.presence.leaveClient("other-user")
AWAIT channel_ops.presence.leaveClient("other-user")
```

### Assertions
```pseudo
# Three presence messages sent: enter, enterClient, leaveClient
# (each operation is awaited, so the capture order is deterministic)
ASSERT captured_presence.length == 3

# The main client's enter is unaffected by the enterClient/leaveClient calls
Expand All @@ -971,7 +988,8 @@ ASSERT captured_presence[1].presence[0].clientId == "other-user"
ASSERT captured_presence[2].presence[0].action == LEAVE
ASSERT captured_presence[2].presence[0].clientId == "other-user"

CLOSE_CLIENT(client)
CLOSE_CLIENT(client_main)
CLOSE_CLIENT(client_ops)
```

---
Expand Down Expand Up @@ -1033,9 +1051,10 @@ mock_ws = MockWebSocket(
)
install_mock(mock_ws)

# Note: clientId "*" may not be accepted by all SDKs at construction time.
# See top-level note for alternative auth patterns (e.g., key auth without clientId).
client = Realtime(options: ClientOptions(key: "fake.key:secret", clientId: "*", autoConnect: false))
# Unidentified client (key auth, no clientId): permitted to act on behalf of
# any clientId via enterClient/updateClient/leaveClient. (RSA7c reserves "*"
# as a ClientOptions#clientId value, so it must not be used here.)
client = Realtime(options: ClientOptions(key: "fake.key:secret", autoConnect: false))
channel = client.channels.get(channel_name)
```

Expand Down Expand Up @@ -1151,9 +1170,10 @@ mock_ws_b = MockWebSocket(
install_mock(mock_ws_a, client: "A")
install_mock(mock_ws_b, client: "B")

# Note: clientId "*" may not be accepted by all SDKs at construction time.
# See top-level note for alternative auth patterns (e.g., key auth without clientId).
client_a = Realtime(options: ClientOptions(key: "fake.key:secret", clientId: "*", autoConnect: false))
# Two unidentified clients (key auth, no clientId): permitted to act on behalf
# of any clientId via enterClient. (RSA7c reserves "*" as a
# ClientOptions#clientId value, so it must not be used here.)
client_a = Realtime(options: ClientOptions(key: "fake.key:secret", autoConnect: false))
client_b = Realtime(options: ClientOptions(key: "fake.key:secret", autoConnect: false))
channel_a = client_a.channels.get(channel_name)
channel_b = client_b.channels.get(channel_name)
Expand Down
9 changes: 5 additions & 4 deletions uts/realtime/unit/presence/realtime_presence_reentry.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,10 +160,11 @@ mock_ws = MockWebSocket(
)
install_mock(mock_ws)

# Use a non-wildcard clientId that has enterClient permission.
# Note: Some SDKs reject wildcard clientId "*" at the ClientOptions level.
# Use a concrete clientId and rely on server-side permission for enterClient.
client = Realtime(options: ClientOptions(key: "fake.key:secret", clientId: "admin", autoConnect: false))
# Unidentified client (key auth, no clientId): permitted to act on behalf of
# any clientId via enterClient. (RSA7c reserves "*" as a ClientOptions#clientId
# value, and per RTP15f an identified client cannot enterClient a different
# clientId.)
client = Realtime(options: ClientOptions(key: "fake.key:secret", autoConnect: false))
channel = client.channels.get(channel_name)
```

Expand Down
9 changes: 6 additions & 3 deletions uts/rest/unit/presence/rest_presence.md
Original file line number Diff line number Diff line change
Expand Up @@ -1221,8 +1221,10 @@ channel_name = "test-RSP5g-${random_id()}"
captured_requests = []
cipher_key = base64_decode("WUP6u0K7MXI5Zeo0VppPwg==")

# Encrypted data for {"secret":"data"}
encrypted_data = "HO4cYSP8LybPYBPZPHQOtuD53yrD3YV3NBoTEYBh4U0="
# Canonical ably-common fixture (test-resources/crypto-data-128.json):
# the encryption of the JSON object {"example":{"json":"Object"}} under the
# key above. 48 bytes: 16-byte IV followed by two ciphertext blocks.
encrypted_data = "HO4cYSP8LybPYBPZPHQOtuD53yrD3YV3NBoTEYBh4U0N1QXHbtkfsDfTspKeLQFt"

mock_http = MockHttpClient(
onConnectionAttempt: (conn) => conn.respond_with_success(),
Expand Down Expand Up @@ -1254,7 +1256,8 @@ result = AWAIT channel.presence.get()
### Assertions
```pseudo
ASSERT result.items[0].data IS Object/Map
# Decryption applied based on cipher+aes-128-cbc encoding
# Decryption applied based on cipher+aes-128-cbc encoding, then JSON decoding
ASSERT result.items[0].data["example"]["json"] == "Object"
```

---
Expand Down
Loading