Files
MyIP/tests/ip-timezone.test.js
T
jason5ng32andClaude Opus 5.5 c29e48687b Refactor(layout): split backend-only code out of common/, group frontend utils
common/ had become the backend's library: 30 of its 41 modules were
imported by nothing in the frontend, so the name no longer said what
belonged there. It now holds only what the frontend or the build imports
too; backend-only code moved to server/, flat except for datasets/ (the
dataset engine and its readers). The *-db/ dataset directories stay in
common/: self-hosting docs and Docker volumes mount those paths.

frontend/utils/ keeps its app-wide modules at the root and gains ip/
(IP / ASN shaping and validation), report/ (the report pipeline) and
features/ (the pure layer of a single feature). Paths only — no logic
changes. The Dockerfile copies server/, and the AGENTS.md files say where
a new module goes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 13:10:02 +08:00

126 lines
5.1 KiB
JavaScript

// Contract tests for server/ip-timezone — the coordinate → IANA zone
// derivation behind the withTimeZone() middleware. Distinct from
// frontend/utils/time-utils.js, which reads the visitor's own browser timezone.
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import { lookupTimeZone, attachTimeZone, withTimeZone } from '../server/ip-timezone.js';
// Minimal res stub — enough to observe what the res.json hook does, and to
// prove the original json() still runs and still returns res for chaining.
function makeRes(statusCode = 200) {
return {
statusCode,
body: null,
json(payload) { this.body = payload; return this; },
};
}
describe('lookupTimeZone — resolving coordinates', () => {
it('resolves land coordinates to their IANA zone', () => {
assert.equal(lookupTimeZone(1.3521, 103.8198), 'Asia/Singapore');
assert.equal(lookupTimeZone(-33.8688, 151.2093), 'Australia/Sydney');
assert.equal(lookupTimeZone(37.7749, -122.4194), 'America/Los_Angeles');
});
it('accepts numeric strings, which some sources return instead of numbers', () => {
assert.equal(lookupTimeZone('1.3521', '103.8198'), 'Asia/Singapore');
});
});
describe('lookupTimeZone — coordinates that carry no timezone', () => {
it("returns '' for the 'N/A' placeholder sources use for a missing location", () => {
assert.equal(lookupTimeZone('N/A', 'N/A'), '');
});
it("returns '' rather than coercing null/undefined/empty to the 0,0 null island", () => {
assert.equal(lookupTimeZone(null, null), '');
assert.equal(lookupTimeZone(undefined, undefined), '');
assert.equal(lookupTimeZone('', ''), '');
// The dangerous half-missing case: a real longitude beside a null
// latitude would otherwise be read as a point on the equator.
assert.equal(lookupTimeZone(null, 103.8198), '');
assert.equal(lookupTimeZone(1.3521, null), '');
});
it("returns '' for open water, where the lookup answers with an Etc/* zone", () => {
assert.equal(lookupTimeZone(0, 0), '');
});
it("returns '' for out-of-range coordinates instead of throwing", () => {
assert.equal(lookupTimeZone(91, 0), '');
assert.equal(lookupTimeZone(0, 181), '');
assert.equal(lookupTimeZone(NaN, NaN), '');
});
});
describe('attachTimeZone — enriching a geo payload', () => {
it('adds the zone derived from the payload own coordinates', () => {
const payload = { ip: '1.1.1.1', city: 'Singapore', latitude: 1.3521, longitude: 103.8198 };
attachTimeZone(payload);
assert.equal(payload.timezone, 'Asia/Singapore');
});
it("always defines the field, so the frontend has one falsy check", () => {
const payload = { ip: '1.1.1.1', city: 'N/A', latitude: 'N/A', longitude: 'N/A' };
attachTimeZone(payload);
assert.equal(payload.timezone, '');
});
it('recomputes rather than trusting a timezone the upstream already sent', () => {
const payload = { latitude: 1.3521, longitude: 103.8198, timezone: 'America/New_York' };
attachTimeZone(payload);
assert.equal(payload.timezone, 'Asia/Singapore');
});
it('mutates in place and returns the same object', () => {
const payload = { latitude: 35.6895, longitude: 139.6917 };
assert.equal(attachTimeZone(payload), payload);
});
it('leaves non-object bodies untouched', () => {
assert.equal(attachTimeZone(null), null);
assert.equal(attachTimeZone('a string body'), 'a string body');
const list = [{ latitude: 1.3521, longitude: 103.8198 }];
attachTimeZone(list);
assert.equal(list[0].timezone, undefined);
});
it('does not invent coordinates for a payload that has none', () => {
const payload = { error: 'Upstream responded 502' };
attachTimeZone(payload);
assert.equal(payload.timezone, '');
});
});
describe('withTimeZone — route middleware', () => {
it('calls next() so the handler still runs', () => {
let nextCalled = false;
withTimeZone()({}, makeRes(), () => { nextCalled = true; });
assert.equal(nextCalled, true);
});
it('enriches a 2xx body on its way out', () => {
const res = makeRes(200);
withTimeZone()({}, res, () => { });
res.json({ city: 'Tokyo', latitude: 35.6895, longitude: 139.6917 });
assert.equal(res.body.timezone, 'Asia/Tokyo');
});
it('leaves error bodies alone, matching cacheable()s 2xx-only rule', () => {
for (const code of [400, 500]) {
const res = makeRes(code);
withTimeZone()({}, res, () => { });
res.json({ error: 'nope', latitude: 35.6895, longitude: 139.6917 });
assert.equal(res.body.timezone, undefined);
}
});
it('preserves the original json() return value for chaining', () => {
const res = makeRes(200);
withTimeZone()({}, res, () => { });
assert.equal(res.json({ latitude: 1.3521, longitude: 103.8198 }), res);
});
});