more stubs

This commit is contained in:
Devin Zuczek
2026-08-15 13:54:19 -04:00
parent 8ea0caa1e5
commit 11b037a2f1
67 changed files with 38566 additions and 2652 deletions
+14
View File
@@ -294,6 +294,13 @@ export const RoomDto = z.object({
PublishedAt: z.string(),
BecameRRStudioRoomAt: z.string().nullable(),
Stats: RoomStatsDto,
BoostCount: z
.int()
.describe('Boosts on the room. Nothing grants boosts here, so always 0 — but present'),
CurrentSnapshotId: z
.int()
.nullable()
.describe('The rooms published snapshot. Nothing takes snapshots here, so always null'),
RankingContext: z.unknown().nullable(),
IsDorm: z.boolean().describe('Auto-provisioned personal room; excluded from every feed'),
IsPlacePlay: z.boolean(),
@@ -333,6 +340,13 @@ export const PagedRooms = z.object({
TotalResults: z.int().describe('The full match count, not the page size'),
})
/**
* `GET /dormroom/me` — the dorm's `RoomId` as a BARE JSON number, not a room and not an
* envelope around one. The caller follows it with `GET /rooms/{roomId}` when it wants the
* room itself, so sending the whole DTO here was a payload nobody read.
*/
export const DormRoomId = z.int().describe('The callers dorm RoomId')
/**
* A room lookup result: the room, or `{}` when nothing matched. The by-id/by-name
* lookups answer an empty object rather than a 404 — the client reads that as "no room".
+17 -11
View File
@@ -73,6 +73,7 @@ import {
CloningRequest,
CreateSubRoomRequest,
DescriptionRequest,
DormRoomId,
FeaturedRoomGroupDto,
FORBIDDEN_RESPONSE,
form,
@@ -848,28 +849,33 @@ const app = new Hono<App>()
ownedRooms
)
// The caller's own dorm, in the same shape `GET /rooms/{roomId}` serves — the client
// renders it with the same code path. Gets-or-creates, exactly as entering a dorm
// does (`match`), so a player who has never been to their dorm gets one here rather
// than a 404; the id is stable from then on.
// The caller's own dorm, as its ID ALONE — a bare JSON number, not the room. The
// caller follows up with `GET /rooms/{roomId}` when it wants the room itself.
//
// Gets-or-creates, exactly as entering a dorm does (`match`): the provisioning is the
// point of the call as much as the answer is, so a player who has never been to their
// dorm gets one minted here rather than a 404, and the id is stable from then on.
.get(
'/dormroom/me',
describeRoute({
tags: ['My rooms'],
summary: 'The callers dorm',
summary: 'The callers dorm id',
description: [
'The callers personal dorm room, as `GET /rooms/{roomId}` would serve it —',
'`SubRooms` re-attached, same DTO. The dorm is provisioned on first access (cloned',
'from the seeded template dorm), so this returns a room for any authed caller and',
'never 404s; calling it repeatedly returns the same dorm.',
'The `RoomId` of the callers personal dorm, as a bare JSON number — NOT the room:',
'fetch that from `GET /rooms/{roomId}` with the id this returns.',
'',
'The dorm is provisioned on first access (cloned from the seeded template dorm), so',
'this answers for any authed caller and never 404s, and calling it again returns the',
'same id.',
].join(' '),
security: AUTHED,
responses: { 200: json(RoomDto, 'The callers dorm'), 401: UNAUTHORIZED_RESPONSE },
responses: { 200: json(DormRoomId, 'The callers dorm id'), 401: UNAUTHORIZED_RESPONSE },
}),
async (c) => {
const accountId = await authedAccountId(c)
if (accountId === null) return unauthorized(c)
return c.json(await getOrCreateDormRoom(c.env.DB, accountId))
const dorm = await getOrCreateDormRoom(c.env.DB, accountId)
return c.json(Number(dorm.RoomId))
}
)
+33 -14
View File
@@ -141,6 +141,17 @@ 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.
@@ -220,39 +231,47 @@ describe('rooms endpoints', () => {
expect(other).toEqual([])
})
it('GET /dormroom/me serves the callers own dorm in the room shape', async () => {
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), served exactly as GET /rooms/1 does —
// same DTO, SubRooms re-attached.
// 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)
const dorm = (await res.json()) as {
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
SubRooms: Array<{ UnitySceneId: string }>
}
expect(dorm).toMatchObject({ RoomId: 1, IsDorm: true, CreatorAccountId: 1 })
expect(dorm.SubRooms[0].UnitySceneId).toBe('76d98498-60a1-430c-ab76-b54a29b7a163')
expect(dorm).toEqual(await (await SELF.fetch(`${ORIGIN}/rooms/1`)).json())
expect(room).toMatchObject({ RoomId: 1, IsDorm: true, CreatorAccountId: 1 })
// A player who has never entered their dorm gets one provisioned rather than a
// 404, and it belongs to THEM — not the template dorm they were cloned from.
// 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 { RoomId: number; IsDorm: boolean; CreatorAccountId: number }
expect(fresh).toMatchObject({ IsDorm: true, CreatorAccountId: 999 })
expect(fresh.RoomId).not.toBe(1)
).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 { RoomId: number }
expect(again.RoomId).toBe(fresh.RoomId)
).json()) as number
expect(again).toBe(fresh)
})
// The website's "My rooms" list is a browser calling this worker from another origin,