Files
vngeoguessr/tests/region-request.test.js
T
tiennm99 00a8f8b627 feat(api): score a guess against the district it was actually in
A round now carries two regions. The one the player picked is public and comes
back in every response; the one the panorama actually sits in is a secret, and
it is what the leaderboard fans out from. Revealing the second before the
guess would collapse a country-wide round to a single district, so it joins
the exact coordinates on the never-serialized list -- including in the session
lookup handler, which echoes fields back to whoever holds the session id, and
that is the player.

Session consumption is now a claim rather than a courtesy. Reading a session
and then deleting it is not a guard: ten concurrent submits all read it alive,
all delete it, and all score. DEL is atomic and returns how many keys it
removed, so exactly one caller sees a 1 -- the route scores only if it won
that. Consuming before the writes also closes the sequential case, where a
failure partway through the fan-out would otherwise leave the session alive
for half an hour and let a retry re-credit every level that already succeeded.
A guess lost to a mid-write failure is the accepted cost.

Region parsing lives in one place. Four routes accept a region, and four
slightly different ideas about casing and defaulting is how a typo becomes a
leaderboard key nobody reads. Unknown and uncovered codes are rejected with a
400 that names the region, rather than served as an empty board that looks
exactly like a region nobody has played yet.

Sessions created before this change still score, at province level, since they
carry no district. They expire within half an hour, so the fallback can go a
release from now.

The debug coverage route serves any region with an outline rather than only
the five provinces. Its directory keeps the city-coverage name for now: the
page that calls it cannot be read in this environment, and renaming the route
without updating its caller would break it.
2026-08-30 18:06:55 +07:00

89 lines
2.9 KiB
JavaScript

import { describe, it, expect } from 'vitest';
import {
resolveRegion,
resolvePlayableRegion,
publicRegion,
} from '../src/lib/region-request.js';
const params = (query) => new URLSearchParams(query);
describe('resolveRegion', () => {
it('accepts a region at any level', () => {
for (const code of ['VN', 'TPHCM', 'TPHCM-Q7', 'DL']) {
expect(resolveRegion(params(`region=${code}`), true)).toEqual({ ok: true, code });
}
});
it('still accepts ?city=, which every existing link sends', () => {
expect(resolveRegion(params('city=HN'), true)).toEqual({ ok: true, code: 'HN' });
});
it('prefers region over city when both are present', () => {
expect(resolveRegion(params('region=DL&city=HN'), true).code).toBe('DL');
});
it('uppercases what it is given', () => {
expect(resolveRegion(params('region=tphcm-q7'), true).code).toBe('TPHCM-Q7');
});
it('treats an absent region as the country when one is optional', () => {
expect(resolveRegion(params(''), false)).toEqual({ ok: true, code: 'VN' });
});
it('treats an empty region as the country rather than an error', () => {
// ?city= with no value is what a cleared filter sends, and it used to mean
// the global board.
expect(resolveRegion(params('city='), false)).toEqual({ ok: true, code: 'VN' });
});
it('rejects an unknown code with a 400, not an empty result', () => {
const result = resolveRegion(params('region=NOPE'), true);
expect(result.ok).toBe(false);
expect(result.status).toBe(400);
expect(result.error).toMatch(/Unknown region: NOPE/);
});
it('rejects a missing region when one is required', () => {
expect(resolveRegion(params(''), true).status).toBe(400);
});
});
describe('resolvePlayableRegion', () => {
it('accepts a region with coverage', () => {
expect(resolvePlayableRegion(params('region=DL')).ok).toBe(true);
});
it.each(['TPHCM-CUCHI', 'DN-CAMLE', 'DN-HOAVANG'])(
'rejects %s, which has no coverage',
(code) => {
// Better here than as "no panoramas left to try" from deep inside the
// draw. Cu Chi has no boundary; the two Da Nang districts have no imagery.
const result = resolvePlayableRegion(params(`region=${code}`));
expect(result.ok).toBe(false);
expect(result.status).toBe(400);
expect(result.error).toMatch(/no street view coverage/);
}
);
});
describe('publicRegion', () => {
it('describes what the player picked', () => {
expect(publicRegion('TPHCM')).toEqual({
code: 'TPHCM',
name: 'Ho Chi Minh',
path: ['Vietnam', 'Ho Chi Minh'],
level: 'province',
});
});
it('gives the country a one-element path', () => {
// A country round must not hint at a province, let alone a district.
expect(publicRegion('VN').path).toEqual(['Vietnam']);
});
it('carries no coordinates', () => {
const serialised = JSON.stringify(publicRegion('DL'));
expect(serialised).not.toMatch(/lat|lng|bbox|center/);
});
});