├── .eslintignore ├── assets └── logo.png ├── jest.config.js ├── .gitignore ├── .travis.yml ├── tsconfig.json ├── src ├── core │ ├── validators.ts │ ├── request.ts │ └── textIDs.ts └── index.ts ├── .eslintrc.json ├── LICENSE ├── package.json ├── CHANGELOG.md ├── README.md └── tests ├── expected └── mock.xml └── api.test.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /build -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wgumenyuk/msn-weather/HEAD/assets/logo.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "node" 4 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Build 5 | /build 6 | 7 | # Coverage 8 | /coverage -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 17 5 | 6 | install: 7 | - npm install 8 | 9 | scripts: 10 | - npm install codecov -g 11 | - npm run lint 12 | - npm run test 13 | 14 | after_success: 15 | - codecov -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "outDir": "./build", 7 | "allowUnreachableCode": false, 8 | "alwaysStrict": true, 9 | "declaration": true, 10 | "esModuleInterop": true, 11 | "noEmitOnError": true, 12 | "removeComments": true, 13 | "sourceMap": true 14 | }, 15 | "include": [ 16 | "./src/**/*" 17 | ], 18 | "exclude": [ 19 | "./build", 20 | "./node_modules" 21 | ] 22 | } -------------------------------------------------------------------------------- /src/core/validators.ts: -------------------------------------------------------------------------------- 1 | /** 2 | Checks if a given input is a valid degree type (either Celcius or Fahrenheit). 3 | @param input The user's input. 4 | @returns Validity of the input. 5 | */ 6 | export function isDegreeType(input: unknown): boolean { 7 | return ( 8 | typeof input === "string" && 9 | input === "C" || 10 | input === "F" 11 | ); 12 | } 13 | 14 | /** 15 | Checks if a given input adheres to the ISO 639.1:2002 standard. 16 | @param input The user's input. 17 | @returns Validity of the input. 18 | */ 19 | export function isLanguageCode(input: unknown): boolean { 20 | return ( 21 | typeof input === "string" && 22 | input.length === 2 && 23 | !!input.match(/[A-Za-z]/) 24 | ); 25 | } -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "@typescript-eslint" 6 | ], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended" 11 | ], 12 | "rules": { 13 | "array-bracket-newline": [ "error", "consistent" ], 14 | "array-bracket-spacing": [ "error", "always" ], 15 | "block-spacing": [ "error", "always" ], 16 | "brace-style": [ "error", "1tbs" ], 17 | "camelcase": [ "error", { "properties": "always" } ], 18 | "eol-last": [ "error", "never" ], 19 | "indent": [ "error", 4, { "SwitchCase": 1 } ], 20 | "line-comment-position": [ "error", { "position": "above" } ], 21 | "object-curly-spacing": [ "error", "always" ], 22 | "quotes": [ "error", "double" ], 23 | "semi": [ "error", "always" ] 24 | } 25 | } -------------------------------------------------------------------------------- /src/core/request.ts: -------------------------------------------------------------------------------- 1 | import https from "https"; 2 | 3 | /** 4 | Sends a request using the native `https` library. 5 | @param url The target URL. 6 | @returns HTTPS response body. 7 | */ 8 | function request(url: string): Promise { 9 | return new Promise((resolve, reject) => { 10 | const req = https.get(url, (res) => { 11 | let data = ""; 12 | 13 | if(res.statusCode !== 200) { 14 | reject(new Error(`Request failed with status ${res.statusCode}`)); 15 | } 16 | 17 | res.on("data", (chunk: string) => { 18 | data += chunk; 19 | }); 20 | 21 | res.on("end", () => { 22 | resolve(data); 23 | }); 24 | }); 25 | 26 | req.setTimeout(5000); 27 | 28 | req.on("timeout", () => { 29 | reject(new Error("Request timed out after 5s")); 30 | }); 31 | 32 | req.on("error", (error) => { 33 | reject(error); 34 | }); 35 | }); 36 | } 37 | 38 | export default request; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Wlad Gumenyuk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msn-weather", 3 | "version": "2.1.3", 4 | "description": "A simple MSN Weather API wrapper with built-in TypeScript support.", 5 | "main": "./build/index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "dev": "ts-node ./src/index.ts", 9 | "lint": "eslint ./src --ext .ts", 10 | "test": "jest --coverage --silent" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/wgumenyuk/msn-weather.git" 15 | }, 16 | "keywords": [ 17 | "msn", 18 | "weather", 19 | "api", 20 | "typescript" 21 | ], 22 | "files": [ 23 | "build/**/*" 24 | ], 25 | "author": "Wlad Gumenyuk", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/wgumenyuk/msn-weather/issues" 29 | }, 30 | "homepage": "https://github.com/wgumenyuk/msn-weather#readme", 31 | "devDependencies": { 32 | "@types/jest": "^26.0.23", 33 | "@types/node": "^15.12.2", 34 | "@typescript-eslint/eslint-plugin": "^4.26.1", 35 | "@typescript-eslint/parser": "^4.26.1", 36 | "eslint": "^7.28.0", 37 | "jest": "^27.0.4", 38 | "nock": "^13.1.0", 39 | "ts-jest": "^27.0.3", 40 | "ts-node": "^10.0.0", 41 | "typescript": "^4.3.2" 42 | }, 43 | "dependencies": { 44 | "fast-xml-parser": "^3.19.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/core/textIDs.ts: -------------------------------------------------------------------------------- 1 | /** 2 | A list of custom text IDs for MSN's number codes. 3 | */ 4 | const textIDs: { [key: string]: string } = { 5 | "0": "thunderstorm", 6 | "1": "thunderstorm", 7 | "2": "thunderstorm", 8 | "3": "thunderstorm", 9 | "4": "thunderstorm", 10 | "17": "thunderstorm", 11 | "35": "thunderstorm", 12 | "5": "rain_snow_mix", 13 | "6": "sleet_snow_mix", 14 | "7": "rain_snow_sleet_mix", 15 | "8": "light_rain", 16 | "9": "light_rain", 17 | "10": "rain_sleet_mix", 18 | "11": "rain_shower", 19 | "12": "rain", 20 | "13": "light_snow", 21 | "14": "snow", 22 | "16": "snow", 23 | "42": "snow", 24 | "43": "snow", 25 | "15": "blizzard", 26 | "18": "rain_showers", 27 | "40": "rain_showers", 28 | "19": "dust", 29 | "20": "foggy", 30 | "21": "haze", 31 | "22": "smoke", 32 | "23": "windy", 33 | "24": "windy", 34 | "25": "frigid", 35 | "26": "cloudy", 36 | "27": "mostly_cloudy_night", 37 | "29": "mostly_cloudy_night", 38 | "33": "mostly_cloudy_night", 39 | "28": "mostly_cloudy", 40 | "30": "partly_sunny", 41 | "34": "partly_sunny", 42 | "31": "clear_night", 43 | "32": "clear", 44 | "36": "hot", 45 | "37": "scattered_thunderstorms", 46 | "38": "scattered_thunderstorms", 47 | "39": "scattered_rain_showers", 48 | "41": "scattered_snow_showers", 49 | "45": "scattered_rain_showers_night", 50 | "46": "scattered_snow_showers_night", 51 | "47": "scattered_thunderstorms_night" 52 | }; 53 | 54 | export default textIDs; -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/). 4 | 5 | ## Unreleased 6 | - Changed `textID` 28 from `partly_sunny` to `mostly_cloudy` 7 | 8 | ## 2.1.3 (June 04, 2022) 9 | ### Fixed 10 | - Faulty index access giving back first character only 11 | 12 | ### Changed 13 | - Changed `textID` 11 from `light_rain` to `rain_shower` 14 | 15 | ## 2.1.2 (August 01, 2021) 16 | ### Added 17 | - Added a contributors anchor 18 | - Added a [`encodeURIComponent`](https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent) explanation to API documentation 19 | 20 | ### Changed 21 | - Replaced version badge with size badge 22 | 23 | ## 2.1.1 (July 20, 2021) 24 | ### Changed 25 | - Switched from `xml2js` to `fast-xml-parser`: this leads to a much smaller bundle size (see [xml2js](https://bundlephobia.com/package/xml2js@0.4.23) vs. [fast-xml-parser](https://bundlephobia.com/package/fast-xml-parser@3.19.0)) 26 | - Renamed `utils` directory to `core` 27 | - Renamed `data` directory to `expected` 28 | - Improved tests 29 | 30 | ## 2.0.1 (July 16, 2021) 31 | ### Fixed 32 | - Fixed bad JSDoc for `request` function 33 | 34 | ## 2.0.0 (July 02, 2021) 35 | ### Added 36 | - Added anchor link to retreived data format to describe the returned data better 37 | - Added contributor acknowledgements to README 38 | 39 | ### Changed 40 | - Changed HTTP to HTTPS ([#2](https://github.com/wgumenyuk/msn-weather/issues/2)) 41 | 42 | ### Fixed 43 | - Broken anchor link in README 44 | 45 | ## 1.0.2 (June 20, 2021) 46 | ### Fixed 47 | - Fixed wrong API documentation ([#1](https://github.com/wgumenyuk/msn-weather/pull/1)) 48 | - Fixed broken link in README 49 | 50 | ### Changed 51 | - Converted language code to lowecase in README 52 | 53 | ## 1.0.1 (June 10, 2021) 54 | ### Added 55 | - Added acknowledgements to README 56 | 57 | ### Fixed 58 | - Fixed some README typos 59 | 60 | ## 1.0.0 (June 10, 2021) 61 | ### Added 62 | - `search()` method to retrieve weather data 63 | - Detailed README 64 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import x2js from "fast-xml-parser"; 2 | import { isDegreeType, isLanguageCode } from "./core/validators"; 3 | import request from "./core/request"; 4 | import textIDs from "./core/textIDs"; 5 | 6 | // #region Types and interfaces 7 | type Degree = "C" | "F"; 8 | 9 | interface Options { 10 | location: string; 11 | language?: string; 12 | degreeType?: Degree; 13 | } 14 | 15 | interface Forecast { 16 | date: string; 17 | day: string; 18 | temperature: { 19 | low: string; 20 | high: string; 21 | }; 22 | sky: { 23 | code: string; 24 | text: string; 25 | }; 26 | precip: string; 27 | } 28 | 29 | interface Current { 30 | date: string; 31 | day: string; 32 | temperature: string; 33 | sky: { 34 | code: string; 35 | text: string; 36 | }; 37 | observation: { 38 | time: string; 39 | point: string; 40 | }; 41 | feelsLike: string; 42 | humidity: string; 43 | wind: { 44 | display: string; 45 | speed: string; 46 | }; 47 | } 48 | 49 | interface Weather { 50 | current: Current; 51 | forecasts: Forecast[]; 52 | } 53 | // #endregion 54 | 55 | /** 56 | Retreives weather data for a given location. 57 | @param options Options. 58 | @param options.location The location you're looking for. 59 | @param options.language The language in which text will be returned. 60 | @param options.degreeType Degree type for temperature values. 61 | @returns Weather data. 62 | */ 63 | async function search(options: Options): Promise { 64 | if(!options || typeof options !== "object") { 65 | throw new Error("Invalid options were specified"); 66 | } 67 | 68 | if(!options.location) { 69 | throw new Error("No location was given"); 70 | } 71 | 72 | const language = options.language || "en"; 73 | const degreeType = options.degreeType || "C"; 74 | 75 | if(!isLanguageCode(language)) { 76 | throw new Error(` 77 | Invalid language code '${language}', make sure it adheres to the ISO 639.1:2002 standard 78 | `); 79 | } 80 | 81 | if(!isDegreeType(degreeType)) { 82 | throw new Error(`Invalid degree type '${degreeType}'`); 83 | } 84 | 85 | const url = 86 | "https://weather.service.msn.com/find.aspx?src=msn&" + 87 | `weadegreetype=${degreeType}&` + 88 | `culture=${language}&` + 89 | `weasearchstr=${encodeURIComponent(options.location)}`; 90 | 91 | const response = await request(url); 92 | 93 | const json = x2js.parse(response, { 94 | attributeNamePrefix: "", 95 | ignoreAttributes: false, 96 | ignoreNameSpace: true, 97 | trimValues: true 98 | }); 99 | 100 | if(!json || !json.weatherdata || !json.weatherdata.weather) { 101 | throw new Error("Bad response: Failed to parse response body"); 102 | } 103 | 104 | const data = json.weatherdata.weather[0]; 105 | 106 | if(!data) { 107 | throw new Error("Bad response: Failed to receive weather data"); 108 | } 109 | 110 | const current = data.current; 111 | const forecasts = []; 112 | 113 | for(let i = 1; i < data.forecast.length; i++) { 114 | const forecast = data.forecast[i]; 115 | 116 | forecasts.push({ 117 | date: forecast.date, 118 | day: forecast.day, 119 | temperature: { 120 | low: forecast.low[0] + `°${degreeType}`, 121 | high: forecast.high[0] + `°${degreeType}`, 122 | }, 123 | sky: { 124 | code: textIDs[forecast.skycodeday[0]], 125 | text: forecast.skytextday[0] 126 | }, 127 | precip: forecast.precip[0] + "%" 128 | }); 129 | } 130 | 131 | const weather: Weather = { 132 | current: { 133 | date: current.date, 134 | day: current.day, 135 | temperature: current.temperature + `°${degreeType}`, 136 | sky: { 137 | code: textIDs[current.skycode], 138 | text: current.skytext 139 | }, 140 | observation: { 141 | time: current.observationtime, 142 | point: current.observationpoint 143 | }, 144 | feelsLike: current.feelslike + `°${degreeType}`, 145 | humidity: current.humidity + "%", 146 | wind: { 147 | display: current.winddisplay, 148 | speed: current.windspeed 149 | } 150 | }, 151 | forecasts 152 | }; 153 | 154 | return weather; 155 | } 156 | 157 | export default { 158 | search 159 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | ![Logo](./assets/logo.png) 4 | 5 | # MSN Weather API 6 | A simple MSN Weather API wrapper with built-in TypeScript support. 7 | 8 | [![Travis CI](https://img.shields.io/travis/com/wgumenyuk/msn-weather?label=Build&style=flat-square)](https://travis-ci.com/github/wgumenyuk/msn-weather) 9 | [![Codecov](https://img.shields.io/codecov/c/github/wgumenyuk/msn-weather?label=Coverage&logo=codecov&style=flat-square)](https://codecov.io/gh/wgumenyuk/msn-weather) 10 | [![Code Climate](https://img.shields.io/codeclimate/maintainability/wgumenyuk/msn-weather?label=Maintainability&logo=Code%20Climate&style=flat-square)](https://codeclimate.com/github/wgumenyuk/msn-weather) 11 | [![NPM downloads](https://img.shields.io/npm/dt/msn-weather?label=Downloads&style=flat-square)](https://www.npmjs.com/package/msn-weather) 12 | [![Size](https://img.shields.io/bundlephobia/minzip/msn-weather?label=Size&style=flat-square)](https://github.com/wgumenyuk/msn-weather) 13 | [![License](https://img.shields.io/github/license/wgumenyuk/msn-weather?label=License&style=flat-square)](./LICENSE) 14 | 15 | [![NPM install info](https://nodei.co/npm/msn-weather.png?downloads=true&stars=true)](https://www.npmjs.com/package/msn-weather) 16 |
17 | 18 | ## Deprecation Notice 19 | > ⚠️ This package is deprecated and no longer works. 20 | 21 | ## Table of contents 22 | - [About](#about) 23 | - [Installation](#installation) 24 | - [Usage](#usage) 25 | - [Usage with CommonJS](#usage-with-commonjs) 26 | - [Retrieved data format](#retrieved-data-format) 27 | - [API documentation](#api-documentation) 28 | - [Other languages](#other-languages) 29 | - [Resources](#resources) 30 | - [Acknowledgements](#acknowledgements) 31 | - [Contributors](#contributors) 32 | - [License](#license) 33 | 34 | ## About 35 | `msn-weather` is a powerful [Node.js](https://nodejs.org) library that allows you to easily retrieve weather data for any location in the world. 36 | As the name suggests, this wrapper uses the MSN weather API behind the scenes. 37 | 38 | - Simple and easy-to-use API 39 | - Built-in TypeScript definitions 40 | - Only one dependency 41 | - Clear weather data format 42 | - Performant 43 | 44 | ## Installation 45 | Install this package using NPM: 46 | 47 | ```sh-session 48 | npm install msn-weather --save 49 | ``` 50 | 51 | ## Usage 52 | ```ts 53 | import weather from "msn-weather"; 54 | 55 | const data = await weather.search({ 56 | location: "Munich, DE", 57 | language: "en", 58 | degreeType: "C" 59 | }); 60 | ``` 61 | 62 | ### Usage with CommonJS 63 | To use this library with CommonJS, use this approach: 64 | ```js 65 | const weather = require("msn-weather").default; 66 | ``` 67 | 68 | ### Retrieved data format 69 | You will receive a JavaScript object looking like this: 70 | 71 |
72 | Show response 73 | 74 | ```js 75 | { 76 | current: { 77 | date: "2021-06-09", 78 | day: "Wednesday", 79 | temperature: "23°C", 80 | sky: { 81 | code: "partly_sunny", 82 | text: "Partly Sunny" 83 | }, 84 | observation: { 85 | time: "12:00:00", 86 | point: "Munich, BY, Germany" 87 | }, 88 | feelsLike: "22°C", 89 | humidity: "58%", 90 | wind: { 91 | display: "7 km/h North", 92 | speed: "7 km/h" 93 | } 94 | }, 95 | forecasts: [ 96 | { 97 | date: "2021-06-09", 98 | day: "Wednesday", 99 | temperature: { 100 | low: "14°C", 101 | high: "24°C" 102 | }, 103 | sky: { 104 | code: "partly_sunny", 105 | text: "Partly Sunny" 106 | }, 107 | precip: "90%" 108 | }, 109 | { 110 | date: "2021-06-10", 111 | day: "Thursday", 112 | temperature: { 113 | low: "13°C", 114 | high: "22°C" 115 | }, 116 | sky: { 117 | code: "light_rain", 118 | text: "Light Rain" 119 | }, 120 | precip: "100%" 121 | }, 122 | { 123 | date: "2021-06-11", 124 | day: "Friday", 125 | temperature: { 126 | low: "15°C", 127 | high: "23°C" 128 | }, 129 | sky: { 130 | code: "light_rain", 131 | text: "Light Rain" 132 | }, 133 | precip: "100%" 134 | }, 135 | { 136 | date: "2021-06-12", 137 | day: "Saturday", 138 | temperature: { 139 | low: "15°C", 140 | high: "24°C" 141 | }, 142 | sky: { 143 | code: "light_rain", 144 | text: "Light Rain" 145 | }, 146 | precip: "100%" 147 | } 148 | ] 149 | } 150 | ``` 151 |
152 | 153 | ### API documentation 154 | #### `weather.search(options)` 155 | Retrieves weather data for a given location. Returns a promise with weather data (see [retrieved data format](#retrieved-data-format)). 156 | 157 | | Parameter | Type | Optional | Default | Description | 158 | |-----------|-----------------------|----------|---------|-------------------------| 159 | | `options` | [`Options`](#options) | ❌ | None | Options for the search. | 160 | 161 | #### `Options` 162 | Options for the search. 163 | | Parameter | Type | Optional | Default | Description | 164 | |-|-|-|-|-| 165 | | `location` | String | ❌ | None | Location for the weather data. The location will be encoded automatically using [`encodeURIComponent`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent). | 166 | | `language` | String | ✔ | `en` | Language in which weather text will be returned. The value must be a [ISO 639.1:2002 language code](https://en.wikipedia.org/wiki/ISO_639-1). | 167 | | `degreeType` | String | ✔ | `C` | Degree type for temperature values. Either Celsius (`C`) or Fahrenheit (`F`). | 168 | 169 | ## Resources 170 | - [Changelog](./CHANGELOG.md) 171 | - [NPM](https://www.npmjs.com/package/msn-weather) 172 | - [GitHub](https://github.com/wgumenyuk/msn-weather) 173 | 174 | ## Acknowledgements 175 | - [Logo](https://twemoji.twitter.com) by Twitter Twemoji (licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)) 176 | 177 | ### Contributors 178 | A special thanks goes out to these contributors: 179 | 180 | - Khang ([khang-nd](https://github.com/khang-nd)) - Contributing a number of times 181 | 182 | ## License 183 | This project is licensed under [MIT](./LICENSE). -------------------------------------------------------------------------------- /tests/expected/mock.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /tests/api.test.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import nock from "nock"; 4 | import weather from "../src"; 5 | import request from "../src/core/request"; 6 | 7 | const msnMock = nock("https://weather.service.msn.com/find.aspx"); 8 | 9 | const mockDataPath = path.join(__dirname, "./expected/mock.xml"); 10 | const mockData = fs.readFileSync(mockDataPath, "utf-8"); 11 | 12 | const url = 13 | "https://weather.service.msn.com/find.aspx?src=msn&" + 14 | "weadegreetype=C&" + 15 | "culture=en&" + 16 | `weasearchstr=${encodeURIComponent("München, DE")}`; 17 | 18 | describe("msn-weather", () => { 19 | describe("msn-weather#search", () => { 20 | it("should be a function", (done: jest.DoneCallback) => { 21 | expect(typeof weather.search).toBe("function"); 22 | done(); 23 | }); 24 | 25 | it("should not accept invalid options", async () => { 26 | await expect(weather.search(null)) 27 | .rejects 28 | .toThrow("Invalid options were specified"); 29 | 30 | await expect(weather.search({ location: "" })) 31 | .rejects 32 | .toThrow("No location was given"); 33 | 34 | await expect(weather.search({ location: "München, DE", language: "ABC" })) 35 | .rejects 36 | .toThrow("Invalid language code 'ABC', make sure it adheres to the ISO 639.1:2002 standard"); 37 | 38 | // @ts-ignore 39 | await expect(weather.search({ location: "München, DE", degreeType: "K" })) 40 | .rejects 41 | .toThrow("Invalid degree type 'K'"); 42 | }); 43 | 44 | it("should fail when receiving a non-200 status code", async () => { 45 | msnMock 46 | .get("") 47 | .query({ 48 | src: "msn", 49 | weadegreetype: "C", 50 | culture: "en", 51 | weasearchstr: "München, DE" 52 | }) 53 | .reply(404); 54 | 55 | await expect(request(url)) 56 | .rejects 57 | .toThrow("Request failed with status 404"); 58 | }); 59 | 60 | it("should fail when timing out after 5 seconds", async () => { 61 | msnMock 62 | .get("") 63 | .query({ 64 | src: "msn", 65 | weadegreetype: "C", 66 | culture: "en", 67 | weasearchstr: "München, DE" 68 | }) 69 | .delayConnection(5100) 70 | .reply(500); 71 | 72 | await expect(request(url)) 73 | .rejects 74 | .toThrow("Request timed out after 5s"); 75 | 76 | nock.abortPendingRequests(); 77 | }); 78 | 79 | it("should fail when a connection error occurs", async () => { 80 | msnMock 81 | .get("") 82 | .query({ 83 | src: "msn", 84 | weadegreetype: "C", 85 | culture: "en", 86 | weasearchstr: "München, DE" 87 | }) 88 | .replyWithError("ECONNRESET"); 89 | 90 | await expect(request(url)) 91 | .rejects 92 | .toThrow("ECONNRESET"); 93 | }); 94 | 95 | it("should fail if the response body can't be parsed", async () => { 96 | msnMock 97 | .get("") 98 | .query({ 99 | src: "msn", 100 | weadegreetype: "C", 101 | culture: "en", 102 | weasearchstr: "München, DE" 103 | }) 104 | .reply(200); 105 | 106 | const options = { 107 | location: "München, DE" 108 | }; 109 | 110 | await expect(weather.search(options)) 111 | .rejects 112 | .toThrow("Bad response: Failed to parse response body"); 113 | }); 114 | 115 | it("should fail if the response body is incomplete", async () => { 116 | msnMock 117 | .get("") 118 | .query({ 119 | src: "msn", 120 | weadegreetype: "C", 121 | culture: "en", 122 | weasearchstr: "München, DE" 123 | }) 124 | .reply(200, 125 | ` 128 | 129 | 130 | 131 | ` 132 | ); 133 | 134 | const options = { 135 | location: "München, DE" 136 | }; 137 | 138 | await expect(weather.search(options)) 139 | .rejects 140 | .toThrow("Bad response: Failed to receive weather data"); 141 | }); 142 | 143 | it("should return correct weather data for a location", async () => { 144 | msnMock 145 | .get("") 146 | .query({ 147 | src: "msn", 148 | weadegreetype: "C", 149 | culture: "en", 150 | weasearchstr: "München, DE" 151 | }) 152 | .reply(200, mockData); 153 | 154 | const data = await weather.search({ 155 | location: "München, DE" 156 | }); 157 | 158 | expect(data).toHaveProperty("current"); 159 | expect(data).toHaveProperty("forecasts"); 160 | 161 | expect(data.current).toHaveProperty("date"); 162 | expect(data.current).toHaveProperty("day"); 163 | expect(data.current).toHaveProperty("temperature"); 164 | expect(data.current).toHaveProperty("sky"); 165 | expect(data.current.sky).toHaveProperty("code"); 166 | expect(data.current.sky).toHaveProperty("text"); 167 | expect(data.current).toHaveProperty("observation"); 168 | expect(data.current.observation).toHaveProperty("time"); 169 | expect(data.current.observation).toHaveProperty("point"); 170 | expect(data.current).toHaveProperty("feelsLike"); 171 | expect(data.current).toHaveProperty("humidity"); 172 | expect(data.current).toHaveProperty("wind"); 173 | expect(data.current.wind).toHaveProperty("display"); 174 | expect(data.current.wind).toHaveProperty("speed"); 175 | 176 | expect(Array.isArray(data.forecasts)).toBeTruthy(); 177 | expect(data.forecasts[0]).toHaveProperty("date"); 178 | expect(data.forecasts[0]).toHaveProperty("day"); 179 | expect(data.forecasts[0]).toHaveProperty("temperature"); 180 | expect(data.forecasts[0].temperature).toHaveProperty("low"); 181 | expect(data.forecasts[0].temperature).toHaveProperty("high"); 182 | expect(data.forecasts[0]).toHaveProperty("sky"); 183 | expect(data.forecasts[0].sky).toHaveProperty("code"); 184 | expect(data.forecasts[0].sky).toHaveProperty("text"); 185 | expect(data.forecasts[0]).toHaveProperty("precip"); 186 | }); 187 | }); 188 | }); 189 | --------------------------------------------------------------------------------