mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 07:40:59 +00:00
8fd46c4c18
Small requests from the discussions, each built end to end: planner and map (cycling mode, date first headings, clear a day, stay times per day, off-route stops, map lock, compact unplanned markers, pending stays, cable car, transit quick picks), places (location filter, category from OSM, open in maps, geo: link, image from an attachment, enrichment on file import, places near a pin, place name language, Amap first in China), lists and costs (packed counter, inherited sharing, Markdown to-dos, payment notes, bookings CSV, my plan PDF), sharing (unplanned places, travel only, without photos), journey (photo zoom, drafts, photo order, trip suggestion, day jump), atlas (region counter, region bucket list, places per country), vacay half company holidays, studio copy and paste plus the book's own layouts, and RESTORE_FROM_BACKUP for a first start restore.
212 lines
8.5 KiB
TypeScript
212 lines
8.5 KiB
TypeScript
import {
|
|
mapsSearchRequestSchema,
|
|
mapsNearbyRequestSchema,
|
|
mapsAutocompleteRequestSchema,
|
|
mapsReverseQuerySchema,
|
|
mapsResolveUrlRequestSchema,
|
|
mapsPlaceEnrichmentRequestSchema,
|
|
mapsPlaceEnrichmentResultSchema,
|
|
placePhotoCandidateSchema,
|
|
placeDescriptionSchema,
|
|
} from './maps.schema';
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
|
|
describe('mapsNearbyRequestSchema', () => {
|
|
it('takes a point and caps the circle and the count', () => {
|
|
expect(mapsNearbyRequestSchema.safeParse({ lat: 52.5, lng: 13.4 }).success).toBe(true);
|
|
expect(mapsNearbyRequestSchema.safeParse({ lat: 52.5, lng: 13.4, radius: 5000, limit: 20 }).success).toBe(true);
|
|
expect(mapsNearbyRequestSchema.safeParse({ lat: 52.5, lng: 13.4, radius: 5001 }).success).toBe(false);
|
|
expect(mapsNearbyRequestSchema.safeParse({ lat: 52.5, lng: 13.4, limit: 21 }).success).toBe(false);
|
|
expect(mapsNearbyRequestSchema.safeParse({ lat: 91, lng: 13.4 }).success).toBe(false);
|
|
expect(mapsNearbyRequestSchema.safeParse({ lng: 13.4 }).success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('mapsSearchRequestSchema', () => {
|
|
it('requires a non-empty query', () => {
|
|
expect(mapsSearchRequestSchema.safeParse({ query: 'berlin' }).success).toBe(true);
|
|
expect(mapsSearchRequestSchema.safeParse({ query: '' }).success).toBe(false);
|
|
expect(mapsSearchRequestSchema.safeParse({}).success).toBe(false);
|
|
});
|
|
|
|
it('takes "google" as the one provider a search can be sent to alone, and nothing else', () => {
|
|
expect(mapsSearchRequestSchema.safeParse({ query: 'berlin', provider: 'google' }).success).toBe(true);
|
|
expect(mapsSearchRequestSchema.safeParse({ query: 'berlin', provider: 'osm' }).success).toBe(false);
|
|
expect(mapsSearchRequestSchema.safeParse({ query: 'berlin', provider: '' }).success).toBe(false);
|
|
});
|
|
|
|
it('allows an optional circle locationBias with numeric lat/lng and optional radius', () => {
|
|
expect(mapsSearchRequestSchema.safeParse({ query: 'berlin', locationBias: { lat: 52.5, lng: 13.4 } }).success).toBe(
|
|
true,
|
|
);
|
|
expect(
|
|
mapsSearchRequestSchema.safeParse({ query: 'berlin', locationBias: { lat: 52.5, lng: 13.4, radius: 50000 } })
|
|
.success,
|
|
).toBe(true);
|
|
// NaN arrives as null over JSON; either way a non-numeric lat must fail.
|
|
expect(mapsSearchRequestSchema.safeParse({ query: 'berlin', locationBias: { lat: null, lng: 13.4 } }).success).toBe(
|
|
false,
|
|
);
|
|
expect(mapsSearchRequestSchema.safeParse({ query: 'berlin', locationBias: { lat: 52.5 } }).success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('mapsAutocompleteRequestSchema', () => {
|
|
it('caps input at 200 chars and allows an optional locationBias', () => {
|
|
expect(mapsAutocompleteRequestSchema.safeParse({ input: 'be' }).success).toBe(true);
|
|
expect(mapsAutocompleteRequestSchema.safeParse({ input: 'x'.repeat(201) }).success).toBe(false);
|
|
expect(
|
|
mapsAutocompleteRequestSchema.safeParse({
|
|
input: 'be',
|
|
locationBias: { low: { lat: 1, lng: 2 }, high: { lat: 3, lng: 4 } },
|
|
}).success,
|
|
).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('mapsReverseQuerySchema', () => {
|
|
it('requires lat and lng as strings (the route parses them downstream)', () => {
|
|
expect(mapsReverseQuerySchema.safeParse({ lat: '52.5', lng: '13.4' }).success).toBe(true);
|
|
expect(mapsReverseQuerySchema.safeParse({ lat: '52.5' }).success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('mapsResolveUrlRequestSchema', () => {
|
|
it('requires a non-empty url', () => {
|
|
expect(
|
|
mapsResolveUrlRequestSchema.safeParse({
|
|
url: 'https://maps.app.goo.gl/x',
|
|
}).success,
|
|
).toBe(true);
|
|
expect(mapsResolveUrlRequestSchema.safeParse({ url: '' }).success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('mapsPlaceEnrichmentRequestSchema', () => {
|
|
const base = { lat: 52.5, lng: 13.4, name: 'Museum Ludwig' };
|
|
|
|
it('needs coordinates and a name, but not a place id', () => {
|
|
expect(mapsPlaceEnrichmentRequestSchema.safeParse(base).success).toBe(true);
|
|
expect(mapsPlaceEnrichmentRequestSchema.safeParse({ ...base, placeId: 'ChIJx' }).success).toBe(true);
|
|
expect(mapsPlaceEnrichmentRequestSchema.safeParse({ lat: 52.5, lng: 13.4 }).success).toBe(false);
|
|
expect(mapsPlaceEnrichmentRequestSchema.safeParse({ ...base, name: '' }).success).toBe(false);
|
|
});
|
|
|
|
it('rejects coordinates that arrived as strings or null', () => {
|
|
// The client reads these off a provider blob, where lat can be a string.
|
|
expect(mapsPlaceEnrichmentRequestSchema.safeParse({ ...base, lat: '52.5' }).success).toBe(false);
|
|
expect(mapsPlaceEnrichmentRequestSchema.safeParse({ ...base, lng: null }).success).toBe(false);
|
|
});
|
|
|
|
it('caps the free-text fields so a pasted blob cannot reach the providers', () => {
|
|
expect(mapsPlaceEnrichmentRequestSchema.safeParse({ ...base, name: 'x'.repeat(301) }).success).toBe(false);
|
|
expect(mapsPlaceEnrichmentRequestSchema.safeParse({ ...base, placeId: 'x'.repeat(301) }).success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('placePhotoCandidateSchema', () => {
|
|
const candidate = {
|
|
key: 'ChIJx~p0',
|
|
url: '/api/maps/place-photo/ChIJx~p0/bytes',
|
|
attribution: 'Jane Doe',
|
|
license: 'CC BY-SA 4.0',
|
|
licenseUrl: 'https://creativecommons.org/licenses/by-sa/4.0/',
|
|
sourceUrl: 'https://commons.wikimedia.org/wiki/File:X.jpg',
|
|
source: 'wikimedia',
|
|
};
|
|
|
|
it('accepts a fully attributed candidate', () => {
|
|
expect(placePhotoCandidateSchema.safeParse(candidate).success).toBe(true);
|
|
});
|
|
|
|
it('allows every licence field to be null, because Google supplies none of them', () => {
|
|
expect(
|
|
placePhotoCandidateSchema.safeParse({
|
|
...candidate,
|
|
source: 'google',
|
|
attribution: null,
|
|
license: null,
|
|
licenseUrl: null,
|
|
sourceUrl: null,
|
|
}).success,
|
|
).toBe(true);
|
|
});
|
|
|
|
it('requires the fields to be present even when null, so the column cannot silently skip attribution', () => {
|
|
const { attribution, ...withoutAttribution } = candidate;
|
|
void attribution;
|
|
expect(placePhotoCandidateSchema.safeParse(withoutAttribution).success).toBe(false);
|
|
});
|
|
|
|
it('rejects an unknown source', () => {
|
|
expect(placePhotoCandidateSchema.safeParse({ ...candidate, source: 'unsplash' }).success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('placeDescriptionSchema', () => {
|
|
it('keeps the source alongside the text', () => {
|
|
expect(
|
|
placeDescriptionSchema.safeParse({
|
|
text: 'A museum in Cologne.',
|
|
source: 'wikipedia',
|
|
sourceUrl: 'https://de.wikipedia.org/wiki/Museum_Ludwig',
|
|
license: 'CC BY-SA 4.0',
|
|
}).success,
|
|
).toBe(true);
|
|
expect(placeDescriptionSchema.safeParse({ text: 'A museum.' }).success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('mapsPlaceEnrichmentResultSchema', () => {
|
|
it('accepts an empty result, which is what a keyless instance with no Commons hit returns', () => {
|
|
expect(mapsPlaceEnrichmentResultSchema.safeParse({ photos: [], description: null, facts: [] }).success).toBe(true);
|
|
});
|
|
|
|
it('treats hours and rating as additive, so an older payload still parses', () => {
|
|
// They arrived after the first release of this endpoint; a cached result
|
|
// written before that has neither and must stay valid.
|
|
expect(mapsPlaceEnrichmentResultSchema.safeParse({ photos: [], description: null, facts: [] }).success).toBe(true);
|
|
expect(
|
|
mapsPlaceEnrichmentResultSchema.safeParse({
|
|
photos: [],
|
|
description: null,
|
|
facts: [],
|
|
hours: { weekdayDescriptions: ['Monday: 09:00-18:00'], periods: null, specialDays: null },
|
|
rating: { value: 4.5, count: 1234 },
|
|
}).success,
|
|
).toBe(true);
|
|
// A place that never closes has a period with no close time.
|
|
expect(
|
|
mapsPlaceEnrichmentResultSchema.safeParse({
|
|
photos: [],
|
|
description: null,
|
|
facts: [],
|
|
hours: {
|
|
weekdayDescriptions: ['Monday: 00:00-24:00'],
|
|
periods: [{ open: { day: 0, hour: 0, minute: 0 }, close: null }],
|
|
},
|
|
}).success,
|
|
).toBe(true);
|
|
// Google's search results carry a rating but no count.
|
|
expect(
|
|
mapsPlaceEnrichmentResultSchema.safeParse({
|
|
photos: [],
|
|
description: null,
|
|
facts: [],
|
|
rating: { value: 4, count: null },
|
|
}).success,
|
|
).toBe(true);
|
|
});
|
|
|
|
it('accepts the kill-switch response', () => {
|
|
expect(
|
|
mapsPlaceEnrichmentResultSchema.safeParse({ photos: [], description: null, facts: [], disabled: true }).success,
|
|
).toBe(true);
|
|
});
|
|
|
|
it('requires description to be explicitly null rather than absent', () => {
|
|
expect(mapsPlaceEnrichmentResultSchema.safeParse({ photos: [] }).success).toBe(false);
|
|
});
|
|
});
|