support for 202507 endpoints (#37)

* [auth][api] accept the 20250424.01 client

* [2025] unstable

* 20250718.0

* correct one this time

* stubs

* more stubs

* more stubs

* [lists] add worker

* [ai] route stubs

* [api] player photo setting

* [econ] add roomEconConfig route

* [infra] update worker generators

* [worker] add cards/moderation/platformnotification workers

* [lists] updates to some endpoints

* [clubs] stub out announcement endpoint, for now

* [econ] stub out season endpoints for now

* [chat] apps/chat stub out party endpoint not sure the shape yet

* [api] stub out statsig and lockeditems

* [doc] new services

* [lists] stub the bulk endpoint

* [datacollection] add placeholder service until we can kill it

* [api] set gifting to lvl5

* update lock

* [cdn] enable cache

* [match] matchmake v2

* [lists] stub some lists

* [ai] stubs

* [rooms] new subroom save endpoint

* [econ] add bulk purchase endpoint

* [discovery] update featured creator to 1 for fun

* [api] add photo settings flag

* [chat] fixup chat permissions (sorta)

* [auth] restrictions endpoint

* [rooms] contributed endpoint

* [api] fix outfit endpoint

* [discovery] attempt to fix store

* [chat] privacy endpoints

* [api] cheered images

* [rooms] add xp endpoint (disbaled)

* [rooms] add xp endpoint (disabled)

* update images-db for cheers

* [rooms] add autocomplete endpoint

* [cdn/img] increase cache ttl for statics

* [api] bulk route for images

* [accounts] add banner image

* [api] add misc missing endpoints

* [discovery] remove AI tab

* [platformnotifications] stub some endpoints

* [lists] add some more lists

* [rooms] additional endpoints

* [chat] stub a few privacy endpoints

* [econ] stub some endpoints

* misc db fixes

* [api] tweak shape for images v6

* [rooms] dont show trending RROs
This commit is contained in:
devin
2026-08-18 23:07:24 -04:00
committed by Devin Zuczek
parent 66c09806f9
commit 178d3b5b0e
162 changed files with 114930 additions and 469 deletions
+445 -13
View File
@@ -34,10 +34,22 @@ function b64url(input: ArrayBuffer | string): string {
}
// `roles` mints the `role` claim the auth worker stamps from an account's flags; left
// off, the token carries none — what a plain player's looks like to the role gates.
async function bearer(sub: string, roles?: string[]): Promise<Record<string, string>> {
async function bearer(
sub: string,
roles?: string[],
// The client build the token was minted for (`rn.ver`), which is what
// `/featuredrooms/current` gates on. Omitted by default, like a token from a grant that
// posted no `ver`.
version?: string
): Promise<Record<string, string>> {
const now = Math.floor(Date.now() / 1000)
const signingInput = `${b64url(JSON.stringify({ alg: 'HS256', typ: 'JWT' }))}.${b64url(
JSON.stringify({ sub, exp: now + 3600, ...(roles && { role: roles }) })
JSON.stringify({
sub,
exp: now + 3600,
...(roles && { role: roles }),
...(version && { 'rn.ver': version }),
})
)}`
const key = await crypto.subtle.importKey(
'raw',
@@ -89,6 +101,18 @@ beforeAll(async () => {
// Seed each room and split its subrooms into the subroom table (mirrors 0007's backfill).
for (const r of importRooms) await seedRoomWithSubRooms(env.DB, r as Record<string, unknown>)
// Accounts table (owned by the auth worker) — provisioning a dorm reads the username
// to name the room. Seed the player `dormroom/me` provisions a fresh dorm for.
await env.DB.prepare(
`CREATE TABLE IF NOT EXISTS account (
data TEXT NOT NULL,
account_id INTEGER GENERATED ALWAYS AS (json_extract(data, '$.accountId')) VIRTUAL
)`
).run()
await env.DB.prepare('INSERT OR IGNORE INTO account (data) VALUES (?1)')
.bind(JSON.stringify({ accountId: 999, username: 'Dormer' }))
.run()
// Relationship table (owned by the api worker) — `visitedby/:playerId` reads it to
// check the caller is a friend of the player whose history they're asking for.
await env.DB.prepare(
@@ -129,6 +153,44 @@ describe('rooms endpoints', () => {
expect(body.SubRooms[0].UnitySceneId).toBe('76d98498-60a1-430c-ab76-b54a29b7a163')
})
// Neither is stored — the seed blobs predate both keys — so they are defaulted on read.
// The client's room DTO always carries them, and an ABSENT key is not the same as a
// zero/null one to its parser.
it('GET /rooms/:id carries BoostCount and CurrentSnapshotId', async () => {
const res = await SELF.fetch(`${ORIGIN}/rooms/1`)
expect(res.status).toBe(200)
const body = (await res.json()) as Record<string, unknown>
expect(body).toHaveProperty('BoostCount', 0)
expect(body).toHaveProperty('CurrentSnapshotId', null)
})
// Pinned whole: these are the numbers the client's publish UI counts against, and
// `error: null` / `error_id` is a different envelope from the room mutations' — a
// "cleanup" that unified the two would break the client silently.
it('GET /publishState/configs returns the republish limits', async () => {
const res = await SELF.fetch(`${ORIGIN}/publishState/configs`)
expect(res.status).toBe(200)
expect(await res.json()).toEqual({
value: {
UpdateMaxCount: 3,
UpdateRollingWindowInDays: 365,
UpdateExpirationInDays: 30,
UpdateCooldownInDays: 45,
},
success: true,
error_id: null,
error: null,
})
})
// Stub. Registered (not 404) matters more than the body: the client asks for this on
// room entry, and an unregistered path stalls the load rather than erroring visibly.
it('GET /rooms/:id/experience/player returns [] for any room', async () => {
const res = await SELF.fetch(`${ORIGIN}/rooms/92/experience/player`)
expect(res.status).toBe(200)
expect(await res.json()).toEqual([])
})
it('GET /rooms/:id 404s for a room not in D1', async () => {
const res = await SELF.fetch(`${ORIGIN}/rooms/99999`)
expect(res.status).toBe(404)
@@ -163,6 +225,62 @@ describe('rooms endpoints', () => {
expect(body.map((r) => r.Name)).toEqual(['RecCenter'])
})
it('POST /rooms/bulk takes repeated id fields in a form body', async () => {
const post = async (body: string) =>
SELF.fetch(`${ORIGIN}/rooms/bulk`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body,
})
// The client's form: one `id` per room, plus the filter. An id that isn't in D1 is
// simply absent, so the answer can be shorter than the request.
const res = await post('id=1&id=2&id=999999&excludePrivateRooms=False')
expect(res.status).toBe(200)
const body = (await res.json()) as Array<{ RoomId: number; Name: string }>
expect(body.map((r) => r.Name).sort()).toEqual(['DormRoom', 'RecCenter'])
// Comma-separated values inside an `id` work too, as they do on the GET.
const commas = (await (await post('id=1,2')).json()) as Array<{ Name: string }>
expect(commas.map((r) => r.Name).sort()).toEqual(['DormRoom', 'RecCenter'])
// `excludePrivateRooms=True` drops the non-public rooms — the dorm here.
const publicOnly = (await (await post('id=1&id=2&excludePrivateRooms=True')).json()) as Array<{
Name: string
Accessibility: number
}>
expect(publicOnly.map((r) => r.Name)).toEqual(['RecCenter'])
expect(publicOnly.every((r) => r.Accessibility === 1)).toBe(true)
// No ids is an empty array, not a 400 — unlike the GET, which needs an `id` or `name`.
expect(await (await post('excludePrivateRooms=False')).json()).toEqual([])
// D1 binds one parameter per id and caps a query at 100, so a longer list is refused
// rather than split — a caller asking about more than a hundred rooms at once has lost
// track of what it is rendering.
const idList = (n: number) => Array.from({ length: n }, (_, i) => 500000 + i)
expect(
(
await post(
idList(100)
.map((id) => `id=${id}`)
.join('&')
)
).status
).toBe(200)
const overCap = await post(
idList(101)
.map((id) => `id=${id}`)
.join('&')
)
expect(overCap.status).toBe(400)
expect(await overCap.json()).toBe('At most 100 room ids may be looked up at once')
// The GET form has the same cap, counting the ids inside its comma-separated `id`.
const overCapGet = await SELF.fetch(`${ORIGIN}/rooms/bulk?id=${idList(101).join(',')}`)
expect(overCapGet.status).toBe(400)
})
it('GET /rooms/ownedby/me is auth-gated and scoped to the caller', async () => {
// No token → 401, no stub-account fallback (would otherwise leak account 1).
const noAuth = await SELF.fetch(`${ORIGIN}/rooms/ownedby/me`)
@@ -181,6 +299,49 @@ describe('rooms endpoints', () => {
expect(other).toEqual([])
})
it('GET /dormroom/me serves the callers dorm id, not the room', async () => {
// No token → 401. Without this the endpoint would hand out (and provision) a dorm
// for whichever account a fallback picked.
const noAuth = await SELF.fetch(`${ORIGIN}/dormroom/me`)
expect(noAuth.status).toBe(401)
// Account 1 owns the seeded dorm (RoomId 1). The body is that id ALONE — a bare
// JSON number, not the room and not an object wrapping the id.
const res = await SELF.fetch(`${ORIGIN}/dormroom/me`, { headers: await bearer('1') })
expect(res.status).toBe(200)
expect(await res.json()).toBe(1)
// It is the id of a room that really is the caller's dorm — the caller fetches the
// room itself from /rooms/{id}.
const room = (await (await SELF.fetch(`${ORIGIN}/rooms/1`)).json()) as {
RoomId: number
IsDorm: boolean
CreatorAccountId: number
}
expect(room).toMatchObject({ RoomId: 1, IsDorm: true, CreatorAccountId: 1 })
// A player who has never entered their dorm gets one provisioned rather than a
// 404 — the get-or-create still happens, only the payload shrank. And it belongs
// to THEM, not the template dorm they were cloned from.
const fresh = (await (
await SELF.fetch(`${ORIGIN}/dormroom/me`, { headers: await bearer('999') })
).json()) as number
expect(typeof fresh).toBe('number')
expect(fresh).not.toBe(1)
const provisioned = (await (await SELF.fetch(`${ORIGIN}/rooms/${fresh}`)).json()) as {
IsDorm: boolean
CreatorAccountId: number
}
expect(provisioned).toMatchObject({ IsDorm: true, CreatorAccountId: 999 })
// Idempotent: the second call is the same dorm, not a second one.
const again = (await (
await SELF.fetch(`${ORIGIN}/dormroom/me`, { headers: await bearer('999') })
).json()) as number
expect(again).toBe(fresh)
})
// The website's "My rooms" list is a browser calling this worker from another origin,
// so a response without CORS headers is one the browser throws away — and the page
// can't tell that apart from the server being down. Pinned on the preflight too: the
@@ -246,6 +407,102 @@ describe('rooms endpoints', () => {
expect(publicList.some((r) => r.Name === 'MyUnpublishedRoom')).toBe(false)
})
it('GET /rooms/contributedby/me lists rooms the caller has a role in, not their own', async () => {
const seed = (data: Record<string, unknown>) =>
env.DB.prepare('INSERT INTO room (data) VALUES (?1)').bind(JSON.stringify(data)).run()
// A room somebody else made, where 820 is a co-owner...
await seed({
RoomId: 30401,
Name: 'ContribCoOwner',
CreatorAccountId: 821,
Accessibility: 1,
SubRooms: [],
Roles: [
{ AccountId: 821, Role: 255 },
{ AccountId: 820, Role: 30 },
],
})
// ...one where they're only a host (every tier counts, not just owner-level)...
await seed({
RoomId: 30402,
Name: 'ContribHost',
CreatorAccountId: 821,
// Unpublished: a contributor works on the room before it goes public, so
// accessibility is not filtered here.
Accessibility: 0,
SubRooms: [],
Roles: [{ AccountId: 820, Role: 10 }],
})
// ...one they created themselves, whose Roles name them as Creator...
await seed({
RoomId: 30403,
Name: 'ContribOwn',
CreatorAccountId: 820,
Accessibility: 1,
SubRooms: [],
Roles: [{ AccountId: 820, Role: 255 }],
})
// ...one they have nothing to do with, and one with no Roles key at all (the older
// seeded rooms have none — json_each must drop them, not error).
await seed({
RoomId: 30404,
Name: 'ContribOther',
CreatorAccountId: 821,
Accessibility: 1,
SubRooms: [],
Roles: [{ AccountId: 822, Role: 30 }],
})
await seed({ RoomId: 30405, Name: 'ContribNoRoles', CreatorAccountId: 821, SubRooms: [] })
const res = await SELF.fetch(`${ORIGIN}/rooms/contributedby/me`, {
headers: await bearer('820'),
})
expect(res.status).toBe(200)
const rooms = (await res.json()) as Array<{ RoomId: number; Name: string }>
// A bare array of the canonical room DTO — no envelope, no paging wrapper.
expect(Array.isArray(rooms)).toBe(true)
expect(rooms.map((r) => r.RoomId).sort((a, b) => a - b)).toEqual([30401, 30402])
// The caller's OWN room is excluded, or this would just repeat createdby/me.
expect(rooms.some((r) => r.RoomId === 30403)).toBe(false)
expect(rooms[0]).toMatchObject({ Name: expect.any(String), Accessibility: expect.any(Number) })
// A player who contributes to nothing gets an empty array, not a 404.
const none = await SELF.fetch(`${ORIGIN}/rooms/contributedby/me`, {
headers: await bearer('829'),
})
expect(await none.json()).toEqual([])
// Auth-scoped: `me` is the token, so no token is a 401.
expect((await SELF.fetch(`${ORIGIN}/rooms/contributedby/me`)).status).toBe(401)
// The DB is shared across this file, and these are the only player-made public rooms
// in it — leaving them behind changes what the `new`/`community` room feeds serve.
await env.DB.prepare('DELETE FROM room WHERE room_id BETWEEN 30401 AND 30405').run()
})
it('GET /rooms/:roomId/experience serves the fixed XP settings, no auth', async () => {
const res = await SELF.fetch(`${ORIGIN}/rooms/2/experience`)
expect(res.status).toBe(200)
// A bare two-key object — no `{ success, error, value }` envelope around it. Disabled:
// no room awards XP here, and DailyLimit is the cap that would apply if one did.
expect(await res.json()).toEqual({ Enabled: false, DailyLimit: 1000 })
// Nothing is stored per room, so every room answers the same — including one that
// doesn't exist, which is never looked up.
expect(await (await SELF.fetch(`${ORIGIN}/rooms/77/experience`)).json()).toEqual({
Enabled: false,
DailyLimit: 1000,
})
expect(await (await SELF.fetch(`${ORIGIN}/rooms/99999/experience`)).json()).toEqual({
Enabled: false,
DailyLimit: 1000,
})
// The id is digits-only, like the other room-scoped routes.
expect((await SELF.fetch(`${ORIGIN}/rooms/abc/experience`)).status).toBe(404)
})
it('GET /rooms/ownedby/:id returns an account public rooms (no auth)', async () => {
const res = await SELF.fetch(`${ORIGIN}/rooms/ownedby/1`)
expect(res.status).toBe(200)
@@ -276,6 +533,46 @@ describe('rooms endpoints', () => {
expect(body.Results.some((r) => r.Name === 'RecCenter')).toBe(true)
})
it('GET /rooms/autocomplete_search suggests names and tags as plain strings', async () => {
const suggest = async (query: string, extra = '') =>
(await (
await SELF.fetch(
`${ORIGIN}/rooms/autocomplete_search?query=${encodeURIComponent(query)}${extra}`
)
).json()) as string[]
// A bare array of STRINGS — not rooms, not an envelope.
const rec = await suggest('rec', '&take=4&searchSessionId=abc-123')
expect(Array.isArray(rec)).toBe(true)
for (const s of rec) expect(typeof s).toBe('string')
expect(rec).toContain('RecCenter')
expect(rec.length).toBeLessThanOrEqual(4)
// Every suggestion finds something when submitted — the point of the endpoint.
for (const term of rec) {
const found = (await (
await SELF.fetch(`${ORIGIN}/rooms/search?query=${encodeURIComponent(term)}`)
).json()) as { TotalResults: number }
expect(found.TotalResults, `"${term}" must find rooms`).toBeGreaterThan(0)
}
// Tags are suggested with their `#`, and a `#` query suggests tags only.
const tags = await suggest('#rro')
expect(tags).toEqual(['#rro'])
expect(tags.every((t) => t.startsWith('#'))).toBe(true)
// `take` caps the list; an empty query suggests nothing rather than everything.
expect((await suggest('e', '&take=2')).length).toBeLessThanOrEqual(2)
expect(await suggest('')).toEqual([])
expect(await suggest('zzzznothingmatchesthis')).toEqual([])
// Deterministic: the same query suggests the same things in the same order.
expect(await suggest('rec')).toEqual(rec)
// Dorms are excluded, exactly as they are from search.
expect(await suggest('dormroom')).toEqual([])
})
it('GET /rooms/search excludes dorms and respects pagination shape', async () => {
const res = await SELF.fetch(`${ORIGIN}/rooms/search?query=dormroom`)
expect(res.status).toBe(200)
@@ -580,9 +877,7 @@ describe('rooms endpoints', () => {
it('GET /rooms/hot?tag=community serves rooms the Coach account did not create', async () => {
type Feed = { Results: Array<{ Name: string }>; TotalResults: number }
const feed = async (): Promise<Feed> =>
(await (
await SELF.fetch(`${ORIGIN}/rooms/hot?tag=community&skip=0&take=100`)
).json()) as Feed
(await (await SELF.fetch(`${ORIGIN}/rooms/hot?tag=community&skip=0&take=100`)).json()) as Feed
const names = async (): Promise<string[]> => (await feed()).Results.map((r) => r.Name)
// No room carries a `community` tag, and every seeded room belongs to Coach
@@ -672,13 +967,13 @@ describe('rooms endpoints', () => {
expect(body.length).toBeLessThanOrEqual(3)
})
// Skipped: the endpoint is disabled. Serving it broke the client — the other room
// listings started failing with NREs, apparently because the featured-room load
// corrupts the client's room cache — so the route is registered under an `XXX`
// prefix (see rooms.app.ts) and this path 404s. The handler and its test are kept
// intact for whenever the cause is found; un-prefix the route to re-enable both.
it.skip('GET /featuredrooms/current returns a featured-room group of public rooms', async () => {
const res = await SELF.fetch(`${ORIGIN}/featuredrooms/current`)
// Served only to the client builds that render it — the 2023 client's other room
// listings start failing with NREs when it gets this payload, which is why the route
// was parked entirely for a while (see FEATURED_ROOMS_VERSIONS in rooms.app.ts).
it('GET /featuredrooms/current serves the group to a supported client build', async () => {
const res = await SELF.fetch(`${ORIGIN}/featuredrooms/current`, {
headers: await bearer('1', undefined, '20250718.01'),
})
expect(res.status).toBe(200)
const body = (await res.json()) as {
FeaturedRoomGroupId: number
@@ -696,6 +991,20 @@ describe('rooms endpoints', () => {
expect(body.Rooms.some((r) => r.RoomId === 1)).toBe(false)
})
it('GET /featuredrooms/current withholds the group from other client builds', async () => {
// The 2023 build gets the 404 it got while the route was parked — the state in which
// its room listings work. Same for a token with no `rn.ver` at all.
for (const version of ['20230414', '20231207', undefined]) {
const res = await SELF.fetch(`${ORIGIN}/featuredrooms/current`, {
headers: await bearer('1', undefined, version),
})
expect(res.status, `build ${version}`).toBe(404)
}
// And no token at all is a 401, not a 404: the build is read off the token.
expect((await SELF.fetch(`${ORIGIN}/featuredrooms/current`)).status).toBe(401)
})
it('GET /rooms/:id/similar returns { Results, TotalResults } of tag-sharing rooms (excluding self)', async () => {
const res = await SELF.fetch(`${ORIGIN}/rooms/2/similar`)
expect(res.status).toBe(200)
@@ -1184,6 +1493,46 @@ describe('rooms endpoints', () => {
expect(await bansOf(2)).toHaveLength(2)
})
it('GET /Room_server/rooms/:id/bans/:playerId/isBanned answers the real ban state', async () => {
const isBanned = async (roomId: number, playerId: number, sub = '300') =>
SELF.fetch(`${ORIGIN}/Room_server/rooms/${roomId}/bans/${playerId}/isBanned`, {
headers: await bearer(sub),
})
// Auth-gated, but any authenticated caller may ask — a ban is not a secret from the
// player it stops.
expect((await SELF.fetch(`${ORIGIN}/Room_server/rooms/2/bans/205/isBanned`)).status).toBe(401)
// Nobody is banned from room 3.
const clean = await isBanned(3, 4242)
expect(clean.status).toBe(200)
// `success` says the CHECK ran; `value` is the answer. Note `error_id` is present and
// `error` is null — not the room mutations' `{ success, error, value }` with `""`.
expect(await clean.json()).toEqual({
success: true,
error: null,
error_id: null,
value: false,
})
// Ban someone from room 3, and the same call now says so.
await env.DB.prepare(
'INSERT INTO room_ban (room_id, banned_player_id, ban_mask, banned_by_account_id, created_at)' +
" VALUES (?1, ?2, 0, 1, '2026-01-01T00:00:00Z')"
)
.bind(3, 4242)
.run()
expect(await (await isBanned(3, 4242)).json()).toMatchObject({ success: true, value: true })
// The ban is per (room, player): another room and another player are unaffected.
expect(await (await isBanned(2, 4242)).json()).toMatchObject({ value: false })
expect(await (await isBanned(3, 4243)).json()).toMatchObject({ value: false })
await env.DB.prepare('DELETE FROM room_ban WHERE room_id = ?1 AND banned_player_id = ?2')
.bind(3, 4242)
.run()
})
it('POST /rooms/:id/bans kicks the banned player', async () => {
type Sent = { playerId: number; notificationType: string | number; data: unknown }
const hub = () => env.RECFLARE_NOTIFICATIONS_HUB.getByName('global')
@@ -2826,6 +3175,80 @@ describe('rooms endpoints', () => {
await clearPresence(999)
})
it('GET /rooms/:id/subrooms/:sid/saves/no_unity_assets lists the same history, lighter', async () => {
const get = async (path: string, sub?: string) =>
SELF.fetch(`${ORIGIN}${path}`, sub === undefined ? {} : { headers: await bearer(sub) })
const light = '/rooms/2/subrooms/2/saves/no_unity_assets'
const res = await get(light, '1')
expect(res.status).toBe(200)
const page = (await res.json()) as {
Results: Array<Record<string, unknown>>
TotalResults: number
TotalCount: number
}
// The same history the full list serves — same rows, same order, same counts.
const full = (await (await get('/rooms/2/subrooms/2/saves', '1')).json()) as {
Results: Array<{ SubRoomDataSaveId: number; DataBlob: string }>
TotalResults: number
}
expect(page.TotalResults).toBe(full.TotalResults)
expect(page.TotalCount).toBe(page.TotalResults)
expect(page.Results.map((r) => r.SubRoomDataSaveId)).toEqual(
full.Results.map((r) => r.SubRoomDataSaveId)
)
// The lighter row: the Unity-asset PAYLOADS are gone (and `Tags` with them), the asset
// IDs stay, and `UnityAssetId` is present-and-null rather than omitted.
const row = page.Results[0]!
expect(Object.keys(row)).toEqual([
'SubRoomDataSaveId',
'SubRoomId',
'UnityAssetId',
'ReferencedUnityAssetIds',
'DataBlob',
'DataBlobHash',
'PersistenceVersion',
'OMVersion',
'SavedByAccountId',
'SavedOnPlatform',
'SavedOnDeviceClass',
'Description',
'ModerationState',
'CreatedAt',
'UgcSubVersion',
])
expect(row.UnityAssetId).toBe(null)
expect(row.ReferencedUnityAssetIds).toEqual([])
expect(row.SubRoomId).toBe(2)
expect(row.DataBlob).toBe(full.Results[0]!.DataBlob)
// skip/take page it the same way.
const paged = (await (await get(`${light}?skip=1&take=1`, '1')).json()) as {
Results: Array<{ SubRoomDataSaveId: number }>
TotalResults: number
}
expect(paged.Results).toHaveLength(1)
expect(paged.Results[0]!.SubRoomDataSaveId).toBe(full.Results[1]!.SubRoomDataSaveId)
expect(paged.TotalResults).toBe(full.TotalResults)
// A never-saved subroom pages empty rather than 404ing, as the full list does.
expect(await (await get('/rooms/3/subrooms/3/saves/no_unity_assets', '1')).json()).toEqual({
Results: [],
TotalResults: 0,
TotalCount: 0,
})
// Same gate as the list it mirrors — it exposes the same unpublished saves.
expect((await get(light)).status).toBe(401)
expect((await get(light, '999')).status).toBe(403)
expect((await get(light, '2')).status).toBe(403)
await putInRoom(999, 2)
expect((await get(light, '999')).status).toBe(200)
await clearPresence(999)
expect((await get(light, '999')).status).toBe(403)
})
it('GET /rooms/:id/subrooms/:sid/saves/:saveId is the detail behind a history row', async () => {
const get = async (path: string, sub?: string) =>
SELF.fetch(`${ORIGIN}${path}`, sub === undefined ? {} : { headers: await bearer(sub) })
@@ -2910,11 +3333,16 @@ describe('rooms endpoints', () => {
'DELETE /rooms/{roomId}/interactionby/me/favorite',
'DELETE /rooms/{roomId}/subrooms/{subRoomId}',
'GET /',
'GET /XXXfeaturedrooms/current',
'GET /Room_server/rooms/{roomId}/bans/{playerId}/isBanned',
'GET /dormroom/me',
'GET /featuredrooms/current',
'GET /photon_access_token',
'GET /publishState/configs',
'GET /rooms',
'GET /rooms/autocomplete_search',
'GET /rooms/base',
'GET /rooms/bulk',
'GET /rooms/contributedby/me',
'GET /rooms/createdby/me',
'GET /rooms/favoritedby/me',
'GET /rooms/hot',
@@ -2926,12 +3354,16 @@ describe('rooms endpoints', () => {
'GET /rooms/visitedby/{playerId}',
'GET /rooms/{roomId}',
'GET /rooms/{roomId}/bans',
'GET /rooms/{roomId}/experience',
'GET /rooms/{roomId}/experience/player',
'GET /rooms/{roomId}/interactionby/me',
'GET /rooms/{roomId}/playerdata/me',
'GET /rooms/{roomId}/similar',
'GET /rooms/{roomId}/subrooms/{subRoomId}/saves',
'GET /rooms/{roomId}/subrooms/{subRoomId}/saves/no_unity_assets',
'GET /rooms/{roomId}/subrooms/{subRoomId}/saves/{saveId}',
'GET /roomserver/rooms/createdby/me',
'POST /rooms/bulk',
'POST /rooms/{roomId}/bans',
'POST /rooms/{roomId}/clone',
'POST /rooms/{roomId}/subrooms',