Stop guessing which board a request means
There was a shim reading a missing game as lemonade. It existed because the deployed lemonade bundle predated this service and posted no game at all, and copies of it were sitting in browsers -- removing it while that was true would have sent every score from a cached page nowhere. It is no longer true. The lemonade bundle has been rebuilt and is what the site actually serves, checked by reading the asset the public URL points at rather than the one the origin holds: there is a CDN in between and those are two different claims. Both bundles name their game on read and on write. So the guess goes. It was right while there was something to guess for and is wrong now: filing an unlabelled score under whichever game happens to be first is the kind of default that stays invisible until it is wrong, and the caller always knows which game it is. A request that does not say is refused, and the refusal lists the boards there are rather than only saying no. One test, over the shapes a missing game actually arrives in -- undefined, null, empty string, and the falsy values that are not strings at all. 19 tests.
This commit is contained in:
@@ -79,14 +79,17 @@ accounts has ever been able to offer.
|
||||
Every entry in a post is validated before any of them is written: a party that
|
||||
half-posts is worse than one that does not post at all.
|
||||
|
||||
### The missing-game shim
|
||||
### Every request names its game
|
||||
|
||||
A request with no `game` is treated as `lemonade`. That is a compatibility
|
||||
shim, not a default worth keeping — the deployed lemonade bundle posts no game
|
||||
at all, because when it was built there was only one board, and copies of it
|
||||
are sitting in people's browsers. It can go once that bundle has been rebuilt
|
||||
and redeployed, and not before, or every score set from a cached page lands
|
||||
nowhere.
|
||||
There is no default. A request that does not say which board it wants is
|
||||
refused with the list of boards there are.
|
||||
|
||||
There was briefly a shim that read a missing `game` as `lemonade`, because the
|
||||
deployed lemonade bundle predated this service and posted no game at all. It
|
||||
was removed once that bundle had been rebuilt and was what the site served —
|
||||
verified by reading the asset the public URL actually points at, not the one
|
||||
the origin holds, because there is a CDN in between and the two are not the
|
||||
same claim.
|
||||
|
||||
## The migration
|
||||
|
||||
|
||||
+16
-12
@@ -113,10 +113,10 @@ function readBody(req: IncomingMessage): Promise<string> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Rows go back in the game's own field names, flattened, exactly as its
|
||||
* client already expects them. That is not politeness -- the lemonade stand's
|
||||
* bundle is sitting in people's browsers right now expecting `assets` and
|
||||
* `days`, and it will keep expecting them until it is rebuilt.
|
||||
* Rows go back in the game's own field names, flattened, exactly as its client
|
||||
* expects them. That is the bargain that makes a shared service invisible: a
|
||||
* game asks for its board and gets `assets` and `days`, or `bagged` and
|
||||
* `arrows`, and never has to know it is sharing a table with anybody.
|
||||
*/
|
||||
const shape = (rows: Row[]) =>
|
||||
rows.map((r) => ({ id: r.id, name: r.name, at: r.at, ...r.fields }))
|
||||
@@ -124,14 +124,18 @@ const shape = (rows: Row[]) =>
|
||||
/**
|
||||
* Which game a request is about.
|
||||
*
|
||||
* A missing game means lemonade, and that is a compatibility shim rather than
|
||||
* a default worth keeping: the deployed lemonade client posts no game at all,
|
||||
* because when it was built there was only one board. It can go once that
|
||||
* bundle has been rebuilt and redeployed -- and not before, or every score set
|
||||
* from a cached page lands nowhere.
|
||||
* There was a shim here that read a missing game as lemonade, because the
|
||||
* deployed lemonade bundle predated this service and posted no game at all.
|
||||
* That bundle has been rebuilt and is what the site serves now -- both games
|
||||
* name themselves on every call -- so the shim has been removed and a request
|
||||
* that does not say which board it wants is refused rather than guessed at.
|
||||
*
|
||||
* Guessing was the right thing while there was something to guess for. It is
|
||||
* the wrong thing now: silently filing an unlabelled score under whichever
|
||||
* game happened to be first is the sort of default that is invisible until it
|
||||
* is wrong, and the caller always knows which game it is.
|
||||
*/
|
||||
function gameFor(explicit: unknown): Game | undefined {
|
||||
if (explicit === undefined || explicit === null || explicit === '') return lookup('lemonade')
|
||||
return lookup(explicit)
|
||||
}
|
||||
|
||||
@@ -153,7 +157,7 @@ const server = createServer(async (req, res) => {
|
||||
|
||||
if (path === '/api/scores' && req.method === 'GET') {
|
||||
const game = gameFor(url.searchParams.get('game') ?? undefined)
|
||||
if (!game) return send(res, 404, { error: 'unknown game' })
|
||||
if (!game) return send(res, 404, { error: `unknown game; try one of: ${known().join(', ')}` })
|
||||
return send(res, 200, { game: game.id, scores: shape(store.board(game, BOARD_LIMIT)) })
|
||||
}
|
||||
|
||||
@@ -170,7 +174,7 @@ const server = createServer(async (req, res) => {
|
||||
|
||||
const body = (parsed ?? {}) as { game?: unknown; entries?: unknown }
|
||||
const game = gameFor(body.game)
|
||||
if (!game) return send(res, 404, { error: 'unknown game' })
|
||||
if (!game) return send(res, 404, { error: `unknown game; try one of: ${known().join(', ')}` })
|
||||
|
||||
const list = Array.isArray(parsed) ? parsed : body.entries
|
||||
if (!Array.isArray(list)) return send(res, 400, { error: 'expected an array of entries' })
|
||||
|
||||
@@ -15,6 +15,21 @@ describe('the registry', () => {
|
||||
expect(lookup('doom')).toBeUndefined()
|
||||
expect(lookup(42)).toBeUndefined()
|
||||
})
|
||||
|
||||
/**
|
||||
* There is no default game, and there must not be one.
|
||||
*
|
||||
* A shim used to read a missing game as lemonade, for bundles that predated
|
||||
* this service. Now that nothing posts unlabelled, guessing would be worse
|
||||
* than refusing: it would file somebody's score under whichever game
|
||||
* happened to be first, silently, and nobody would find out until the board
|
||||
* looked wrong. These are the shapes a missing game arrives in.
|
||||
*/
|
||||
it('refuses to guess when nothing says which board', () => {
|
||||
for (const missing of [undefined, null, '', 0, false, {}, []]) {
|
||||
expect(lookup(missing)).toBeUndefined()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user