@xenterprises/fastify-xgeocode 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +144 -0
- package/QUICK_START.md +126 -0
- package/README.md +126 -0
- package/package.json +40 -0
- package/src/xGeocode.js +363 -0
- package/test/xGeocode.test.js +901 -0
|
@@ -0,0 +1,901 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import Fastify from "fastify";
|
|
4
|
+
import xGeocode from "../src/xGeocode.js";
|
|
5
|
+
|
|
6
|
+
test("xGeocode Plugin - registers successfully with apiKey", async () => {
|
|
7
|
+
const fastify = Fastify({ logger: false });
|
|
8
|
+
try {
|
|
9
|
+
await fastify.register(xGeocode, {
|
|
10
|
+
apiKey: "test-api-key"
|
|
11
|
+
});
|
|
12
|
+
assert.ok(true, "Plugin registered successfully");
|
|
13
|
+
} finally {
|
|
14
|
+
await fastify.close();
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("xGeocode Plugin - throws error when apiKey is missing", async () => {
|
|
19
|
+
const fastify = Fastify({ logger: false });
|
|
20
|
+
try {
|
|
21
|
+
await assert.rejects(
|
|
22
|
+
async () => await fastify.register(xGeocode, {}),
|
|
23
|
+
{ message: /apiKey is required/ }
|
|
24
|
+
);
|
|
25
|
+
} finally {
|
|
26
|
+
await fastify.close();
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("xGeocode Plugin - can be disabled with active: false", async () => {
|
|
31
|
+
const fastify = Fastify({ logger: false });
|
|
32
|
+
try {
|
|
33
|
+
await fastify.register(xGeocode, {
|
|
34
|
+
active: false,
|
|
35
|
+
apiKey: "test-api-key"
|
|
36
|
+
});
|
|
37
|
+
assert.ok(!fastify.geocode, "Geocode decorator not added when disabled");
|
|
38
|
+
} finally {
|
|
39
|
+
await fastify.close();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("xGeocode Plugin - provides xGeocode decorator", async () => {
|
|
44
|
+
const fastify = Fastify({ logger: false });
|
|
45
|
+
try {
|
|
46
|
+
await fastify.register(xGeocode, {
|
|
47
|
+
apiKey: "test-api-key"
|
|
48
|
+
});
|
|
49
|
+
assert.ok(fastify.xGeocode, "xGeocode decorator is available");
|
|
50
|
+
assert.ok(typeof fastify.xGeocode.getLatLongByZip === "function", "getLatLongByZip method exists");
|
|
51
|
+
} finally {
|
|
52
|
+
await fastify.close();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("xGeocode Validation - rejects invalid zip code (too short)", async () => {
|
|
57
|
+
const fastify = Fastify({ logger: false });
|
|
58
|
+
try {
|
|
59
|
+
await fastify.register(xGeocode, {
|
|
60
|
+
apiKey: "test-api-key"
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
await assert.rejects(
|
|
64
|
+
() => fastify.xGeocode.getLatLongByZip("123"),
|
|
65
|
+
{ message: /Invalid zip code format/ }
|
|
66
|
+
);
|
|
67
|
+
} finally {
|
|
68
|
+
await fastify.close();
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("xGeocode Validation - rejects invalid zip code (letters)", async () => {
|
|
73
|
+
const fastify = Fastify({ logger: false });
|
|
74
|
+
try {
|
|
75
|
+
await fastify.register(xGeocode, {
|
|
76
|
+
apiKey: "test-api-key"
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
await assert.rejects(
|
|
80
|
+
() => fastify.xGeocode.getLatLongByZip("1000A"),
|
|
81
|
+
{ message: /Invalid zip code format/ }
|
|
82
|
+
);
|
|
83
|
+
} finally {
|
|
84
|
+
await fastify.close();
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("xGeocode Validation - accepts 5-digit zip code format", async () => {
|
|
89
|
+
const fastify = Fastify({ logger: false });
|
|
90
|
+
try {
|
|
91
|
+
await fastify.register(xGeocode, {
|
|
92
|
+
apiKey: "test-api-key"
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Mock fetch to avoid actual API call
|
|
96
|
+
const originalFetch = global.fetch;
|
|
97
|
+
global.fetch = async () => ({
|
|
98
|
+
ok: true,
|
|
99
|
+
json: async () => ({
|
|
100
|
+
results: [{
|
|
101
|
+
location: { lat: 40.7506, lng: -73.9972 },
|
|
102
|
+
formatted_address: "New York, NY 10001",
|
|
103
|
+
address_components: {
|
|
104
|
+
city: "New York",
|
|
105
|
+
county: "New York County",
|
|
106
|
+
state: "NY",
|
|
107
|
+
country: "US",
|
|
108
|
+
zip: "10001"
|
|
109
|
+
}
|
|
110
|
+
}]
|
|
111
|
+
})
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const result = await fastify.xGeocode.getLatLongByZip("10001");
|
|
116
|
+
assert.ok(result, "Accepts valid 5-digit zip code");
|
|
117
|
+
assert.equal(result.zip, "10001", "Returns original zip code");
|
|
118
|
+
assert.equal(result.lat, 40.7506, "Returns latitude");
|
|
119
|
+
assert.equal(result.lng, -73.9972, "Returns longitude");
|
|
120
|
+
} finally {
|
|
121
|
+
global.fetch = originalFetch;
|
|
122
|
+
}
|
|
123
|
+
} finally {
|
|
124
|
+
await fastify.close();
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("xGeocode Validation - accepts 9-digit zip code (ZIP+4) format", async () => {
|
|
129
|
+
const fastify = Fastify({ logger: false });
|
|
130
|
+
try {
|
|
131
|
+
await fastify.register(xGeocode, {
|
|
132
|
+
apiKey: "test-api-key"
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Mock fetch
|
|
136
|
+
const originalFetch = global.fetch;
|
|
137
|
+
global.fetch = async () => ({
|
|
138
|
+
ok: true,
|
|
139
|
+
json: async () => ({
|
|
140
|
+
results: [{
|
|
141
|
+
location: { lat: 40.7506, lng: -73.9972 },
|
|
142
|
+
formatted_address: "New York, NY 10001-1234",
|
|
143
|
+
address_components: {
|
|
144
|
+
city: "New York",
|
|
145
|
+
county: "New York County",
|
|
146
|
+
state: "NY",
|
|
147
|
+
country: "US",
|
|
148
|
+
zip: "10001-1234"
|
|
149
|
+
}
|
|
150
|
+
}]
|
|
151
|
+
})
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const result = await fastify.xGeocode.getLatLongByZip("10001-1234");
|
|
156
|
+
assert.ok(result, "Accepts valid 9-digit zip code");
|
|
157
|
+
assert.equal(result.zip, "10001-1234", "Returns original zip code format");
|
|
158
|
+
assert.equal(result.lat, 40.7506, "Returns latitude");
|
|
159
|
+
assert.equal(result.lng, -73.9972, "Returns longitude");
|
|
160
|
+
} finally {
|
|
161
|
+
global.fetch = originalFetch;
|
|
162
|
+
}
|
|
163
|
+
} finally {
|
|
164
|
+
await fastify.close();
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("xGeocode Validation - rejects null or undefined zip code", async () => {
|
|
169
|
+
const fastify = Fastify({ logger: false });
|
|
170
|
+
try {
|
|
171
|
+
await fastify.register(xGeocode, {
|
|
172
|
+
apiKey: "test-api-key"
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
await assert.rejects(
|
|
176
|
+
() => fastify.xGeocode.getLatLongByZip(null),
|
|
177
|
+
{ message: /Invalid input/ }
|
|
178
|
+
);
|
|
179
|
+
} finally {
|
|
180
|
+
await fastify.close();
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("xGeocode Error Handling - handles API errors gracefully", async () => {
|
|
185
|
+
const fastify = Fastify({ logger: false });
|
|
186
|
+
try {
|
|
187
|
+
await fastify.register(xGeocode, {
|
|
188
|
+
apiKey: "test-api-key"
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Mock fetch to return error status
|
|
192
|
+
const originalFetch = global.fetch;
|
|
193
|
+
global.fetch = async () => ({
|
|
194
|
+
ok: false,
|
|
195
|
+
status: 401
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
await assert.rejects(
|
|
200
|
+
() => fastify.xGeocode.getLatLongByZip("10001"),
|
|
201
|
+
{ message: /Geocoding/ }
|
|
202
|
+
);
|
|
203
|
+
} finally {
|
|
204
|
+
global.fetch = originalFetch;
|
|
205
|
+
}
|
|
206
|
+
} finally {
|
|
207
|
+
await fastify.close();
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("xGeocode Error Handling - handles empty results", async () => {
|
|
212
|
+
const fastify = Fastify({ logger: false });
|
|
213
|
+
try {
|
|
214
|
+
await fastify.register(xGeocode, {
|
|
215
|
+
apiKey: "test-api-key"
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Mock fetch to return empty results
|
|
219
|
+
const originalFetch = global.fetch;
|
|
220
|
+
global.fetch = async () => ({
|
|
221
|
+
ok: true,
|
|
222
|
+
json: async () => ({ results: [] })
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
await assert.rejects(
|
|
227
|
+
() => fastify.xGeocode.getLatLongByZip("99999"),
|
|
228
|
+
{ message: /No results found/ }
|
|
229
|
+
);
|
|
230
|
+
} finally {
|
|
231
|
+
global.fetch = originalFetch;
|
|
232
|
+
}
|
|
233
|
+
} finally {
|
|
234
|
+
await fastify.close();
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("xGeocode Response - returns complete location object", async () => {
|
|
239
|
+
const fastify = Fastify({ logger: false });
|
|
240
|
+
try {
|
|
241
|
+
await fastify.register(xGeocode, {
|
|
242
|
+
apiKey: "test-api-key"
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Mock fetch
|
|
246
|
+
const originalFetch = global.fetch;
|
|
247
|
+
global.fetch = async () => ({
|
|
248
|
+
ok: true,
|
|
249
|
+
json: async () => ({
|
|
250
|
+
results: [{
|
|
251
|
+
location: { lat: 40.7506, lng: -73.9972 },
|
|
252
|
+
formatted_address: "New York, NY 10001",
|
|
253
|
+
address_components: {
|
|
254
|
+
city: "New York",
|
|
255
|
+
county: "New York County",
|
|
256
|
+
state: "NY",
|
|
257
|
+
country: "US",
|
|
258
|
+
zip: "10001"
|
|
259
|
+
}
|
|
260
|
+
}]
|
|
261
|
+
})
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
const result = await fastify.xGeocode.getLatLongByZip("10001");
|
|
266
|
+
|
|
267
|
+
assert.equal(result.zip, "10001", "Contains zip");
|
|
268
|
+
assert.equal(result.lat, 40.7506, "Contains latitude");
|
|
269
|
+
assert.equal(result.lng, -73.9972, "Contains longitude");
|
|
270
|
+
assert.equal(result.city, "New York", "Contains city");
|
|
271
|
+
assert.equal(result.county, "New York County", "Contains county");
|
|
272
|
+
assert.equal(result.state, "NY", "Contains state");
|
|
273
|
+
assert.equal(result.country, "US", "Contains country");
|
|
274
|
+
assert.ok(result.addressComponents, "Contains full address components");
|
|
275
|
+
} finally {
|
|
276
|
+
global.fetch = originalFetch;
|
|
277
|
+
}
|
|
278
|
+
} finally {
|
|
279
|
+
await fastify.close();
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("xGeocode Response - handles null address components gracefully", async () => {
|
|
284
|
+
const fastify = Fastify({ logger: false });
|
|
285
|
+
try {
|
|
286
|
+
await fastify.register(xGeocode, {
|
|
287
|
+
apiKey: "test-api-key"
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// Mock fetch with missing address components
|
|
291
|
+
const originalFetch = global.fetch;
|
|
292
|
+
global.fetch = async () => ({
|
|
293
|
+
ok: true,
|
|
294
|
+
json: async () => ({
|
|
295
|
+
results: [{
|
|
296
|
+
location: { lat: 40.7506, lng: -73.9972 },
|
|
297
|
+
address_components: {
|
|
298
|
+
city: undefined,
|
|
299
|
+
county: undefined,
|
|
300
|
+
state: "NY",
|
|
301
|
+
country: "US"
|
|
302
|
+
}
|
|
303
|
+
}]
|
|
304
|
+
})
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
const result = await fastify.xGeocode.getLatLongByZip("10001");
|
|
309
|
+
|
|
310
|
+
assert.equal(result.city, null, "Missing city becomes null");
|
|
311
|
+
assert.equal(result.county, null, "Missing county becomes null");
|
|
312
|
+
assert.equal(result.state, "NY", "Present values preserved");
|
|
313
|
+
} finally {
|
|
314
|
+
global.fetch = originalFetch;
|
|
315
|
+
}
|
|
316
|
+
} finally {
|
|
317
|
+
await fastify.close();
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("xGeocode Trim - trims whitespace from zip codes", async () => {
|
|
322
|
+
const fastify = Fastify({ logger: false });
|
|
323
|
+
try {
|
|
324
|
+
await fastify.register(xGeocode, {
|
|
325
|
+
apiKey: "test-api-key"
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// Mock fetch
|
|
329
|
+
const originalFetch = global.fetch;
|
|
330
|
+
global.fetch = async () => ({
|
|
331
|
+
ok: true,
|
|
332
|
+
json: async () => ({
|
|
333
|
+
results: [{
|
|
334
|
+
location: { lat: 40.7506, lng: -73.9972 },
|
|
335
|
+
formatted_address: "New York, NY 10001",
|
|
336
|
+
address_components: {
|
|
337
|
+
city: "New York",
|
|
338
|
+
county: "New York County",
|
|
339
|
+
state: "NY",
|
|
340
|
+
country: "US",
|
|
341
|
+
zip: "10001"
|
|
342
|
+
}
|
|
343
|
+
}]
|
|
344
|
+
})
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
const result = await fastify.xGeocode.getLatLongByZip(" 10001 ");
|
|
349
|
+
assert.equal(result.zip, "10001", "Whitespace trimmed from result");
|
|
350
|
+
} finally {
|
|
351
|
+
global.fetch = originalFetch;
|
|
352
|
+
}
|
|
353
|
+
} finally {
|
|
354
|
+
await fastify.close();
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// getLatLongByAddress() Tests
|
|
359
|
+
test("xGeocode getLatLongByAddress - accepts valid address", async () => {
|
|
360
|
+
const fastify = Fastify({ logger: false });
|
|
361
|
+
try {
|
|
362
|
+
await fastify.register(xGeocode, {
|
|
363
|
+
apiKey: "test-api-key"
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// Mock fetch
|
|
367
|
+
const originalFetch = global.fetch;
|
|
368
|
+
global.fetch = async () => ({
|
|
369
|
+
ok: true,
|
|
370
|
+
json: async () => ({
|
|
371
|
+
results: [{
|
|
372
|
+
location: { lat: 40.7128, lng: -74.0060 },
|
|
373
|
+
formatted_address: "123 Main St, New York, NY 10001",
|
|
374
|
+
address_components: {
|
|
375
|
+
city: "New York",
|
|
376
|
+
county: "New York County",
|
|
377
|
+
state: "NY",
|
|
378
|
+
country: "US",
|
|
379
|
+
zip: "10001"
|
|
380
|
+
}
|
|
381
|
+
}]
|
|
382
|
+
})
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
const result = await fastify.xGeocode.getLatLongByAddress("123 Main St, New York, NY");
|
|
387
|
+
assert.ok(result, "Returns result for valid address");
|
|
388
|
+
assert.equal(result.lat, 40.7128, "Contains latitude");
|
|
389
|
+
assert.equal(result.lng, -74.0060, "Contains longitude");
|
|
390
|
+
assert.equal(result.city, "New York", "Contains city");
|
|
391
|
+
} finally {
|
|
392
|
+
global.fetch = originalFetch;
|
|
393
|
+
}
|
|
394
|
+
} finally {
|
|
395
|
+
await fastify.close();
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test("xGeocode getLatLongByAddress - rejects address with less than 3 characters", async () => {
|
|
400
|
+
const fastify = Fastify({ logger: false });
|
|
401
|
+
try {
|
|
402
|
+
await fastify.register(xGeocode, {
|
|
403
|
+
apiKey: "test-api-key"
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
await assert.rejects(
|
|
407
|
+
() => fastify.xGeocode.getLatLongByAddress("12"),
|
|
408
|
+
{ message: /minimum 3 characters/ }
|
|
409
|
+
);
|
|
410
|
+
} finally {
|
|
411
|
+
await fastify.close();
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test("xGeocode getLatLongByAddress - rejects null address", async () => {
|
|
416
|
+
const fastify = Fastify({ logger: false });
|
|
417
|
+
try {
|
|
418
|
+
await fastify.register(xGeocode, {
|
|
419
|
+
apiKey: "test-api-key"
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
await assert.rejects(
|
|
423
|
+
() => fastify.xGeocode.getLatLongByAddress(null),
|
|
424
|
+
{ message: /Invalid input/ }
|
|
425
|
+
);
|
|
426
|
+
} finally {
|
|
427
|
+
await fastify.close();
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
test("xGeocode getLatLongByAddress - trims whitespace from address", async () => {
|
|
432
|
+
const fastify = Fastify({ logger: false });
|
|
433
|
+
try {
|
|
434
|
+
await fastify.register(xGeocode, {
|
|
435
|
+
apiKey: "test-api-key"
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// Mock fetch
|
|
439
|
+
const originalFetch = global.fetch;
|
|
440
|
+
global.fetch = async () => ({
|
|
441
|
+
ok: true,
|
|
442
|
+
json: async () => ({
|
|
443
|
+
results: [{
|
|
444
|
+
location: { lat: 40.7128, lng: -74.0060 },
|
|
445
|
+
formatted_address: "123 Main St, New York, NY 10001",
|
|
446
|
+
address_components: {
|
|
447
|
+
city: "New York",
|
|
448
|
+
county: "New York County",
|
|
449
|
+
state: "NY",
|
|
450
|
+
country: "US",
|
|
451
|
+
zip: "10001"
|
|
452
|
+
}
|
|
453
|
+
}]
|
|
454
|
+
})
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
try {
|
|
458
|
+
const result = await fastify.xGeocode.getLatLongByAddress(" 123 Main St ");
|
|
459
|
+
assert.ok(result, "Successfully processes address with whitespace");
|
|
460
|
+
} finally {
|
|
461
|
+
global.fetch = originalFetch;
|
|
462
|
+
}
|
|
463
|
+
} finally {
|
|
464
|
+
await fastify.close();
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
// getReverseGeocode() Tests
|
|
469
|
+
test("xGeocode getReverseGeocode - accepts valid coordinates", async () => {
|
|
470
|
+
const fastify = Fastify({ logger: false });
|
|
471
|
+
try {
|
|
472
|
+
await fastify.register(xGeocode, {
|
|
473
|
+
apiKey: "test-api-key"
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
// Mock fetch
|
|
477
|
+
const originalFetch = global.fetch;
|
|
478
|
+
global.fetch = async () => ({
|
|
479
|
+
ok: true,
|
|
480
|
+
json: async () => ({
|
|
481
|
+
results: [{
|
|
482
|
+
location: { lat: 40.7128, lng: -74.0060 },
|
|
483
|
+
formatted_address: "123 Main St, New York, NY 10001",
|
|
484
|
+
address_components: {
|
|
485
|
+
city: "New York",
|
|
486
|
+
county: "New York County",
|
|
487
|
+
state: "NY",
|
|
488
|
+
country: "US",
|
|
489
|
+
zip: "10001"
|
|
490
|
+
}
|
|
491
|
+
}]
|
|
492
|
+
})
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
try {
|
|
496
|
+
const result = await fastify.xGeocode.getReverseGeocode(40.7128, -74.0060);
|
|
497
|
+
assert.ok(result, "Returns result for valid coordinates");
|
|
498
|
+
assert.equal(result.city, "New York", "Contains city");
|
|
499
|
+
assert.equal(result.state, "NY", "Contains state");
|
|
500
|
+
} finally {
|
|
501
|
+
global.fetch = originalFetch;
|
|
502
|
+
}
|
|
503
|
+
} finally {
|
|
504
|
+
await fastify.close();
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
test("xGeocode getReverseGeocode - rejects invalid latitude", async () => {
|
|
509
|
+
const fastify = Fastify({ logger: false });
|
|
510
|
+
try {
|
|
511
|
+
await fastify.register(xGeocode, {
|
|
512
|
+
apiKey: "test-api-key"
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
await assert.rejects(
|
|
516
|
+
() => fastify.xGeocode.getReverseGeocode(95, -74.0060),
|
|
517
|
+
{ message: /Invalid latitude/ }
|
|
518
|
+
);
|
|
519
|
+
} finally {
|
|
520
|
+
await fastify.close();
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
test("xGeocode getReverseGeocode - rejects invalid longitude", async () => {
|
|
525
|
+
const fastify = Fastify({ logger: false });
|
|
526
|
+
try {
|
|
527
|
+
await fastify.register(xGeocode, {
|
|
528
|
+
apiKey: "test-api-key"
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
await assert.rejects(
|
|
532
|
+
() => fastify.xGeocode.getReverseGeocode(40.7128, -185),
|
|
533
|
+
{ message: /Invalid longitude/ }
|
|
534
|
+
);
|
|
535
|
+
} finally {
|
|
536
|
+
await fastify.close();
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
test("xGeocode getReverseGeocode - rejects non-numeric coordinates", async () => {
|
|
541
|
+
const fastify = Fastify({ logger: false });
|
|
542
|
+
try {
|
|
543
|
+
await fastify.register(xGeocode, {
|
|
544
|
+
apiKey: "test-api-key"
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
await assert.rejects(
|
|
548
|
+
() => fastify.xGeocode.getReverseGeocode("invalid", "coordinates"),
|
|
549
|
+
{ message: /must be numbers/ }
|
|
550
|
+
);
|
|
551
|
+
} finally {
|
|
552
|
+
await fastify.close();
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
// getDistance() Tests
|
|
557
|
+
test("xGeocode getDistance - calculates distance between two points", async () => {
|
|
558
|
+
const fastify = Fastify({ logger: false });
|
|
559
|
+
try {
|
|
560
|
+
await fastify.register(xGeocode, {
|
|
561
|
+
apiKey: "test-api-key"
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
const result = fastify.xGeocode.getDistance(40.7128, -74.0060, 40.7614, -73.9776);
|
|
565
|
+
assert.ok(result, "Returns distance result");
|
|
566
|
+
assert.ok(result.kilometers, "Contains kilometers");
|
|
567
|
+
assert.ok(result.miles, "Contains miles");
|
|
568
|
+
assert.ok(result.meters, "Contains meters");
|
|
569
|
+
assert.ok(result.kilometers > 0, "Distance is greater than zero");
|
|
570
|
+
} finally {
|
|
571
|
+
await fastify.close();
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
test("xGeocode getDistance - validates coordinate ranges", async () => {
|
|
576
|
+
const fastify = Fastify({ logger: false });
|
|
577
|
+
try {
|
|
578
|
+
await fastify.register(xGeocode, {
|
|
579
|
+
apiKey: "test-api-key"
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
assert.throws(
|
|
583
|
+
() => fastify.xGeocode.getDistance(100, -74.0060, 40.7614, -73.9776),
|
|
584
|
+
{ message: /Invalid latitude/ }
|
|
585
|
+
);
|
|
586
|
+
} finally {
|
|
587
|
+
await fastify.close();
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
test("xGeocode getDistance - handles string coordinates as numbers", async () => {
|
|
592
|
+
const fastify = Fastify({ logger: false });
|
|
593
|
+
try {
|
|
594
|
+
await fastify.register(xGeocode, {
|
|
595
|
+
apiKey: "test-api-key"
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
const result = fastify.xGeocode.getDistance("40.7128", "-74.0060", "40.7614", "-73.9776");
|
|
599
|
+
assert.ok(result, "Accepts string coordinates that parse as numbers");
|
|
600
|
+
assert.ok(result.kilometers > 0, "Calculates valid distance");
|
|
601
|
+
} finally {
|
|
602
|
+
await fastify.close();
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
test("xGeocode getDistance - rejects non-numeric coordinates", async () => {
|
|
607
|
+
const fastify = Fastify({ logger: false });
|
|
608
|
+
try {
|
|
609
|
+
await fastify.register(xGeocode, {
|
|
610
|
+
apiKey: "test-api-key"
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
assert.throws(
|
|
614
|
+
() => fastify.xGeocode.getDistance("invalid", "-74.0060", "40.7614", "-73.9776"),
|
|
615
|
+
{ message: /must be numbers/ }
|
|
616
|
+
);
|
|
617
|
+
} finally {
|
|
618
|
+
await fastify.close();
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
// batchGeocode() Tests
|
|
623
|
+
test("xGeocode batchGeocode - processes array of locations", async () => {
|
|
624
|
+
const fastify = Fastify({ logger: false });
|
|
625
|
+
try {
|
|
626
|
+
await fastify.register(xGeocode, {
|
|
627
|
+
apiKey: "test-api-key"
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
// Mock fetch
|
|
631
|
+
const originalFetch = global.fetch;
|
|
632
|
+
global.fetch = async (url) => {
|
|
633
|
+
if (url.includes("10001")) {
|
|
634
|
+
return {
|
|
635
|
+
ok: true,
|
|
636
|
+
json: async () => ({
|
|
637
|
+
results: [{
|
|
638
|
+
location: { lat: 40.7506, lng: -73.9972 },
|
|
639
|
+
address_components: {
|
|
640
|
+
city: "New York",
|
|
641
|
+
county: "New York County",
|
|
642
|
+
state: "NY",
|
|
643
|
+
country: "US"
|
|
644
|
+
}
|
|
645
|
+
}]
|
|
646
|
+
})
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
return {
|
|
650
|
+
ok: true,
|
|
651
|
+
json: async () => ({
|
|
652
|
+
results: [{
|
|
653
|
+
location: { lat: 40.7128, lng: -74.0060 },
|
|
654
|
+
formatted_address: "123 Main St, New York, NY",
|
|
655
|
+
address_components: {
|
|
656
|
+
city: "New York",
|
|
657
|
+
county: "New York County",
|
|
658
|
+
state: "NY",
|
|
659
|
+
country: "US"
|
|
660
|
+
}
|
|
661
|
+
}]
|
|
662
|
+
})
|
|
663
|
+
};
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
try {
|
|
667
|
+
const result = await fastify.xGeocode.batchGeocode(["10001", "123 Main St"]);
|
|
668
|
+
assert.ok(Array.isArray(result), "Returns array");
|
|
669
|
+
assert.equal(result.length, 2, "Returns correct number of results");
|
|
670
|
+
assert.ok(result[0].lat, "First result has coordinates");
|
|
671
|
+
} finally {
|
|
672
|
+
global.fetch = originalFetch;
|
|
673
|
+
}
|
|
674
|
+
} finally {
|
|
675
|
+
await fastify.close();
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
test("xGeocode batchGeocode - rejects non-array input", async () => {
|
|
680
|
+
const fastify = Fastify({ logger: false });
|
|
681
|
+
try {
|
|
682
|
+
await fastify.register(xGeocode, {
|
|
683
|
+
apiKey: "test-api-key"
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
await assert.rejects(
|
|
687
|
+
() => fastify.xGeocode.batchGeocode("not an array"),
|
|
688
|
+
{ message: /locations must be an array/ }
|
|
689
|
+
);
|
|
690
|
+
} finally {
|
|
691
|
+
await fastify.close();
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
test("xGeocode batchGeocode - rejects empty array", async () => {
|
|
696
|
+
const fastify = Fastify({ logger: false });
|
|
697
|
+
try {
|
|
698
|
+
await fastify.register(xGeocode, {
|
|
699
|
+
apiKey: "test-api-key"
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
await assert.rejects(
|
|
703
|
+
() => fastify.xGeocode.batchGeocode([]),
|
|
704
|
+
{ message: /cannot be empty/ }
|
|
705
|
+
);
|
|
706
|
+
} finally {
|
|
707
|
+
await fastify.close();
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
test("xGeocode batchGeocode - rejects batch exceeding 100 locations", async () => {
|
|
712
|
+
const fastify = Fastify({ logger: false });
|
|
713
|
+
try {
|
|
714
|
+
await fastify.register(xGeocode, {
|
|
715
|
+
apiKey: "test-api-key"
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
const locations = Array(101).fill("10001");
|
|
719
|
+
await assert.rejects(
|
|
720
|
+
() => fastify.xGeocode.batchGeocode(locations),
|
|
721
|
+
{ message: /cannot exceed 100/ }
|
|
722
|
+
);
|
|
723
|
+
} finally {
|
|
724
|
+
await fastify.close();
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
test("xGeocode batchGeocode - handles per-item errors gracefully", async () => {
|
|
729
|
+
const fastify = Fastify({ logger: false });
|
|
730
|
+
try {
|
|
731
|
+
await fastify.register(xGeocode, {
|
|
732
|
+
apiKey: "test-api-key"
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
// Mock fetch to fail for specific items
|
|
736
|
+
const originalFetch = global.fetch;
|
|
737
|
+
global.fetch = async (url) => {
|
|
738
|
+
if (url.includes("invalid")) {
|
|
739
|
+
return {
|
|
740
|
+
ok: true,
|
|
741
|
+
json: async () => ({ results: [] })
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
return {
|
|
745
|
+
ok: true,
|
|
746
|
+
json: async () => ({
|
|
747
|
+
results: [{
|
|
748
|
+
location: { lat: 40.7506, lng: -73.9972 },
|
|
749
|
+
address_components: {
|
|
750
|
+
city: "New York",
|
|
751
|
+
county: "New York County",
|
|
752
|
+
state: "NY",
|
|
753
|
+
country: "US"
|
|
754
|
+
}
|
|
755
|
+
}]
|
|
756
|
+
})
|
|
757
|
+
};
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
try {
|
|
761
|
+
const result = await fastify.xGeocode.batchGeocode(["10001", "invalid", "10001"]);
|
|
762
|
+
assert.ok(Array.isArray(result), "Returns array even with errors");
|
|
763
|
+
assert.equal(result.length, 3, "Returns result for all items");
|
|
764
|
+
assert.ok(result[0].lat, "Successful items have coordinates");
|
|
765
|
+
assert.equal(result[1].success, false, "Failed items marked with success: false");
|
|
766
|
+
assert.ok(result[1].error, "Failed items have error message");
|
|
767
|
+
} finally {
|
|
768
|
+
global.fetch = originalFetch;
|
|
769
|
+
}
|
|
770
|
+
} finally {
|
|
771
|
+
await fastify.close();
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
// validateAddress() Tests
|
|
776
|
+
test("xGeocode validateAddress - validates and returns formatted address", async () => {
|
|
777
|
+
const fastify = Fastify({ logger: false });
|
|
778
|
+
try {
|
|
779
|
+
await fastify.register(xGeocode, {
|
|
780
|
+
apiKey: "test-api-key"
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
// Mock fetch
|
|
784
|
+
const originalFetch = global.fetch;
|
|
785
|
+
global.fetch = async () => ({
|
|
786
|
+
ok: true,
|
|
787
|
+
json: async () => ({
|
|
788
|
+
results: [{
|
|
789
|
+
location: { lat: 40.7128, lng: -74.0060 },
|
|
790
|
+
formatted_address: "123 Main Street, New York, NY 10001, USA",
|
|
791
|
+
address_components: {
|
|
792
|
+
city: "New York",
|
|
793
|
+
county: "New York County",
|
|
794
|
+
state: "NY",
|
|
795
|
+
country: "US",
|
|
796
|
+
zip: "10001"
|
|
797
|
+
},
|
|
798
|
+
accuracy: "rooftop"
|
|
799
|
+
}]
|
|
800
|
+
})
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
try {
|
|
804
|
+
const result = await fastify.xGeocode.validateAddress("123 Main St");
|
|
805
|
+
assert.ok(result, "Returns validation result");
|
|
806
|
+
assert.equal(result.valid, true, "Marks valid address as valid");
|
|
807
|
+
assert.equal(result.input, "123 Main St", "Returns original input");
|
|
808
|
+
assert.ok(result.formatted, "Returns formatted address");
|
|
809
|
+
assert.ok(result.confidence, "Returns confidence/accuracy");
|
|
810
|
+
} finally {
|
|
811
|
+
global.fetch = originalFetch;
|
|
812
|
+
}
|
|
813
|
+
} finally {
|
|
814
|
+
await fastify.close();
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
test("xGeocode validateAddress - returns invalid for unrecognized address", async () => {
|
|
819
|
+
const fastify = Fastify({ logger: false });
|
|
820
|
+
try {
|
|
821
|
+
await fastify.register(xGeocode, {
|
|
822
|
+
apiKey: "test-api-key"
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
// Mock fetch to return empty results
|
|
826
|
+
const originalFetch = global.fetch;
|
|
827
|
+
global.fetch = async () => ({
|
|
828
|
+
ok: true,
|
|
829
|
+
json: async () => ({ results: [] })
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
try {
|
|
833
|
+
const result = await fastify.xGeocode.validateAddress("completely invalid address");
|
|
834
|
+
assert.equal(result.valid, false, "Marks unrecognized address as invalid");
|
|
835
|
+
assert.equal(result.input, "completely invalid address", "Returns original input");
|
|
836
|
+
assert.ok(result.error, "Returns error message");
|
|
837
|
+
} finally {
|
|
838
|
+
global.fetch = originalFetch;
|
|
839
|
+
}
|
|
840
|
+
} finally {
|
|
841
|
+
await fastify.close();
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
test("xGeocode validateAddress - handles API errors gracefully", async () => {
|
|
846
|
+
const fastify = Fastify({ logger: false });
|
|
847
|
+
try {
|
|
848
|
+
await fastify.register(xGeocode, {
|
|
849
|
+
apiKey: "test-api-key"
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
// Mock fetch to return error status
|
|
853
|
+
const originalFetch = global.fetch;
|
|
854
|
+
global.fetch = async () => ({
|
|
855
|
+
ok: false,
|
|
856
|
+
status: 500
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
try {
|
|
860
|
+
const result = await fastify.xGeocode.validateAddress("123 Main St");
|
|
861
|
+
assert.equal(result.valid, false, "Marks API errors as invalid");
|
|
862
|
+
assert.ok(result.error, "Returns error message");
|
|
863
|
+
} finally {
|
|
864
|
+
global.fetch = originalFetch;
|
|
865
|
+
}
|
|
866
|
+
} finally {
|
|
867
|
+
await fastify.close();
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
test("xGeocode validateAddress - rejects address with less than 3 characters", async () => {
|
|
872
|
+
const fastify = Fastify({ logger: false });
|
|
873
|
+
try {
|
|
874
|
+
await fastify.register(xGeocode, {
|
|
875
|
+
apiKey: "test-api-key"
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
await assert.rejects(
|
|
879
|
+
() => fastify.xGeocode.validateAddress("ab"),
|
|
880
|
+
{ message: /minimum 3 characters/ }
|
|
881
|
+
);
|
|
882
|
+
} finally {
|
|
883
|
+
await fastify.close();
|
|
884
|
+
}
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
test("xGeocode validateAddress - rejects null address", async () => {
|
|
888
|
+
const fastify = Fastify({ logger: false });
|
|
889
|
+
try {
|
|
890
|
+
await fastify.register(xGeocode, {
|
|
891
|
+
apiKey: "test-api-key"
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
await assert.rejects(
|
|
895
|
+
() => fastify.xGeocode.validateAddress(null),
|
|
896
|
+
{ message: /Invalid input/ }
|
|
897
|
+
);
|
|
898
|
+
} finally {
|
|
899
|
+
await fastify.close();
|
|
900
|
+
}
|
|
901
|
+
});
|