mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-09 15:11:29 -07:00
more stubs
This commit is contained in:
@@ -4,6 +4,7 @@ import { useWorkersLogger } from 'workers-tagged-logger'
|
||||
|
||||
import { withCleanSpec, withDefaultCors, withNotFound, withOnError } from '@repo/hono-helpers'
|
||||
|
||||
import { accountRoutes } from './routes/account'
|
||||
import { avatarRoutes } from './routes/avatar'
|
||||
import { configRoutes } from './routes/config'
|
||||
import { eventRoutes } from './routes/events'
|
||||
@@ -62,6 +63,7 @@ const app = new Hono<App>({ strict: false })
|
||||
.route('/', inventoryRoutes)
|
||||
.route('/', roomRoutes)
|
||||
.route('/', imageRoutes)
|
||||
.route('/', accountRoutes)
|
||||
|
||||
// The generated spec. Documentation only — no request is validated against it (see
|
||||
// openapi.ts). `hide: true` keeps this route out of its own output.
|
||||
|
||||
@@ -417,6 +417,17 @@ export const GenerateGiftRequest = z.object({
|
||||
Xp: z.string().optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* `POST /api/customAvatarItems/v1/bulk` form body. A repeated form field, not a JSON
|
||||
* array: the reference binds `[FromForm] List<string>`, so the client posts
|
||||
* `customAvatarItemIds=a&customAvatarItemIds=b`.
|
||||
*/
|
||||
export const BulkCustomAvatarItemsRequest = z.object({
|
||||
customAvatarItemIds: z
|
||||
.array(z.string())
|
||||
.describe('The ids to resolve; repeat the field once per id'),
|
||||
})
|
||||
|
||||
/** A paginated custom-avatar-item page (no storage yet, so always empty). */
|
||||
export const CustomAvatarItemsPage = z.object({
|
||||
Results: JsonArray,
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Hono } from 'hono'
|
||||
import { describeRoute } from 'hono-openapi'
|
||||
|
||||
import { json, JsonArray, stringParam } from '../openapi'
|
||||
|
||||
import type { App } from '../context'
|
||||
|
||||
// ---- Account ---------------------------------------------------------------
|
||||
// The identity service's account-scoped reads. Nothing here is backed by storage — the
|
||||
// client calls it while loading the account, and an empty list is a complete answer for a
|
||||
// server that links no external channels to an account.
|
||||
export const accountRoutes = new Hono<App>({ strict: false }).get(
|
||||
'/iam/me/channels/:type',
|
||||
describeRoute({
|
||||
tags: ['Account'],
|
||||
summary: 'The caller’s channels of a type',
|
||||
description:
|
||||
'The channels of the given `{type}` linked to the caller’s account. This server ' +
|
||||
'links none, so the list is always empty — a real answer rather than a placeholder ' +
|
||||
'for one, since the client renders "nothing linked" from it. `{type}` is accepted ' +
|
||||
'but not inspected, and the route is not auth-gated: the answer is the same for ' +
|
||||
'every caller and every type.',
|
||||
parameters: [stringParam('type', 'The channel type. Accepted but not inspected.')],
|
||||
responses: { 200: json(JsonArray, 'Always an empty list') },
|
||||
}),
|
||||
(c) => c.json([])
|
||||
)
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
import {
|
||||
AUTHED,
|
||||
BareBoolean,
|
||||
BulkCustomAvatarItemsRequest,
|
||||
CustomAvatarItemsPage,
|
||||
ErrorResponse,
|
||||
form,
|
||||
@@ -224,6 +225,42 @@ export const avatarRoutes = new Hono<App>({ strict: false })
|
||||
(c) => c.json([])
|
||||
)
|
||||
|
||||
// A batch lookup of custom avatar items by id. The reference filters a static catalog
|
||||
// down to the posted ids and returns the MATCHES AS A BARE ARRAY — not the
|
||||
// `{ Results, TotalResults }` page its catalog file is written in, and not a 404 for
|
||||
// ids it doesn't hold. Nothing stores custom items here (the reference's own catalog
|
||||
// ships empty too), so every id misses and the array is empty.
|
||||
//
|
||||
// Auth-gated, and the token is checked before anything else, as the reference does.
|
||||
.post(
|
||||
'/api/customAvatarItems/v1/bulk',
|
||||
describeRoute({
|
||||
tags: ['Avatar'],
|
||||
summary: 'Custom avatar items in bulk',
|
||||
description:
|
||||
'Resolves a batch of custom-avatar-item ids to their items: the posted ' +
|
||||
'`customAvatarItemIds` filtered against the catalog, returned as a BARE ARRAY of ' +
|
||||
'the ones that matched. Not the `{ Results, TotalResults }` page the sibling ' +
|
||||
'custom-item reads serve — the reference keeps its catalog in that shape but ' +
|
||||
'answers this route with the filtered array alone.\n\n' +
|
||||
'A miss is not an error: unknown ids are simply absent from the response, and the ' +
|
||||
'client reads the items it got back rather than the ids it asked for. Nothing ' +
|
||||
'stores custom items here, so every id misses and this is always `[]` — which is ' +
|
||||
'why the posted ids are not parsed.',
|
||||
security: AUTHED,
|
||||
requestBody: form(BulkCustomAvatarItemsRequest, 'The custom-avatar-item ids to resolve'),
|
||||
responses: {
|
||||
200: json(JsonArray, 'The matching items — always empty here'),
|
||||
401: UNAUTHORIZED_RESPONSE,
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
const id = await authedId(c)
|
||||
if (id === null) return unauthorized(c)
|
||||
return c.json([])
|
||||
}
|
||||
)
|
||||
|
||||
// Custom avatar items created by a given account. No storage yet → an empty
|
||||
// paginated result (matches the econ `customAvatarItems/v1/owned` shape).
|
||||
.get(
|
||||
|
||||
@@ -139,6 +139,25 @@ export const configRoutes = new Hono<App>({ strict: false })
|
||||
(c) => c.json(gameConfigsV1All)
|
||||
)
|
||||
|
||||
// The property bag the client would attach to its Statsig user. Analytics are off here
|
||||
// (see the placeholder keys `/api/config/v1/amplitude` serves), so there is nothing to
|
||||
// segment on and the bag is empty — the client reads that as "no overrides" and carries
|
||||
// on, which is why this answers `{}` rather than 404ing.
|
||||
.get(
|
||||
'/statsigUserProperties',
|
||||
describeRoute({
|
||||
tags: ['Config'],
|
||||
summary: 'Statsig user properties',
|
||||
description:
|
||||
'The custom properties the client would attach to its Statsig user for experiment ' +
|
||||
'targeting. This server runs no experiments and collects no analytics, so the bag ' +
|
||||
'is always empty — the client reads `{}` as “no overrides”. Not auth-gated: there ' +
|
||||
'is nothing per-account to leak in an empty object.',
|
||||
responses: { 200: json(JsonObject, 'An empty object') },
|
||||
}),
|
||||
(c) => c.json({})
|
||||
)
|
||||
|
||||
// Voice chat config. The client fetches it to set up voice.
|
||||
// No reference shape, so return an empty object until the client needs fields.
|
||||
.get(
|
||||
|
||||
@@ -488,6 +488,44 @@ describe('public endpoints', () => {
|
||||
expect(await res.json()).toEqual({ Results: [], TotalResults: 0 })
|
||||
})
|
||||
|
||||
// A BARE ARRAY of the items that matched — not the `{ Results, TotalResults }` page
|
||||
// the sibling custom-item reads serve. Nothing stores custom items, so every id
|
||||
// misses, and a miss is an absent entry rather than an error.
|
||||
test('POST /api/customAvatarItems/v1/bulk returns the matching items as an array', async () => {
|
||||
const res = await exports.default.fetch(`${ORIGIN}/api/customAvatarItems/v1/bulk`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/x-www-form-urlencoded',
|
||||
...(await bearer()),
|
||||
},
|
||||
// Repeated form field, as `[FromForm] List<string>` binds it.
|
||||
body: new URLSearchParams([
|
||||
['customAvatarItemIds', 'a'],
|
||||
['customAvatarItemIds', 'b'],
|
||||
]),
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.json()).toEqual([])
|
||||
})
|
||||
|
||||
// The ids are never parsed (nothing could match), so a missing body is still a 200
|
||||
// rather than the 400 a body-reading handler would produce.
|
||||
test('POST /api/customAvatarItems/v1/bulk ignores the body', async () => {
|
||||
const res = await exports.default.fetch(`${ORIGIN}/api/customAvatarItems/v1/bulk`, {
|
||||
method: 'POST',
|
||||
headers: await bearer(),
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.json()).toEqual([])
|
||||
})
|
||||
|
||||
test('POST /api/customAvatarItems/v1/bulk is auth-gated', async () => {
|
||||
const res = await exports.default.fetch(`${ORIGIN}/api/customAvatarItems/v1/bulk`, {
|
||||
method: 'POST',
|
||||
})
|
||||
expect(res.status).toBe(401)
|
||||
})
|
||||
|
||||
test('POST /api/customAvatarItems/GetCustomAvatarItemCurrentSavesForLegacyAvatarItems returns an empty map', async () => {
|
||||
const res = await exports.default.fetch(
|
||||
`${ORIGIN}/api/customAvatarItems/GetCustomAvatarItemCurrentSavesForLegacyAvatarItems`,
|
||||
@@ -634,6 +672,12 @@ describe('public endpoints', () => {
|
||||
expect(await cats.json()).toEqual({ Results: [], TotalResults: 0 })
|
||||
})
|
||||
|
||||
test('GET /statsigUserProperties returns an empty object', async () => {
|
||||
const res = await exports.default.fetch(`${ORIGIN}/statsigUserProperties`)
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.json()).toEqual({})
|
||||
})
|
||||
|
||||
test('GET /voice/config returns an object', async () => {
|
||||
const res = await exports.default.fetch(`${ORIGIN}/voice/config`)
|
||||
expect(res.status).toBe(200)
|
||||
@@ -1592,6 +1636,17 @@ describe('public endpoints', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('account', () => {
|
||||
test.each(['email', 'phone', 'anything'])(
|
||||
'GET /iam/me/channels/%s is an empty list',
|
||||
async (type) => {
|
||||
const res = await exports.default.fetch(`${ORIGIN}/iam/me/channels/${type}`)
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.json()).toEqual([])
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
describe('auth-gated endpoints', () => {
|
||||
test('401 without a bearer token', async () => {
|
||||
const res = await exports.default.fetch(`${ORIGIN}/api/consumables/v2/getUnlocked`)
|
||||
@@ -3816,8 +3871,10 @@ describe('openapi', () => {
|
||||
'GET /api/rooms/v1/filters',
|
||||
'GET /api/versioncheck/islandedversions',
|
||||
'GET /api/versioncheck/v4',
|
||||
'GET /iam/me/channels/{type}',
|
||||
'GET /outfits/me',
|
||||
'GET /outfits/me/saved',
|
||||
'GET /statsigUserProperties',
|
||||
'GET /voice/config',
|
||||
'POST /api/PlayerReporting/v1/deviceId',
|
||||
'POST /api/PlayerReporting/v1/hile',
|
||||
@@ -3826,6 +3883,7 @@ describe('openapi', () => {
|
||||
'POST /api/PlayerReporting/v3/create',
|
||||
'POST /api/avatar/v2/gifts/generate',
|
||||
'POST /api/customAvatarItems/GetCustomAvatarItemCurrentSavesForLegacyAvatarItems',
|
||||
'POST /api/customAvatarItems/v1/bulk',
|
||||
'POST /api/gamesight/event',
|
||||
'POST /api/images/v1/cheer',
|
||||
'POST /api/images/v4/uploadsaved',
|
||||
|
||||
Reference in New Issue
Block a user