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
85 changes: 85 additions & 0 deletions uts/rest/integration/time_stats.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,88 @@ result = AWAIT client.stats(
ASSERT result IS PaginatedResult
ASSERT result.items.length <= 5
```

---

## RSC6 - stats() returns the flattened entries API

**Test ID**: `rest/integration/RSC6/stats-flattened-entries-2`

**Spec requirement:** RSC6, TS12 - a `Stats` datapoint exposes the flattened
API: `intervalId` (TS12a), `unit` (TS12c), `entries` (TS12r), `schema` (TS12s),
and `appId` (TS12t). `entries` is a flat map keyed by dotted metric path.

Known statistics are injected via the sandbox stats-injection endpoint (G3),
then read back and asserted against the flattened shape. This guards against a
client silently falling back to the deprecated deeply-nested Stats structure:
such a client would deserialize the response into empty/absent `entries` and
fail these assertions.

> **Note on the injection shape.** The `POST /stats` body uses the *ingestion*
> form — deeply nested per-type (`inbound.realtime.messages.count`, ...) — which
> is distinct from the flattened `entries` form returned by a read. On read-back
> the ingested `inbound`/`outbound`/`all` groups appear under a `messages.`
> prefix, e.g. ingested `inbound.realtime.messages.count` is read back as the
> entry key `messages.inbound.realtime.messages.count`. The `X-Ably-Version: 6`
> header is required both to inject and, on read, to receive the flattened API.

### Setup
```pseudo
# A dedicated app is not required: the chosen interval is in the previous year,
# so it is complete (never "in progress") and untouched by other tests' traffic.
year = current_utc_year() - 1

# Ingestion-shape fixtures for three consecutive minute intervals.
fixtures = [
{ intervalId: "{year}-02-03:15:03",
inbound: { realtime: { messages: { count: 50, data: 5000 } } },
outbound: { realtime: { messages: { count: 20, data: 2000 } } } },
{ intervalId: "{year}-02-03:15:04",
inbound: { realtime: { messages: { count: 60, data: 6000 } } },
outbound: { realtime: { messages: { count: 10, data: 1000 } } } },
{ intervalId: "{year}-02-03:15:05",
inbound: { realtime: { messages: { count: 70, data: 7000 } } },
outbound: { realtime: { messages: { count: 40, data: 4000 } } } },
]

# Inject via the authenticated stats endpoint (G3).
POST https://sandbox.realtime.ably-nonprod.net/stats
WITH Authorization: Basic {api_key}
WITH header X-Ably-Version: 6
WITH body from fixtures

client = Rest(options: ClientOptions(
key: api_key,
endpoint: "nonprod:sandbox"
))
```

### Test Steps
```pseudo
# Injected stats can lag briefly; poll until all three intervals appear
# (deadline ~30s).
REPEAT UNTIL result.items.length == 3 OR deadline_reached:
result = AWAIT client.stats(
start: "{year}-02-03:15:03",
end: "{year}-02-03:15:05",
direction: "forwards"
)
```

### Assertions
```pseudo
ASSERT result.items.length == 3

FOR i, item IN result.items:
ASSERT item.intervalId == "{year}-02-03:15:0{i + 3}"
ASSERT item.unit == "minute" # TS12c
ASSERT item.schema IS String # TS12s
ASSERT item.appId IS String # TS12t
ASSERT item.intervalTime() IS DateTime # TS12p (parsed from intervalId)

# entries (TS12r) is a flat dotted-path map; sum across the three intervals.
inbound = SUM(item.entries["messages.inbound.realtime.messages.count"] FOR item IN result.items)
outbound = SUM(item.entries["messages.outbound.realtime.messages.count"] FOR item IN result.items)
ASSERT inbound == 50 + 60 + 70
ASSERT outbound == 20 + 10 + 40
```
32 changes: 22 additions & 10 deletions uts/rest/unit/stats.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Stats API Tests

Spec points: `RSC6`, `RSC6a`, `RSC6b1`, `RSC6b2`, `RSC6b3`, `RSC6b4`
Spec points: `RSC6`, `RSC6a`, `RSC6b1`, `RSC6b2`, `RSC6b3`, `RSC6b4`, `TS12`, `TS12a`, `TS12c`, `TS12p`, `TS12r`

## Test Type
Unit test with mocked HTTP client
Expand All @@ -26,21 +26,27 @@ Tests that `stats()` makes a GET request to `/stats` and returns a PaginatedResu
### Setup
```pseudo
captured_requests = []
# TS12: the flattened Stats shape — intervalId, unit, and a flat `entries`
# map keyed by dotted metric path (plus optional schema/appId/inProgress).
# The deep per-type structure (all/inbound/outbound/… → messages/presence →
# count/data) was deprecated and removed as of specification version 2.2.
stats_data = [
{
"intervalId": "2024-01-01:00:00",
"unit": "hour",
"all": {
"messages": {"count": 100, "data": 5000},
"all": {"count": 100, "data": 5000}
"schema": "https://schemas.ably.com/json/app-stats-0.0.1.json",
"appId": "abcd1234",
"entries": {
"messages.all.all.count": 100,
"messages.all.all.data": 5000
}
},
{
"intervalId": "2024-01-01:01:00",
"unit": "hour",
"all": {
"messages": {"count": 150, "data": 7500},
"all": {"count": 150, "data": 7500}
"entries": {
"messages.all.all.count": 150,
"messages.all.all.data": 7500
}
}
]
Expand Down Expand Up @@ -68,10 +74,16 @@ result = AWAIT client.stats()
ASSERT result IS PaginatedResult
ASSERT result.items.length == 2

# Stats objects should have correct fields
ASSERT result.items[0].intervalId == "2024-01-01:00:00"
ASSERT result.items[0].unit == "hour"
# Stats objects should have the correct fields (TS12)
ASSERT result.items[0].intervalId == "2024-01-01:00:00" # TS12a
ASSERT result.items[0].unit == "hour" # TS12c
# TS12r: statistics are exposed as a flat `entries` map
ASSERT result.items[0].entries["messages.all.all.count"] == 100
ASSERT result.items[0].entries["messages.all.all.data"] == 5000
# TS12p: intervalTime is parsed from intervalId
ASSERT result.items[0].intervalTime == Time(2024, 1, 1, 0, 0, 0, UTC)
ASSERT result.items[1].intervalId == "2024-01-01:01:00"
ASSERT result.items[1].entries["messages.all.all.count"] == 150

# Verify correct endpoint and method
ASSERT captured_requests.length == 1
Expand Down
Loading