mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 14:41:28 -07:00
add currency, outfits
This commit is contained in:
@@ -5,6 +5,13 @@ import { beforeAll, describe, expect, test } from 'vitest'
|
||||
import '../../econ.app'
|
||||
|
||||
import { SCHEMA_DDL } from '../../avatar-db'
|
||||
import {
|
||||
BALANCE_SCHEMA_DDL,
|
||||
CurrencyType,
|
||||
getBalance,
|
||||
spendCurrency,
|
||||
} from '../../balance-db'
|
||||
import { OUTFIT_SCHEMA_DDL } from '../../outfit-db'
|
||||
|
||||
import type { Env } from '../../context'
|
||||
|
||||
@@ -20,11 +27,32 @@ beforeAll(async () => {
|
||||
// Seed the shared JWT signing key into the local Secrets Store so .get() resolves.
|
||||
await adminSecretsStore(env.JWT_SECRET).create('test-signing-key')
|
||||
for (const stmt of SCHEMA_DDL) await env.DB.prepare(stmt).run()
|
||||
for (const stmt of BALANCE_SCHEMA_DDL) await env.DB.prepare(stmt).run()
|
||||
for (const stmt of OUTFIT_SCHEMA_DDL) await env.DB.prepare(stmt).run()
|
||||
await env.DB.prepare('INSERT OR IGNORE INTO account (data) VALUES (?1)')
|
||||
.bind(JSON.stringify({ accountId: 42, username: 'Tester', displayName: 'Tester' }))
|
||||
.run()
|
||||
})
|
||||
|
||||
/**
|
||||
* A real outfit as the client posts it to /api/avatar/v3/saved/set — kept verbatim
|
||||
* (including the JSON-in-a-string OutfitSelectionsV2/FaceFeatures fields) so the
|
||||
* round-trip is tested against the actual payload shape, not a tidied-up version.
|
||||
*/
|
||||
const SAVED_OUTFIT = {
|
||||
Slot: 4,
|
||||
PreviewImageName: 'outfit/2026-07-14/38e84678-1ccf-4cfd-bf3f-5b21eec88b0f.jpg',
|
||||
OutfitSelections:
|
||||
'5cd08cfb-c729-4c30-96d9-6a99bb934d91,,1;77d3c585-4928-4471-a425-89036efe7299,,0;40528de7-38a3-4a7c-8f93-6d3bfa5573f2,51ef8d39-2b94-4f9e-9620-07b6b0a913a5,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,,0;d0a9262f-5504-46a7-bb10-7507503db58e,95e4cc30-cb68-473d-a395-feadf5b51512,0440f08f-ef1d-49d8-942b-523056e8bb45,,1',
|
||||
OutfitSelectionsV2:
|
||||
'{"selections":[{"PrefabGuid":"5cd08cfb-c729-4c30-96d9-6a99bb934d91","CombinationGuid":"","BodyPart":1,"UgcOutfitData":{"BaseAvatarItemColor":{"r":0.0,"g":0.0,"b":0.0,"a":0.0},"CustomAvatarItemId":""}}]}',
|
||||
FaceFeatures:
|
||||
'{"ver":6,"eyeId":"pY0dY6IxOEaNv8uNL8qUgQ","eyeScl":-0.007145103067159653,"useHelmetHair":1,"hideEars":false}',
|
||||
SkinColor: 'Xac-W_R330KfOz-pQla9qg',
|
||||
HairColor: 'UAT0OaWEkUG-mWDIyiX1Kg',
|
||||
CustomAvatarItems: [],
|
||||
}
|
||||
|
||||
// Mint a token the way the `auth` worker does, signing with the shared test key seeded into the JWT_SECRET store.
|
||||
const TEST_SECRET = 'test-signing-key'
|
||||
|
||||
@@ -247,13 +275,69 @@ describe('econ endpoints', () => {
|
||||
test('GET /api/avatar/v3/saved 401s without a token, returns [] with one', async () => {
|
||||
const anon = await exports.default.fetch(`${ORIGIN}/api/avatar/v3/saved`)
|
||||
expect(anon.status).toBe(401)
|
||||
// Account 21 has saved nothing.
|
||||
const res = await exports.default.fetch(`${ORIGIN}/api/avatar/v3/saved`, {
|
||||
headers: await bearer(),
|
||||
headers: await bearer('21'),
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.json()).toEqual([])
|
||||
})
|
||||
|
||||
test('POST /api/avatar/v3/saved/set saves an outfit, read back by /saved', async () => {
|
||||
const anon = await exports.default.fetch(`${ORIGIN}/api/avatar/v3/saved/set`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(SAVED_OUTFIT),
|
||||
})
|
||||
expect(anon.status).toBe(401)
|
||||
|
||||
const res = await exports.default.fetch(`${ORIGIN}/api/avatar/v3/saved/set`, {
|
||||
method: 'POST',
|
||||
headers: { ...(await bearer('22')), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(SAVED_OUTFIT),
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.json()).toEqual(SAVED_OUTFIT)
|
||||
|
||||
// Round-trips verbatim — including the JSON-in-a-string fields the client parses
|
||||
// back itself (OutfitSelectionsV2, FaceFeatures).
|
||||
const saved = await exports.default.fetch(`${ORIGIN}/api/avatar/v3/saved`, {
|
||||
headers: await bearer('22'),
|
||||
})
|
||||
expect(await saved.json()).toEqual([SAVED_OUTFIT])
|
||||
})
|
||||
|
||||
test('POST /api/avatar/v3/saved/set overwrites the same slot, and keeps others', async () => {
|
||||
const headers = await bearer('23')
|
||||
const post = (outfit: unknown) =>
|
||||
exports.default.fetch(`${ORIGIN}/api/avatar/v3/saved/set`, {
|
||||
method: 'POST',
|
||||
headers: { ...headers, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(outfit),
|
||||
})
|
||||
|
||||
await post({ ...SAVED_OUTFIT, Slot: 4, SkinColor: 'first' })
|
||||
await post({ ...SAVED_OUTFIT, Slot: 7, SkinColor: 'other-slot' })
|
||||
// Re-saving slot 4 replaces it rather than adding a second row for it.
|
||||
await post({ ...SAVED_OUTFIT, Slot: 4, SkinColor: 'second' })
|
||||
|
||||
const res = await exports.default.fetch(`${ORIGIN}/api/avatar/v3/saved`, { headers })
|
||||
const outfits = (await res.json()) as Array<{ Slot: number; SkinColor: string }>
|
||||
expect(outfits.map((o) => [o.Slot, o.SkinColor])).toEqual([
|
||||
[4, 'second'],
|
||||
[7, 'other-slot'],
|
||||
])
|
||||
})
|
||||
|
||||
test('POST /api/avatar/v3/saved/set 400s without an integer Slot', async () => {
|
||||
const { Slot: _Slot, ...noSlot } = SAVED_OUTFIT
|
||||
const res = await exports.default.fetch(`${ORIGIN}/api/avatar/v3/saved/set`, {
|
||||
method: 'POST',
|
||||
headers: { ...(await bearer('24')), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(noSlot),
|
||||
})
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
|
||||
test('GET /api/avatar/v2/gifts 401s without a token, returns [] with one', async () => {
|
||||
const anon = await exports.default.fetch(`${ORIGIN}/api/avatar/v2/gifts`)
|
||||
expect(anon.status).toBe(401)
|
||||
@@ -333,7 +417,43 @@ describe('econ endpoints', () => {
|
||||
headers: await bearer(),
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.json()).toEqual([{ CurrencyType: 2, Platform: -2, Balance: 2147483648 }])
|
||||
// The starting grant, applied on this first read.
|
||||
expect(await res.json()).toEqual([{ CurrencyType: 2, Platform: -2, Balance: 10000 }])
|
||||
})
|
||||
|
||||
test('GET /api/storefronts/v4/balance/2 reflects what the player has spent', async () => {
|
||||
// Spend from account 7 (a fresh account: the read below grants it first).
|
||||
expect(await spendCurrency(env.DB, 7, CurrencyType.RecCenterTokens, 2500)).toBe(true)
|
||||
const res = await exports.default.fetch(`${ORIGIN}/api/storefronts/v4/balance/2`, {
|
||||
headers: await bearer('7'),
|
||||
})
|
||||
expect(await res.json()).toEqual([{ CurrencyType: 2, Platform: -2, Balance: 7500 }])
|
||||
})
|
||||
|
||||
test('a spend the player cannot afford changes nothing', async () => {
|
||||
const before = await getBalance(env.DB, 8, CurrencyType.RecCenterTokens)
|
||||
expect(await spendCurrency(env.DB, 8, CurrencyType.RecCenterTokens, before + 1)).toBe(false)
|
||||
expect(await getBalance(env.DB, 8, CurrencyType.RecCenterTokens)).toBe(before)
|
||||
})
|
||||
|
||||
test('the starting grant is not re-granted after spending down to zero', async () => {
|
||||
// The grant is INSERT OR IGNORE against the row, not a top-up: a player who spends
|
||||
// everything stays at 0 rather than being refilled by their next balance read.
|
||||
expect(await spendCurrency(env.DB, 9, CurrencyType.RecCenterTokens, 10_000)).toBe(true)
|
||||
const res = await exports.default.fetch(`${ORIGIN}/api/storefronts/v4/balance/2`, {
|
||||
headers: await bearer('9'),
|
||||
})
|
||||
expect(await res.json()).toEqual([{ CurrencyType: 2, Platform: -2, Balance: 0 }])
|
||||
})
|
||||
|
||||
test('GET /api/storefronts/v4/balance for a room-scoped currency returns 0, not a balance', async () => {
|
||||
// RoomCurrency (300) is scoped to a room and served elsewhere; this table must not
|
||||
// hand out an account-wide balance for it.
|
||||
const res = await exports.default.fetch(`${ORIGIN}/api/storefronts/v4/balance/300`, {
|
||||
headers: await bearer(),
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.json()).toEqual([{ CurrencyType: 300, Platform: -2, Balance: 0 }])
|
||||
})
|
||||
|
||||
test('GET /api/storefronts/v3/giftdropstore/3 returns the storefront catalog', async () => {
|
||||
|
||||
Reference in New Issue
Block a user