mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-09 07:01:27 -07:00
more stubs
This commit is contained in:
@@ -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 room’s 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 caller’s 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
@@ -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 caller’s dorm',
|
||||
summary: 'The caller’s dorm id',
|
||||
description: [
|
||||
'The caller’s 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 caller’s 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 caller’s dorm'), 401: UNAUTHORIZED_RESPONSE },
|
||||
responses: { 200: json(DormRoomId, 'The caller’s 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))
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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 caller’s own dorm in the room shape', async () => {
|
||||
it('GET /dormroom/me serves the caller’s 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,
|
||||
|
||||
Reference in New Issue
Block a user