Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add provider name #17

Merged
merged 2 commits into from
Oct 10, 2024
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 package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "weather-plus",
"version": "1.0.0",
"version": "1.0.1",
"description": "Weather Plus is a powerful wrapper around various Weather APIs that simplifies adding weather data to your application",
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
Expand Down
23 changes: 23 additions & 0 deletions src/cache.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Cache } from './cache';
import { RedisClientType } from 'redis';
import { IWeatherUnits } from './interfaces';

describe('Cache', () => {
let cache: Cache;
Expand Down Expand Up @@ -199,5 +200,27 @@ describe('Cache', () => {

jest.useRealTimers();
});

test('Cache stores and retrieves weather data including provider', async () => {
// Arrange
const cache = new Cache();
const key = 'geohash_key';
const weatherData = {
dewPoint: { value: 8, unit: IWeatherUnits.C },
humidity: { value: 65, unit: IWeatherUnits.percent },
temperature: { value: 16, unit: IWeatherUnits.C },
conditions: { value: 'Rainy', unit: IWeatherUnits.string },
provider: 'nws',
};

// Act
await cache.set(key, JSON.stringify(weatherData));
const cachedData = await cache.get(key);

// Assert
expect(cachedData).not.toBeNull();
expect(JSON.parse(cachedData!)).toEqual(weatherData);
expect(JSON.parse(cachedData!).provider).toBe('nws'); // Verify provider name
});
});
});
1 change: 1 addition & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ describe('WeatherPlus Library', () => {
const weatherPlus = new WeatherPlus();
const response = await weatherPlus.getWeather(lat, lng);
const expectedResponse: IWeatherData = {
provider: 'nws',
dewPoint: {
value: 20,
unit: 'C',
Expand Down
2 changes: 2 additions & 0 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ export interface IWeatherData {
[IWeatherKey.humidity]: IRelativeHumidity;
[IWeatherKey.temperature]: ITemperature;
[IWeatherKey.conditions]: IConditions;
provider: string;
}

export type IBaseWeatherProperty<T, U extends IWeatherUnits> = {
value: T;
unit: U | keyof typeof IWeatherUnits;
Expand Down
2 changes: 2 additions & 0 deletions src/providers/nws/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ describe('NWSProvider', () => {
humidity: { value: 80, unit: 'percent' },
temperature: { value: 20, unit: 'C' },
conditions: { value: 'Clear', unit: 'string' },
provider: 'nws',
});
});

Expand Down Expand Up @@ -132,6 +133,7 @@ describe('NWSProvider', () => {
humidity: { value: 80, unit: 'percent' },
temperature: { value: 20, unit: 'C' },
conditions: { value: 'Clear', unit: 'string' },
provider: 'nws',
});
});
});
2 changes: 2 additions & 0 deletions src/providers/nws/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export class NWSProvider implements IWeatherProvider {
if (Object.keys(data).length === 0) {
throw new Error('Invalid observation data');
}
data.provider = 'nws';

return data as IWeatherData;
} catch (error) {
Expand Down Expand Up @@ -152,6 +153,7 @@ async function fetchLatestObservation(
function convertToWeatherData(observation: any): IWeatherData {
const properties = observation.properties;
return {
provider: 'nws',
dewPoint: {
value: properties.dewpoint.value,
unit:
Expand Down
1 change: 1 addition & 0 deletions src/providers/openweather/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ describe('OpenWeatherProvider', () => {
humidity: { value: 80, unit: 'percent' },
temperature: { value: 20, unit: 'C' },
conditions: { value: 'clear sky', unit: 'string' },
provider: 'openweather',
});
});

Expand Down
1 change: 1 addition & 0 deletions src/providers/openweather/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export class OpenWeatherProvider implements IWeatherProvider {

function convertToWeatherData(data: IOpenWeatherResponse): IWeatherData {
return {
provider: 'openweather',
dewPoint: {
value: data.current.dew_point,
unit: IWeatherUnits.C,
Expand Down
97 changes: 86 additions & 11 deletions src/weatherService.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { WeatherService } from './weatherService';
import { InvalidProviderLocationError } from './errors';
import { NWSProvider } from './providers/nws/client';
import { OpenWeatherProvider } from './providers/openweather/client';
import { IWeatherUnits, IWeatherData } from './interfaces';
import { IWeatherProvider } from './providers/IWeatherProvider';

jest.mock('./cache', () => {
return {
Expand Down Expand Up @@ -30,7 +30,13 @@ jest.mock('./providers/nws/client', () => {
'The NWS provider only supports locations within the United States.'
);
}
return { provider: 'nws', lat, lng, weather: 'sunny' };
return {
dewPoint: { value: 10, unit: IWeatherUnits.C },
humidity: { value: 80, unit: IWeatherUnits.percent },
temperature: { value: 15, unit: IWeatherUnits.C },
conditions: { value: 'Sunny', unit: IWeatherUnits.string },
provider: 'nws',
} as IWeatherData;
}
}

Expand All @@ -46,7 +52,13 @@ jest.mock('./providers/openweather/client', () => {

class MockOpenWeatherProvider extends originalModule.OpenWeatherProvider {
async getWeather(lat: number, lng: number) {
return { provider: 'openweather', lat, lng, weather: 'cloudy' };
return {
dewPoint: { value: 12, unit: IWeatherUnits.C },
humidity: { value: 70, unit: IWeatherUnits.percent },
temperature: { value: 18, unit: IWeatherUnits.C },
conditions: { value: 'Cloudy', unit: IWeatherUnits.string },
provider: 'openweather',
} as IWeatherData;
}
}

Expand All @@ -62,16 +74,26 @@ describe('WeatherService', () => {

beforeEach(() => {
jest.clearAllMocks();
weatherService = new WeatherService({ providers: ['nws'] });
weatherService = new WeatherService({
providers: ['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 expectedWeatherData: IWeatherData = {
dewPoint: { value: 10, unit: IWeatherUnits.C },
humidity: { value: 80, unit: IWeatherUnits.percent },
temperature: { value: 15, unit: IWeatherUnits.C },
conditions: { value: 'Sunny', unit: IWeatherUnits.string },
provider: 'nws',
};

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

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

it('should fallback to the next provider if the first provider does not support the location', async () => {
Expand All @@ -81,18 +103,28 @@ describe('WeatherService', () => {
openweather: 'your-openweather-api-key',
},
});

const lat = 51.5074; // London, UK
const lng = -0.1278;

const expectedWeatherData: IWeatherData = {
dewPoint: { value: 12, unit: IWeatherUnits.C },
humidity: { value: 70, unit: IWeatherUnits.percent },
temperature: { value: 18, unit: IWeatherUnits.C },
conditions: { value: 'Cloudy', unit: IWeatherUnits.string },
provider: 'openweather',
};

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

expect(weather).toEqual({ provider: 'openweather', lat, lng, weather: 'cloudy' });
expect(weather).toEqual(expectedWeatherData);
});

it('should throw InvalidProviderLocationError if no provider supports the location', async () => {
weatherService = new WeatherService({
providers: ['nws'],
});

const lat = 51.5074; // London, UK
const lng = -0.1278;

Expand All @@ -103,17 +135,25 @@ describe('WeatherService', () => {

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

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

it('should use cached weather data if available', async () => {
const cachedWeatherData = {
dewPoint: { value: 11, unit: IWeatherUnits.C },
humidity: { value: 75, unit: IWeatherUnits.percent },
temperature: { value: 16, unit: IWeatherUnits.C },
conditions: { value: 'Overcast', unit: IWeatherUnits.string },
provider: 'nws',
};

const cacheGetMock = jest
.fn()
.mockResolvedValue(JSON.stringify({ cached: true }));
.mockResolvedValue(JSON.stringify(cachedWeatherData));
const cacheSetMock = jest.fn();

// Replace cache methods with mocks
Expand All @@ -125,13 +165,14 @@ describe('WeatherService', () => {

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

expect(weather).toEqual({ cached: true });
expect(weather).toEqual(cachedWeatherData);
expect(cacheGetMock).toHaveBeenCalled();
expect(cacheSetMock).not.toHaveBeenCalled();
});

it('should rethrow generic errors from provider.getWeather', async () => {
const genericError = new Error('Generic provider error');

// Mock the provider's getWeather to throw a generic error
jest
.spyOn(weatherService['providers'][0], 'getWeather')
Expand Down Expand Up @@ -189,7 +230,6 @@ describe('WeatherService', () => {
});

it('should throw an error if an unsupported provider is specified', () => {
// Use type assertion to bypass TypeScript error for testing purposes
expect(() => {
new WeatherService({ providers: ['unsupportedProvider' as any] });
}).toThrow('Provider unsupportedProvider is not supported yet');
Expand All @@ -200,4 +240,39 @@ describe('WeatherService', () => {
new WeatherService({ providers: ['openweather'] });
}).toThrow('OpenWeather provider requires an API key.');
});

it('should verify that the provider name is included in the weather data', async () => {
// Arrange
const lat = 37.7749; // San Francisco
const lng = -122.4194;

const mockWeatherData: IWeatherData = {
dewPoint: { value: 12, unit: IWeatherUnits.C },
humidity: { value: 70, unit: IWeatherUnits.percent },
temperature: { value: 18, unit: IWeatherUnits.C },
conditions: { value: 'Partly Cloudy', unit: IWeatherUnits.string },
provider: 'openweather',
};

const mockProvider: IWeatherProvider = {
name: 'openweather',
getWeather: jest.fn().mockResolvedValue(mockWeatherData),
};

const weatherService = new WeatherService({
providers: ['openweather'],
apiKeys: { openweather: 'test-api-key' },
});

// Inject mock provider
(weatherService as any).providers = [mockProvider];

// Act
const result = await weatherService.getWeather(lat, lng);

// Assert
expect(mockProvider.getWeather).toHaveBeenCalledWith(lat, lng);
expect(result).toEqual(mockWeatherData);
expect(result.provider).toBe('openweather'); // Verify provider name
});
});
Loading