mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 14:41:28 -07:00
[econ] add the POST equipment update endpoint
This commit is contained in:
@@ -2070,11 +2070,17 @@ const app = new Hono<App>({ strict: false })
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Favourite/un-favourite owned equipment. [Authorize]. The client PUTs the entries
|
// Favourite/un-favourite owned equipment. [Authorize]. The client sends the entries it
|
||||||
// it wants changed (one request can carry several) and reads nothing back. Only
|
// wants changed (one request can carry several) and reads nothing back. Only
|
||||||
// `Favorited` is written — the rest of each entry is the client echoing what it was
|
// `Favorited` is written — the rest of each entry is the client echoing what it was
|
||||||
// served, and a guid the caller doesn't own matches no row and is dropped.
|
// served, and a guid the caller doesn't own matches no row and is dropped.
|
||||||
.put(
|
//
|
||||||
|
// PUT or POST: the client uses both spellings for this one call, with an identical body
|
||||||
|
// either way, so they are the same route rather than two handlers. A 404 on the POST
|
||||||
|
// leaves the star drawn on the item the client already redrew, and the favourite
|
||||||
|
// silently doesn't stick.
|
||||||
|
.on(
|
||||||
|
['PUT', 'POST'],
|
||||||
'/api/equipment/v1/update',
|
'/api/equipment/v1/update',
|
||||||
describeRoute({
|
describeRoute({
|
||||||
tags: ['Equipment'],
|
tags: ['Equipment'],
|
||||||
@@ -2082,7 +2088,8 @@ const app = new Hono<App>({ strict: false })
|
|||||||
description: [
|
description: [
|
||||||
'Applies the posted `Favorited` flags to the caller’s owned equipment, matched by',
|
'Applies the posted `Favorited` flags to the caller’s owned equipment, matched by',
|
||||||
'`ModificationGuid`. Everything else in each entry is ignored, and a guid the caller',
|
'`ModificationGuid`. Everything else in each entry is ignored, and a guid the caller',
|
||||||
'doesn’t own is silently skipped. Empty body on success.',
|
'doesn’t own is silently skipped. Empty body on success. Accepts PUT or POST — the',
|
||||||
|
'client uses both, with the same body.',
|
||||||
].join(' '),
|
].join(' '),
|
||||||
security: AUTHED,
|
security: AUTHED,
|
||||||
requestBody: jsonBody(EquipmentUpdateRequest, 'The entries to update'),
|
requestBody: jsonBody(EquipmentUpdateRequest, 'The entries to update'),
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const EQUIPMENT_SCHEMA_DDL: string[] = [
|
|||||||
* inconsistency to tidy up: a drop is a flat record holding avatar, consumable and
|
* inconsistency to tidy up: a drop is a flat record holding avatar, consumable and
|
||||||
* equipment fields side by side, so it needs the prefix to disambiguate, while this
|
* equipment fields side by side, so it needs the prefix to disambiguate, while this
|
||||||
* record is all equipment. Confirmed against the live endpoint, and the entries the
|
* record is all equipment. Confirmed against the live endpoint, and the entries the
|
||||||
* client PUTs back to `/api/equipment/v1/update` use the same unprefixed names.
|
* client sends back to `/api/equipment/v1/update` use the same unprefixed names.
|
||||||
*/
|
*/
|
||||||
export interface Equipment extends Record<string, unknown> {
|
export interface Equipment extends Record<string, unknown> {
|
||||||
ModificationGuid: string
|
ModificationGuid: string
|
||||||
@@ -46,7 +46,7 @@ export interface Equipment extends Record<string, unknown> {
|
|||||||
Rarity: number
|
Rarity: number
|
||||||
/** Always -1 (all platforms) — we don't gate equipment per platform. */
|
/** Always -1 (all platforms) — we don't gate equipment per platform. */
|
||||||
PlatformMask: number
|
PlatformMask: number
|
||||||
/** Player-set favourite flag, toggled by `PUT /api/equipment/v1/update`. */
|
/** Player-set favourite flag, toggled by `PUT`/`POST /api/equipment/v1/update`. */
|
||||||
Favorited: boolean
|
Favorited: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ export async function grantEquipment(
|
|||||||
.run()
|
.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** One entry of the `PUT /api/equipment/v1/update` body. */
|
/** One entry of the `PUT`/`POST /api/equipment/v1/update` body. */
|
||||||
export interface EquipmentFavoriteUpdate {
|
export interface EquipmentFavoriteUpdate {
|
||||||
ModificationGuid: string
|
ModificationGuid: string
|
||||||
Favorited: boolean
|
Favorited: boolean
|
||||||
|
|||||||
@@ -1094,9 +1094,9 @@ describe('econ endpoints', () => {
|
|||||||
expect(await unlocked()).toHaveLength(1)
|
expect(await unlocked()).toHaveLength(1)
|
||||||
|
|
||||||
// Favouriting sticks.
|
// Favouriting sticks.
|
||||||
const update = async (favorited: boolean) =>
|
const update = async (favorited: boolean, method: 'PUT' | 'POST' = 'PUT') =>
|
||||||
exports.default.fetch(`${ORIGIN}/api/equipment/v1/update`, {
|
exports.default.fetch(`${ORIGIN}/api/equipment/v1/update`, {
|
||||||
method: 'PUT',
|
method,
|
||||||
headers: { ...(await bearer('31')), 'Content-Type': 'application/json' },
|
headers: { ...(await bearer('31')), 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify([
|
body: JSON.stringify([
|
||||||
{ PrefabName: '[DiscGolfDisc]', ModificationGuid: guid, Favorited: favorited },
|
{ PrefabName: '[DiscGolfDisc]', ModificationGuid: guid, Favorited: favorited },
|
||||||
@@ -1113,22 +1113,75 @@ describe('econ endpoints', () => {
|
|||||||
expect((await update(false)).status).toBe(200)
|
expect((await update(false)).status).toBe(200)
|
||||||
after = await unlocked()
|
after = await unlocked()
|
||||||
expect(after[0].Favorited).toBe(false)
|
expect(after[0].Favorited).toBe(false)
|
||||||
|
|
||||||
|
// The client sends this as a POST too, with the same body — same effect.
|
||||||
|
expect((await update(true, 'POST')).status).toBe(200)
|
||||||
|
expect((await unlocked())[0]?.Favorited).toBe(true)
|
||||||
|
expect((await update(false, 'POST')).status).toBe(200)
|
||||||
|
expect((await unlocked())[0]?.Favorited).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('PUT /api/equipment/v1/update 401s without a token, 400s on a non-array body', async () => {
|
test('POST /api/equipment/v1/update favourites from the client’s own body', async () => {
|
||||||
const anon = await exports.default.fetch(`${ORIGIN}/api/equipment/v1/update`, {
|
// The body verbatim as the client sends it — a full echo of the entry it was served,
|
||||||
method: 'PUT',
|
// of which only `Favorited` is read.
|
||||||
headers: { 'Content-Type': 'application/json' },
|
const post = async (favorited: boolean, sub = '33') =>
|
||||||
body: '[]',
|
exports.default.fetch(`${ORIGIN}/api/equipment/v1/update`, {
|
||||||
})
|
method: 'POST',
|
||||||
expect(anon.status).toBe(401)
|
headers: { ...(await bearer(sub)), 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify([
|
||||||
|
{
|
||||||
|
PrefabName: '[ShareCamera]',
|
||||||
|
ModificationGuid: 'g5u0weNLmkCLeUXFUVn74Q',
|
||||||
|
FriendlyName: 'Camera Skin (Comic)',
|
||||||
|
Tooltip: 'ShareCamera Comic Debug: 2121',
|
||||||
|
Rarity: 5,
|
||||||
|
Favorited: favorited,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
|
||||||
const bad = await exports.default.fetch(`${ORIGIN}/api/equipment/v1/update`, {
|
// Nothing owned yet: the guid matches no row, so this is a silent no-op, not an error.
|
||||||
method: 'PUT',
|
expect((await post(true)).status).toBe(200)
|
||||||
headers: { ...(await bearer('32')), 'Content-Type': 'application/json' },
|
|
||||||
body: '{}',
|
await grantEquipment(env.DB, 33, {
|
||||||
|
PrefabName: '[ShareCamera]',
|
||||||
|
ModificationGuid: 'g5u0weNLmkCLeUXFUVn74Q',
|
||||||
|
FriendlyName: 'Camera Skin (Comic)',
|
||||||
|
Tooltip: 'ShareCamera Comic Debug: 2121',
|
||||||
|
Rarity: 5,
|
||||||
|
PlatformMask: -1,
|
||||||
|
Favorited: false,
|
||||||
})
|
})
|
||||||
expect(bad.status).toBe(400)
|
const owned = async () => {
|
||||||
|
const res = await exports.default.fetch(`${ORIGIN}/api/equipment/v2/getUnlocked`, {
|
||||||
|
headers: await bearer('33'),
|
||||||
|
})
|
||||||
|
return (await res.json()) as Array<{ ModificationGuid: string; Favorited: boolean }>
|
||||||
|
}
|
||||||
|
expect((await owned())[0]?.Favorited).toBe(false)
|
||||||
|
|
||||||
|
expect((await post(true)).status).toBe(200)
|
||||||
|
expect((await owned())[0]?.Favorited).toBe(true)
|
||||||
|
expect((await post(false)).status).toBe(200)
|
||||||
|
expect((await owned())[0]?.Favorited).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('equipment/v1/update 401s without a token, 400s on a non-array body (PUT and POST)', async () => {
|
||||||
|
for (const method of ['PUT', 'POST'] as const) {
|
||||||
|
const anon = await exports.default.fetch(`${ORIGIN}/api/equipment/v1/update`, {
|
||||||
|
method,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: '[]',
|
||||||
|
})
|
||||||
|
expect(anon.status).toBe(401)
|
||||||
|
|
||||||
|
const bad = await exports.default.fetch(`${ORIGIN}/api/equipment/v1/update`, {
|
||||||
|
method,
|
||||||
|
headers: { ...(await bearer('32')), 'Content-Type': 'application/json' },
|
||||||
|
body: '{}',
|
||||||
|
})
|
||||||
|
expect(bad.status).toBe(400)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
test('POST /api/storefronts/v2/buyItem 409s when the sent price no longer matches', async () => {
|
test('POST /api/storefronts/v2/buyItem 409s when the sent price no longer matches', async () => {
|
||||||
@@ -2770,6 +2823,7 @@ describe('econ endpoints', () => {
|
|||||||
'POST /api/checklist/v1/complete',
|
'POST /api/checklist/v1/complete',
|
||||||
'POST /api/checklist/v2/complete',
|
'POST /api/checklist/v2/complete',
|
||||||
'POST /api/consumables/v1/consume',
|
'POST /api/consumables/v1/consume',
|
||||||
|
'POST /api/equipment/v1/update',
|
||||||
'POST /api/gamerewards/v1/request',
|
'POST /api/gamerewards/v1/request',
|
||||||
'POST /api/items/bulkpurchase',
|
'POST /api/items/bulkpurchase',
|
||||||
'POST /api/objectives/v1/cleargroup',
|
'POST /api/objectives/v1/cleargroup',
|
||||||
|
|||||||
Reference in New Issue
Block a user