mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-09 07:01:27 -07:00
[api] fix saved outfits not appearing
This commit is contained in:
@@ -4,6 +4,7 @@ import { describeRoute } from 'hono-openapi'
|
||||
import {
|
||||
CURRENT_OUTFIT_SLOT,
|
||||
getOutfit,
|
||||
getOutfits,
|
||||
getOutfitsByAccounts,
|
||||
inventionDescriptionRejection,
|
||||
inventionLongDescriptionRejection,
|
||||
@@ -1021,29 +1022,46 @@ export const avatarRoutes = new Hono<App>({ strict: false })
|
||||
}
|
||||
)
|
||||
|
||||
// The caller's outfit wardrobe. An empty list for now — the outfits saved through
|
||||
// `PUT /outfits/me` are in the shared `outfit` table already, but which of them
|
||||
// belong in this list (and in what shape) has not been pinned down, so it answers []
|
||||
// rather than guessing.
|
||||
// The caller's outfit wardrobe — every slot they have saved, ordered by slot. The same
|
||||
// read as `econ`'s `GET /api/avatar/v3/saved`, on the bare path the newer client uses:
|
||||
// both worker's write paths land in the shared `outfit` table, so both list endpoints
|
||||
// serve the same rows.
|
||||
//
|
||||
// Slot 0 is INCLUDED. It is the outfit being worn (what `/outfits/me` reads), but it is
|
||||
// also a saved slot: the newer client picks the slot it saves into (`/api/avatar/v4/saved/set`
|
||||
// 400s without one), so filtering slot 0 out would hide a real saved outfit whenever a
|
||||
// wardrobe entry lands there. Showing the worn outfit as a wardrobe entry is the cheaper
|
||||
// mistake of the two.
|
||||
//
|
||||
// Rows are served exactly as they were stored, unprojected — see the note atop
|
||||
// `outfits-db.ts`: econ's saved slots hold the old flat PascalCase outfit while
|
||||
// `/outfits/me` holds the newer envelope, and neither is converted into the other.
|
||||
.get(
|
||||
'/outfits/me/saved',
|
||||
describeRoute({
|
||||
tags: ['Avatar', '2025'],
|
||||
summary: 'The caller’s saved outfits',
|
||||
description:
|
||||
'The wardrobe behind the newer outfit screen. Empty for now: the outfits saved ' +
|
||||
'through `PUT /outfits/me` are in the shared `outfit` table, but which of them this ' +
|
||||
'list should carry, and in what shape, is not pinned down yet.',
|
||||
'The wardrobe behind the newer outfit screen: every slot the caller has saved, ' +
|
||||
'ordered by slot, and `[]` when they have saved none. The same rows `econ`’s ' +
|
||||
'`GET /api/avatar/v3/saved` serves — both write paths land in the shared `outfit` ' +
|
||||
'table.\n\n' +
|
||||
'Slot 0 is included. It is the outfit being worn (what `GET /outfits/me` reads) but ' +
|
||||
'it is a saved slot too, and the client chooses the slot it saves into, so omitting ' +
|
||||
'it would hide a real outfit whenever a wardrobe entry lands there.\n\n' +
|
||||
'Each outfit is served exactly as it was stored, unprojected: slots written through ' +
|
||||
'`PUT /outfits/me` hold the newer envelope while `econ`’s saved-set slots hold the ' +
|
||||
'old flat shape, and neither is converted into the other.',
|
||||
security: AUTHED,
|
||||
responses: {
|
||||
200: json(JsonArray, 'An empty list'),
|
||||
200: json(JsonArray, 'The saved outfits, ordered by slot (empty when none)'),
|
||||
401: UNAUTHORIZED_RESPONSE,
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
const id = await authedId(c)
|
||||
if (id === null) return unauthorized(c)
|
||||
return c.json([])
|
||||
return c.json(await getOutfits(c.env.DB, id))
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -1566,15 +1566,58 @@ describe('public endpoints', () => {
|
||||
expect(((await worn.json()) as { Name: string | null }).Name).toBe(null)
|
||||
})
|
||||
|
||||
test('GET /outfits/me/saved 401s without a token, returns [] with one', async () => {
|
||||
test('GET /outfits/me/saved lists every saved slot, ordered by slot', async () => {
|
||||
const anon = await exports.default.fetch(`${ORIGIN}/outfits/me/saved`)
|
||||
expect(anon.status).toBe(401)
|
||||
// Empty even for account 42, which saved an outfit through PUT /outfits/me above.
|
||||
const res = await exports.default.fetch(`${ORIGIN}/outfits/me/saved`, {
|
||||
headers: await bearer(),
|
||||
|
||||
// A distinct account, so this doesn't depend on what the tests above saved for 42.
|
||||
const saved = async (sub: string) => {
|
||||
const res = await exports.default.fetch(`${ORIGIN}/outfits/me/saved`, {
|
||||
headers: await bearer(sub),
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
return (await res.json()) as Array<Record<string, unknown>>
|
||||
}
|
||||
|
||||
// A player who has never saved gets [], not the empty-outfit envelope `/outfits/me`
|
||||
// serves — an empty wardrobe is an empty list.
|
||||
expect(await saved('4242')).toEqual([])
|
||||
|
||||
const outfit = (slot: number, name: string | null) => ({
|
||||
DataVersion: 2,
|
||||
LegacyData: {
|
||||
SelectionsV1: '193a3bf9-abc0-4d78-8d63-92046908b1c5,,0',
|
||||
SelectionsV2: '{"selections":[]}',
|
||||
FaceFeatures: '{"ver":7}',
|
||||
SkinColor: 'Dc6StLFk60u5iUTrb3_C3w',
|
||||
HairColor: 'UAT0OaWEkUG-mWDIyiX1Kg',
|
||||
},
|
||||
CustomizationSettings: '{"AvatarVersion":2,"AvatarBodyType":0}',
|
||||
Selections: [],
|
||||
Slot: slot,
|
||||
Name: name,
|
||||
Accessibility: 1,
|
||||
ThumbnailFileName: null,
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.json()).toEqual([])
|
||||
|
||||
// Saved out of order, to prove the list is ordered by slot rather than by write time.
|
||||
for (const [slot, name] of [
|
||||
[2, 'two'],
|
||||
[0, null],
|
||||
] as Array<[number, string | null]>) {
|
||||
await exports.default.fetch(`${ORIGIN}/outfits/me`, {
|
||||
method: 'PUT',
|
||||
headers: { ...(await bearer('4242')), 'content-type': 'application/json' },
|
||||
body: JSON.stringify(outfit(slot, name)),
|
||||
})
|
||||
}
|
||||
|
||||
// Slot 0 is in the list: it is the outfit being worn, but it is a saved slot too, and
|
||||
// the client picks the slot it writes. Each row comes back verbatim.
|
||||
expect(await saved('4242')).toEqual([outfit(0, null), outfit(2, 'two')])
|
||||
|
||||
// Another account's wardrobe is its own.
|
||||
expect(await saved('4343')).toEqual([])
|
||||
})
|
||||
|
||||
test('POST /outfits/bulk serves each account’s worn outfit, keyed by id', async () => {
|
||||
|
||||
Reference in New Issue
Block a user