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 {
|
import {
|
||||||
CURRENT_OUTFIT_SLOT,
|
CURRENT_OUTFIT_SLOT,
|
||||||
getOutfit,
|
getOutfit,
|
||||||
|
getOutfits,
|
||||||
getOutfitsByAccounts,
|
getOutfitsByAccounts,
|
||||||
inventionDescriptionRejection,
|
inventionDescriptionRejection,
|
||||||
inventionLongDescriptionRejection,
|
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
|
// The caller's outfit wardrobe — every slot they have saved, ordered by slot. The same
|
||||||
// `PUT /outfits/me` are in the shared `outfit` table already, but which of them
|
// read as `econ`'s `GET /api/avatar/v3/saved`, on the bare path the newer client uses:
|
||||||
// belong in this list (and in what shape) has not been pinned down, so it answers []
|
// both worker's write paths land in the shared `outfit` table, so both list endpoints
|
||||||
// rather than guessing.
|
// 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(
|
.get(
|
||||||
'/outfits/me/saved',
|
'/outfits/me/saved',
|
||||||
describeRoute({
|
describeRoute({
|
||||||
tags: ['Avatar', '2025'],
|
tags: ['Avatar', '2025'],
|
||||||
summary: 'The caller’s saved outfits',
|
summary: 'The caller’s saved outfits',
|
||||||
description:
|
description:
|
||||||
'The wardrobe behind the newer outfit screen. Empty for now: the outfits saved ' +
|
'The wardrobe behind the newer outfit screen: every slot the caller has saved, ' +
|
||||||
'through `PUT /outfits/me` are in the shared `outfit` table, but which of them this ' +
|
'ordered by slot, and `[]` when they have saved none. The same rows `econ`’s ' +
|
||||||
'list should carry, and in what shape, is not pinned down yet.',
|
'`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,
|
security: AUTHED,
|
||||||
responses: {
|
responses: {
|
||||||
200: json(JsonArray, 'An empty list'),
|
200: json(JsonArray, 'The saved outfits, ordered by slot (empty when none)'),
|
||||||
401: UNAUTHORIZED_RESPONSE,
|
401: UNAUTHORIZED_RESPONSE,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const id = await authedId(c)
|
const id = await authedId(c)
|
||||||
if (id === null) return unauthorized(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)
|
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`)
|
const anon = await exports.default.fetch(`${ORIGIN}/outfits/me/saved`)
|
||||||
expect(anon.status).toBe(401)
|
expect(anon.status).toBe(401)
|
||||||
// Empty even for account 42, which saved an outfit through PUT /outfits/me above.
|
|
||||||
|
// 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`, {
|
const res = await exports.default.fetch(`${ORIGIN}/outfits/me/saved`, {
|
||||||
headers: await bearer(),
|
headers: await bearer(sub),
|
||||||
})
|
})
|
||||||
expect(res.status).toBe(200)
|
expect(res.status).toBe(200)
|
||||||
expect(await res.json()).toEqual([])
|
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,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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 () => {
|
test('POST /outfits/bulk serves each account’s worn outfit, keyed by id', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user