Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/handbook/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Function: normalizePlaceOrigin

- **Purpose:** Trim a pin origin and accept only `^[a-z][a-z0-9-]{0,31}$`. Uppercase tokens such as SPAR are invalid.
- **Purpose:** Trim a pin origin and accept only `^[a-z0-9][a-z0-9-]{0,31}$`. A leading digit is valid. Uppercase tokens such as SPAR are invalid.
- **Inputs:** Unknown value, typically a JSON field or the `origin` query string.
- **Returns / side effects:** `{ ok: true, value }` with the trimmed origin, or `{ ok: false, error: "Place origin is invalid" }`. No I/O.
- **Used by:** `normalizeMapPlace`, `normalizeMapPlaceKey`, and `mapRoutes` on `GET /map/places`.
Expand Down
29 changes: 29 additions & 0 deletions src/__tests__/map/place.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ describe('normalizePlaceOrigin', () => {
expect(normalizePlaceOrigin(' spar ')).toEqual({ ok: true, value: 'spar' });
});

it('trims an origin that starts with a digit', () => {
expect(normalizePlaceOrigin(' 21gifts ')).toEqual({ ok: true, value: '21gifts' });
});

it('accepts a 32-character origin that starts with a digit', () => {
const origin = '2' + 'a'.repeat(31);
expect(normalizePlaceOrigin(origin)).toEqual({ ok: true, value: origin });
});

it('rejects SPAR and any other invalid value', () => {
expect(normalizePlaceOrigin('SPAR')).toEqual({
ok: false,
Expand All @@ -36,6 +45,26 @@ describe('normalizePlaceOrigin', () => {
ok: false,
error: 'Place origin is invalid',
});
expect(normalizePlaceOrigin('2' + 'a'.repeat(32))).toEqual({
ok: false,
error: 'Place origin is invalid',
});
expect(normalizePlaceOrigin('-a')).toEqual({
ok: false,
error: 'Place origin is invalid',
});
expect(normalizePlaceOrigin('21.gifts')).toEqual({
ok: false,
error: 'Place origin is invalid',
});
expect(normalizePlaceOrigin('21Gifts')).toEqual({
ok: false,
error: 'Place origin is invalid',
});
expect(normalizePlaceOrigin(' ')).toEqual({
ok: false,
error: 'Place origin is invalid',
});
});
});

Expand Down
53 changes: 53 additions & 0 deletions src/__tests__/map/routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,59 @@ describe('map routes', () => {
expect(all.places.map((row) => row.origin).sort()).toEqual(['dfx', 'spar']);
});

it('accepts origin 21gifts on PUT and GET and rejects a leading hyphen or uppercase', async () => {
const api = app({ token: 'secret' });
const created = await api.request('/map/places', {
method: 'PUT',
headers: jsonHeaders,
body: JSON.stringify({
origin: '21gifts',
externalId: 'shop-1',
name: 'Shop',
lat: 14.620311,
lon: 120.959626,
category: 'shopping',
paymentMethods: 'lightning',
techProvider: '21.gifts',
}),
});
expect(created.status).toBe(201);
const createdJson = (await created.json()) as { created: boolean };
expect(createdJson.created).toBe(true);
const listed = await api.request('/map/places?origin=21gifts');
expect(listed.status).toBe(200);
const listedJson = (await listed.json()) as {
places: Array<{ origin: string; name: string; category: string; techProvider: string }>;
};
expect(listedJson.places).toHaveLength(1);
expect(listedJson.places[0]?.origin).toBe('21gifts');
expect(listedJson.places[0]?.name).toBe('Shop');
expect(listedJson.places[0]?.category).toBe('shopping');
expect(listedJson.places[0]?.techProvider).toBe('21.gifts');
const hyphen = await api.request('/map/places?origin=-21gifts');
expect(hyphen.status).toBe(400);
expect(await hyphen.json()).toEqual({ error: 'Place origin is invalid' });
const uppercase = await api.request('/map/places?origin=21Gifts');
expect(uppercase.status).toBe(400);
expect(await uppercase.json()).toEqual({ error: 'Place origin is invalid' });
const putHyphen = await api.request('/map/places', {
method: 'PUT',
headers: jsonHeaders,
body: JSON.stringify({
origin: '-21gifts',
externalId: 'shop-2',
name: 'Shop',
lat: 14.620311,
lon: 120.959626,
category: 'shopping',
paymentMethods: 'lightning',
techProvider: '21.gifts',
}),
});
expect(putHyphen.status).toBe(400);
expect(await putHyphen.json()).toEqual({ error: 'Place origin is invalid' });
});

it('rejects a bad limit', async () => {
const api = app({});
expect((await api.request('/map/places?limit=0')).status).toBe(400);
Expand Down
2 changes: 1 addition & 1 deletion src/lib/map/place.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ function uniqueSortedSupports(items: MapPlaceSupport[]): MapPlaceSupport[] {
export function normalizePlaceOrigin(
raw: unknown,
): { ok: true; value: string } | { ok: false; error: string } {
if (typeof raw !== 'string' || !/^[a-z][a-z0-9-]{0,31}$/.test(raw.trim())) {
if (typeof raw !== 'string' || !/^[a-z0-9][a-z0-9-]{0,31}$/.test(raw.trim())) {
return { ok: false, error: ORIGIN_ERROR };
}
return { ok: true, value: raw.trim() };
Expand Down
Loading