mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-25 23:18:51 -07:00
[2026] fix a few wire shapes
This commit is contained in:
@@ -297,6 +297,28 @@ export const progressionRoutes = new Hono<App>({ strict: false })
|
|||||||
}),
|
}),
|
||||||
async (c) => c.json(await getReputation(c.env.DB, Number.parseInt(c.req.param('id'), 10)))
|
async (c) => c.json(await getReputation(c.env.DB, Number.parseInt(c.req.param('id'), 10)))
|
||||||
)
|
)
|
||||||
|
// How much XP the caller has earned today — what the client measures against its daily
|
||||||
|
// XP cap. A BARE INTEGER (the client decodes a plain `int`), not a progression record.
|
||||||
|
//
|
||||||
|
// Registered BEFORE `/progression/:id` below, which otherwise takes `xpEarnedToday` as
|
||||||
|
// an account id and answers `{ PlayerId: null, Level: 1, XP: 0 }` — the client's decoder
|
||||||
|
// threw on that (`expected:'Number Token', actual:'{'`).
|
||||||
|
//
|
||||||
|
// STUB: always 0. Nothing here records when XP was earned, only the running total. No
|
||||||
|
// auth while the answer is the same for everybody; it will need the caller once it is
|
||||||
|
// real, since the path names no player.
|
||||||
|
.get(
|
||||||
|
'/api/players/v1/progression/xpEarnedToday',
|
||||||
|
describeRoute({
|
||||||
|
tags: ['Progression'],
|
||||||
|
summary: 'XP the caller earned today (stub)',
|
||||||
|
description:
|
||||||
|
'The XP earned so far today, as a bare integer. Always `0` — XP is banked as a ' +
|
||||||
|
'running total and nothing records when it was earned.',
|
||||||
|
responses: { 200: json(BareInteger, 'A bare `0`') },
|
||||||
|
}),
|
||||||
|
(c) => c.json(0)
|
||||||
|
)
|
||||||
.get(
|
.get(
|
||||||
'/api/players/v1/progression/:id',
|
'/api/players/v1/progression/:id',
|
||||||
describeRoute({
|
describeRoute({
|
||||||
|
|||||||
@@ -2081,6 +2081,14 @@ describe('public endpoints', () => {
|
|||||||
expect(res.status).toBe(400)
|
expect(res.status).toBe(400)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('GET /api/players/v1/progression/xpEarnedToday is a bare 0', async () => {
|
||||||
|
// A plain int on the client. It must not fall through to `/progression/:id`, which
|
||||||
|
// would read the path segment as an account id and answer a progression OBJECT.
|
||||||
|
const res = await exports.default.fetch(`${ORIGIN}/api/players/v1/progression/xpEarnedToday`)
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
expect(await res.json()).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
test('GET /api/progressionEvents/active is a bare 18 (no auth)', async () => {
|
test('GET /api/progressionEvents/active is a bare 18 (no auth)', async () => {
|
||||||
// A hardcoded stub: the id of an archived event (0 would mean "no active event").
|
// A hardcoded stub: the id of an archived event (0 would mean "no active event").
|
||||||
// A 404 would stall the client's load instead.
|
// A 404 would stall the client's load instead.
|
||||||
@@ -10208,6 +10216,7 @@ describe('openapi', () => {
|
|||||||
'GET /api/playerevents/v1/{eventId}/responses',
|
'GET /api/playerevents/v1/{eventId}/responses',
|
||||||
'GET /api/playerevents/v2/{eventId}',
|
'GET /api/playerevents/v2/{eventId}',
|
||||||
'GET /api/players/v1/playerPhotoTaggingSetting',
|
'GET /api/players/v1/playerPhotoTaggingSetting',
|
||||||
|
'GET /api/players/v1/progression/xpEarnedToday',
|
||||||
'GET /api/players/v1/progression/{id}',
|
'GET /api/players/v1/progression/{id}',
|
||||||
'GET /api/players/v2/progression/bulk',
|
'GET /api/players/v2/progression/bulk',
|
||||||
'GET /api/progressionEvents/active',
|
'GET /api/progressionEvents/active',
|
||||||
|
|||||||
@@ -48,12 +48,13 @@ const app = new Hono<App>()
|
|||||||
return c.json([])
|
return c.json([])
|
||||||
})
|
})
|
||||||
|
|
||||||
// Sampling configuration, asked for once per session (`?sessionId=<guid>`). An empty
|
// Sampling configuration, asked for once per session (`?sessionId=<guid>`). A BARE ARRAY
|
||||||
// object carries no per-event overrides, which the client reads as "sample everything
|
// of `{ EventType, ShouldBeSampled }` entries (the client decodes a `List<EventDTO>`),
|
||||||
// at the built-in default rates". Since the events are discarded anyway, the rate it
|
// e.g. `[{ "EventType": "player_join", "ShouldBeSampled": true }]` — not an object: the
|
||||||
// picks makes no difference here.
|
// `{}` this once served made the client's decoder throw. Empty says "sample nothing",
|
||||||
|
// which is the right answer since the events are discarded anyway.
|
||||||
.get('/sampling', async (c) => {
|
.get('/sampling', async (c) => {
|
||||||
return c.json({})
|
return c.json([])
|
||||||
})
|
})
|
||||||
|
|
||||||
export default app
|
export default app
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ it('acknowledges an unreadable body on both event paths', async () => {
|
|||||||
it('serves an empty sampling configuration', async () => {
|
it('serves an empty sampling configuration', async () => {
|
||||||
const res = await SELF.fetch(`${ORIGIN}/sampling?sessionId=e9a899ce-ce46-447d-bb22-11e82ed68f8d`)
|
const res = await SELF.fetch(`${ORIGIN}/sampling?sessionId=e9a899ce-ce46-447d-bb22-11e82ed68f8d`)
|
||||||
expect(res.status).toBe(200)
|
expect(res.status).toBe(200)
|
||||||
expect(await res.json()).toEqual({})
|
expect(await res.json()).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
// The client always sends `sessionId`, but nothing here reads it, so a missing one must
|
// The client always sends `sessionId`, but nothing here reads it, so a missing one must
|
||||||
@@ -57,5 +57,5 @@ it('serves an empty sampling configuration', async () => {
|
|||||||
it('serves the sampling configuration without a sessionId', async () => {
|
it('serves the sampling configuration without a sessionId', async () => {
|
||||||
const res = await SELF.fetch(`${ORIGIN}/sampling`)
|
const res = await SELF.fetch(`${ORIGIN}/sampling`)
|
||||||
expect(res.status).toBe(200)
|
expect(res.status).toBe(200)
|
||||||
expect(await res.json()).toEqual({})
|
expect(await res.json()).toEqual([])
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -96,14 +96,24 @@ const app = new Hono<App>()
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// The caller's CRM configuration — the campaign/messaging settings the client fetches on
|
// The caller's CRM configuration — which CRM (Braze, Rengage) and push service the
|
||||||
// startup before it will ask about anything else here. An object, not an envelope.
|
// client is assigned, fetched on startup before it will ask about anything else here.
|
||||||
//
|
//
|
||||||
// STUB: there is no CRM behind this, so the config is EMPTY rather than invented. An
|
// ENVELOPED: the client decodes a `RecNetResultObject<CrmAssignmentDTO>` and reads
|
||||||
// empty object is the honest answer and the client reads it as "nothing configured";
|
// `Value` off it itself, so the `{ Value, Success, Error, error_id }` wrapper is on the
|
||||||
// made-up keys would switch on messaging paths that have nothing to serve them. Unlike
|
// wire — this once served a bare `{}`, which leaves `Value` null. Note the casing:
|
||||||
// `/config/categories` there is no visible text to mark as a stub — nothing here is
|
// PascalCase `Success`/`Error` beside a snake_case `error_id`.
|
||||||
// rendered — so the marker stays in this comment.
|
//
|
||||||
|
// `Value` has exactly two keys. `UserConfig.AssignedCrmType` is the `CrmType` enum (None
|
||||||
|
// 0, Braze 1, Rengage 2) and `AllowPush` a bool — both NON-nullable on the client, so
|
||||||
|
// never null. `AssignedPushNotificationService` is a nullable `PushNotificationServices`
|
||||||
|
// (APNS 0, Firebase 1, Mock 2, Braze 3, Rengage 4). `BrazeConfig` is `{ AppId,
|
||||||
|
// SdkEndpoint }` or null.
|
||||||
|
//
|
||||||
|
// STUB: there is no CRM behind this, so the answer is "none assigned" — type 0, no push
|
||||||
|
// service, push off, no Braze config — rather than an invented assignment that would
|
||||||
|
// switch on messaging paths with nothing to serve them. Nothing here is rendered, so
|
||||||
|
// the stub marker stays in this comment.
|
||||||
//
|
//
|
||||||
// Auth-gated because the route is `me`-scoped, like `/preferences` below: the config
|
// Auth-gated because the route is `me`-scoped, like `/preferences` below: the config
|
||||||
// belongs to the caller even while the answer is the same for everyone.
|
// belongs to the caller even while the answer is the same for everyone.
|
||||||
@@ -111,7 +121,19 @@ const app = new Hono<App>()
|
|||||||
const accountId = await authedId(c)
|
const accountId = await authedId(c)
|
||||||
if (accountId === null) return unauthorized(c)
|
if (accountId === null) return unauthorized(c)
|
||||||
|
|
||||||
return c.json({})
|
return c.json({
|
||||||
|
Value: {
|
||||||
|
UserConfig: {
|
||||||
|
AssignedCrmType: 0,
|
||||||
|
AssignedPushNotificationService: null,
|
||||||
|
AllowPush: false,
|
||||||
|
},
|
||||||
|
BrazeConfig: null,
|
||||||
|
},
|
||||||
|
Success: true,
|
||||||
|
Error: null,
|
||||||
|
error_id: null,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// The caller's own notification preferences — which categories they have muted, by
|
// The caller's own notification preferences — which categories they have muted, by
|
||||||
|
|||||||
@@ -123,9 +123,22 @@ it('serves the caller’s CRM config', async () => {
|
|||||||
const res = await SELF.fetch(`${ORIGIN}/crm/me/config/v3`, { headers: await bearer('205') })
|
const res = await SELF.fetch(`${ORIGIN}/crm/me/config/v3`, { headers: await bearer('205') })
|
||||||
expect(res.status).toBe(200)
|
expect(res.status).toBe(200)
|
||||||
expect(res.headers.get('content-type')).toContain('application/json')
|
expect(res.headers.get('content-type')).toContain('application/json')
|
||||||
// Empty rather than invented: there is no CRM behind this, and made-up keys would switch
|
// A RecNetResultObject envelope — the client reads `Value` off it. "No CRM assigned"
|
||||||
// on client messaging paths with nothing to serve them.
|
// rather than invented: there is no CRM behind this. `AssignedCrmType` and `AllowPush`
|
||||||
expect(await res.json()).toEqual({})
|
// are non-nullable on the client, so they must never be null.
|
||||||
|
expect(await res.json()).toEqual({
|
||||||
|
Value: {
|
||||||
|
UserConfig: {
|
||||||
|
AssignedCrmType: 0,
|
||||||
|
AssignedPushNotificationService: null,
|
||||||
|
AllowPush: false,
|
||||||
|
},
|
||||||
|
BrazeConfig: null,
|
||||||
|
},
|
||||||
|
Success: true,
|
||||||
|
Error: null,
|
||||||
|
error_id: null,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('401s the CRM config read without a bearer token', async () => {
|
it('401s the CRM config read without a bearer token', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user