Skip to content

Commit

Permalink
Now checking to ensure within the U.S. before calling NWS and returni…
Browse files Browse the repository at this point in the history
…ngan error if it is not
  • Loading branch information
victorquinn committed Oct 8, 2024
1 parent 93edff9 commit f450f57
Show file tree
Hide file tree
Showing 5 changed files with 1,786 additions and 2 deletions.
7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,16 @@
"dist"
],
"dependencies": {
"@turf/boolean-point-in-polygon": "^7.1.0",
"@turf/turf": "^7.1.0",
"@types/geojson": "^7946.0.14",
"@types/turf": "^3.5.32",
"axios": "^1.6.8",
"debug": "^4.3.4",
"ngeohash": "^0.6.3",
"redis": "^4.6.13",
"topojson-client": "^3.1.0",
"us-atlas": "^3.0.1",
"zod": "^3.23.8"
},
"devDependencies": {
Expand All @@ -34,6 +40,7 @@
"@types/ngeohash": "^0.6.8",
"@types/node": "^20.12.12",
"@types/redis": "^4.0.11",
"@types/topojson-client": "^3.1.5",
"axios-mock-adapter": "^1.22.0",
"jest": "^29.7.0",
"rimraf": "^5.0.7",
Expand Down
87 changes: 87 additions & 0 deletions src/weatherService.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { WeatherService, InvalidProviderLocationError } from './weatherService';
import * as nwsClient from './providers/nws/client';

jest.mock('./cache', () => {
return {
Cache: jest.fn().mockImplementation(() => {
return {
get: jest.fn().mockResolvedValue(null),
set: jest.fn().mockResolvedValue(null),
};
}),
};
});

jest.mock('./providers/nws/client', () => {
return {
getWeather: jest.fn().mockImplementation(async (lat: number, lng: number) => {
return { lat, lng, weather: 'sunny' };
}),
};
});

describe('WeatherService', () => {
let weatherService: WeatherService;

beforeEach(() => {
jest.clearAllMocks();
weatherService = new WeatherService({ provider: 'nws' });
});

it('should return weather data for a location inside the United States', async () => {
const lat = 38.8977; // Washington, D.C.
const lng = -77.0365;

const weather = await weatherService.getWeather(lat, lng);

expect(weather).toEqual({ lat, lng, weather: 'sunny' });
});

it('should throw InvalidProviderLocationError for a location outside the United States', async () => {
const lat = 51.5074; // London, UK
const lng = -0.1278;

await expect(weatherService.getWeather(lat, lng)).rejects.toThrow(
InvalidProviderLocationError
);
});

it('should not call NWS API for a location outside the United States', async () => {
const getWeatherSpy = jest.spyOn(nwsClient, 'getWeather');
const lat = -33.8688; // Sydney, Australia
const lng = 151.2093;

await expect(weatherService.getWeather(lat, lng)).rejects.toThrow(
InvalidProviderLocationError
);

expect(getWeatherSpy).not.toHaveBeenCalled();
});

it('should handle invalid latitude or longitude', async () => {
const lat = 100; // Invalid latitude
const lng = 200;

await expect(weatherService.getWeather(lat, lng)).rejects.toThrow(
'Invalid latitude or longitude'
);
});

it('should use cached weather data if available', async () => {
const cacheGetMock = jest.fn().mockResolvedValue(JSON.stringify({ cached: true }));
const cacheSetMock = jest.fn();

// Replace cache methods with mocks
(weatherService as any).cache.get = cacheGetMock;
(weatherService as any).cache.set = cacheSetMock;

const lat = 38.8977;
const lng = -77.0365;

const weather = await weatherService.getWeather(lat, lng);

expect(weather).toEqual({ cached: true });
expect(cacheGetMock).toHaveBeenCalled();
expect(cacheSetMock).not.toHaveBeenCalled();
});
});
58 changes: 57 additions & 1 deletion src/weatherService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import { Cache } from './cache';
import * as nws from './providers/nws/client';
import debug from 'debug';
import { z } from 'zod';
import { Feature, Geometry, Point, GeoJsonProperties } from 'geojson';
import booleanPointInPolygon from '@turf/boolean-point-in-polygon';
import { feature } from 'topojson-client';
import { Topology } from 'topojson-specification';
import usAtlasData from 'us-atlas/states-10m.json';
import { Polygon, MultiPolygon } from 'geojson';

const log = debug('weather-plus');

Expand All @@ -18,6 +24,21 @@ const CoordinatesSchema = z.object({
lng: z.number().min(-180).max(180),
});

// Define and export the new Error type
export class InvalidProviderLocationError extends Error {
constructor(message: string) {
super(message);
this.name = 'InvalidProviderLocationError';
}
}

// Cast the imported JSON data to 'any'
const usAtlas = usAtlasData as any;

// Convert TopoJSON to GeoJSON and extract US boundaries
const usGeoJSON = feature(usAtlas, usAtlas.objects.states) as any;
const usBoundaries = usGeoJSON.features as Feature<Geometry, GeoJsonProperties>[];

export class WeatherService {
private cache: Cache;
private provider: string;
Expand Down Expand Up @@ -47,6 +68,41 @@ export class WeatherService {
throw new Error('Invalid latitude or longitude');
}

// If provider is 'nws', check if lat/lng is within the US
if (this.provider === 'nws') {
const point: Feature<Point, GeoJsonProperties> = {
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [lng, lat],
},
properties: {},
};

let isInUS = false;
for (const boundary of usBoundaries) {
// Check if the boundary is a Polygon or MultiPolygon
if (
boundary.geometry.type === 'Polygon' ||
boundary.geometry.type === 'MultiPolygon'
) {
// Cast boundary to the correct type
const polygon = boundary as Feature<Polygon | MultiPolygon, GeoJsonProperties>;

if (booleanPointInPolygon(point, polygon)) {
isInUS = true;
break;
}
}
}

if (!isInUS) {
throw new InvalidProviderLocationError(
'The NWS provider only supports locations within the United States.'
);
}
}

log(`Getting weather for (${lat}, ${lng})`);
const geohash = getGeohash(lat, lng, 6);

Expand All @@ -59,4 +115,4 @@ export class WeatherService {
return weather;
}
}
}
}
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
"skipLibCheck": true,
"resolveJsonModule": true
},
"exclude": ["node_modules", "jest.config.ts"]
}
Loading

0 comments on commit f450f57

Please sign in to comment.