<repo_name>s1ckn3zz1337/stocard-be-challenge<file_sep>/test/integratiom/cities.test.js
const request = require('supertest');
const nock = require('nock');
const app = require('../../src');

const { configApp: { openWeather: { appId } } } = require('../../src/const');

const {
  ids: { cityId },
  values: { cities: { cityLng, cityLat } },
  expectedResponses: { cities: { cityListExpected, cityIdWeatherExpected, cityIdExpected } },
  rawResponses: { openWeather: { openWeatherCityIdResponse, openWeatherCityListResponse } },
} = require('../dummy/data');


describe('test cities route', () => {
  afterAll((done) => {
    app.close(done);
  });
  describe('test GET /cities route', () => {
    it('should return 200 and expected body on succes if query is valid', async () => {
      nock('https://api.openweathermap.org/')
        .get(`/data/2.5/find?lat=${cityLat}&lon=${cityLng}&appid=${appId}`)
        .reply(200, openWeatherCityListResponse);
      await request(app)
        .get(`/cities?lat=${cityLat}&lng=${cityLng}`)
        .expect('Content-Type', /json/)
        .expect(200, cityListExpected);
    });
    it('should return 400 if lat param is missing', async () => {
      await request(app)
        .get('/cities')
        .expect('Content-Type', /json/)
        .expect(400, {
          code: 'BadRequestError',
          message: 'lat/lng required',
        });
    });
    it('should return 400 if lng param is missing', async () => {
      await request(app)
        .get('/cities')
        .expect('Content-Type', /json/)
        .expect(400, {
          code: 'BadRequestError',
          message: 'lat/lng required',
        });
    });
    it('should return 204 on no result', async () => {
      nock('https://api.openweathermap.org/')
        .get(`/data/2.5/find?lat=${cityLat}&lon=${cityLng}&appid=${appId}`)
        .reply(200, { list: [] });
      await request(app)
        .get(`/cities?lat=${cityLat}&lng=${cityLng}`)
        .expect(204, {});
    });
    it('should return 500 on error', async () => {
      nock('https://api.openweathermap.org/')
        .get(`/data/2.5/find?lat=${cityLat}&lon=${cityLng}&appid=${appId}`)
        .reply(500, {});
      await request(app)
        .get(`/cities?lat=${cityLat}&lng=${cityLng}`)
        .expect(500, {
          code: 'InternalServerError',
          message: 'Hoops looks like something went wrong :(',
        });
    });
  });
  describe('test GET /cities/{city_id} route', () => {
    it('should return right body and http 200 on success', async () => {
      nock('https://api.openweathermap.org/')
        .get(`/data/2.5/weather?id=${cityId}&appid=${appId}`)
        .reply(200, openWeatherCityIdResponse);
      await request(app)
        .get(`/cities/${cityId}`)
        .expect('Content-Type', /json/)
        .expect(200, cityIdExpected);
    });
    it('should return 404 if city not found', async () => {
      nock('https://api.openweathermap.org/')
        .get(`/data/2.5/weather?id=${cityId}&appid=${appId}`)
        .reply(404, {});
      await request(app)
        .get(`/cities/${cityId}`)
        .expect('Content-Type', /json/)
        .expect(404, {
          code: 'NotFoundError',
          message: 'not found',
        });
    });
    it('should return 500 on error', async () => {
      nock('https://api.openweathermap.org/')
        .get(`/data/2.5/weather?id=${cityId}&appid=${appId}`)
        .reply(500, {});
      await request(app)
        .get(`/cities/${cityId}`)
        .expect('Content-Type', /json/)
        .expect(500, {
          code: 'InternalServerError',
          message: 'Hoops looks like something went wrong :(',
        });
    });
  });
  describe('test GET /cities/{city_id}/weather route', () => {
    it('should return http 200 and right body on success', async () => {
      nock('https://api.openweathermap.org/')
        .get(`/data/2.5/weather?id=${cityId}&appid=${appId}`)
        .reply(200, openWeatherCityIdResponse);
      await request(app)
        .get(`/cities/${cityId}/weather`)
        .expect('Content-Type', /json/)
        .expect(200, cityIdWeatherExpected);
    });
    it('should return http 404 on not found', async () => {
      nock('https://api.openweathermap.org/')
        .get(`/data/2.5/weather?id=${cityId}&appid=${appId}`)
        .reply(404, {});
      await request(app)
        .get(`/cities/${cityId}/weather`)
        .expect('Content-Type', /json/)
        .expect(404, {
          code: 'NotFoundError',
          message: 'not found',
        });
    });
    it('should return http 500 on error', async () => {
      nock('https://api.openweathermap.org/')
        .get(`/data/2.5/weather?id=${cityId}&appid=${appId}`)
        .reply(500, {});
      await request(app)
        .get(`/cities/${cityId}/weather`)
        .expect('Content-Type', /json/)
        .expect(500, {
          code: 'InternalServerError',
          message: 'Hoops looks like something went wrong :(',
        });
    });
  });
});
<file_sep>/src/route/index.js
const routeCities = require('./cities.route');

module.exports = {
  routeCities,
};
<file_sep>/src/model/City.js
module.exports = (
  {
    externalApiOpenWeather:
        {
          getCitiesByLngLat,
          getWeatherByCityId,
        },
    ModelWeather,
    restifyErrors: { NotFoundError },
  },
) => class City {
  static async listAllByLngLat(lng, lat) {
    const results = [];
    try {
      const { list } = await getCitiesByLngLat(lng, lat);
      list.forEach(({ id, coord, name }) => {
        const city = new City({
          id, name, lat: coord.lat, lng: coord.lon,
        });
        results.push(city);
      });
      return results;
    } catch (err) {
      console.error('error parsing cities');
      throw err;
    }
  }

  static async getById(id) {
    try {
      const cityData = await getWeatherByCityId(id);
      const weather = ModelWeather.parseFromCityData({ cityId: id, ...cityData });
      return new City({
        id: cityData.id,
        name: cityData.name,
        weather,
        lng: cityData.coord.lon,
        lat: cityData.coord.lat,
      });
    } catch (err) {
      if (err.response.status === NotFoundError.prototype.statusCode) {
        throw new NotFoundError('not found');
      }
      throw err;
    }
  }

  constructor({
    id, name, lat, lng,
    weather,
  }) {
    this.attributes = {
      id, name, lat, lng,
    };
    this.currentWeather = weather;
  }

  toDetailedJSON() {
    return this.attributes;
  }

  toJSON() {
    return { id: this.attributes.id, name: this.attributes.name };
  }
};
<file_sep>/src/external/index.js
const externalApiOpenWeather = require('./openWeather');

module.exports = {
  externalApiOpenWeather,
};
<file_sep>/src/route/cities.route.js
module.exports = ({ validate, cities }) => ([
  {
    location: '/cities',
    method: 'get',
    middlewares: [
      validate,
      cities.getCityByLanLng,
    ],
  },
  {
    location: '/cities/:city_id',
    method: 'get',
    middlewares: [
      validate,
      cities.getCityById,
    ],
  },
  {
    location: '/cities/:city_id/weather',
    method: 'get',
    middlewares: [
      validate,
      cities.getWeatherByCityId,
    ],
  },
]);
<file_sep>/src/model/index.js
const ModelCity = require('./City');
const ModelWeather = require('./Weather');

module.exports = {
  ModelCity,
  ModelWeather,
};
<file_sep>/src/external/openWeather.js
module.exports = ({ axios, configApp: { openWeather: { appId } } }) => {
  const { get } = axios.create({
    baseURL: 'https://api.openweathermap.org/data/2.5/',
  });
  return {
    /**
       * https://openweathermap.org/current#cycle
       * @param lng length °
       * @param lat lat °
       * @returns {Promise<*>} resolves with raw weather data
       */
    getCitiesByLngLat: async (lng, lat) => {
      try {
        const { data } = await get('/find', {
          params: {
            lat,
            lon: lng,
            appid: appId,
          },
        });
        return data;
      } catch (err) {
        console.error('could not load city by lng lat');
        throw (err);
      }
    },

    /**
       *  https://openweathermap.org/current#cityid
       * @param id cityId
       * @returns {*}
       */
    getWeatherByCityId: async (id) => {
      try {
        const { data } = await get('/weather', {
          params: {
            id,
            appid: appId,
          },
        });
        return data;
      } catch (err) {
        console.error('could not load weather by city id');
        throw err;
      }
    },
  };
};
<file_sep>/src/model/Weather.js
/* eslint-disable camelcase */
module.exports = () => class Weather {
  static parseFromCityData({
    id, weather, sys: { sunrise, sunset }, main: {
      temp, pressure, humidity, temp_min, temp_max,
    }, clouds: { all }, wind: { speed },
  }) {
    return new Weather({
      cityId: id,
      weatherData: {
        type: weather[0].main,
        type_description: weather[0].description,
        sunrise: sunrise * 1000,
        sunset: sunset * 1000,
        temp,
        temp_min,
        temp_max,
        clouds_percent: all,
        wind_speed: speed,
        pressure,
        humidity,
      },
    });
  }

  constructor({
    cityId, weatherData: {
      type,
      type_description,
      sunrise,
      sunset,
      temp,
      temp_min,
      temp_max,
      pressure,
      humidity,
      clouds_percent,
      wind_speed,
    },
  }) {
    this.cityId = cityId;
    this.attributes = {
      type,
      type_description,
      sunrise,
      sunset,
      temp,
      temp_min,
      temp_max,
      pressure,
      humidity,
      clouds_percent,
      wind_speed,
    };
  }

  toJSON() {
    const attributes = { ...this.attributes };
    attributes.sunset = new Date(this.attributes.sunset).toISOString();
    attributes.sunrise = new Date(this.attributes.sunrise).toISOString();
    return attributes;
  }
};
<file_sep>/src/middleware/validate.js
/* eslint-disable no-case-declarations */
module.exports = ({ Joi, restifyErrors: { BadRequestError } }) => async (req, res, next) => {
  switch (true) {
    case /\/cities\/\d*\/weather/.test(req.url):
      break;
    case /\/cities\/\d*/.test(req.url):
      break;
    case /\/cities\?\w/.test(req.url) || /\/cities\/*/.test(req.url):
      const citiesShema = Joi.object({
        lng: Joi.number().required(),
        lat: Joi.number().required(),
      });
      const { error } = citiesShema.validate(req.query);
      if (error) {
        throw new BadRequestError('lat/lng required');
      }
      break;
    default:
      throw Error('not found');
  }
  return next();
};
<file_sep>/src/const/index.js
const configApp = require('./configApp');

module.exports = {
  configApp,
};
<file_sep>/src/middleware/cities.js
module.exports = ({ ModelCity }) => ({
  async getCityById(req, res, next) {
    const city = await ModelCity.getById(req.params.city_id);
    res.send(city.toDetailedJSON());
    return next();
  },
  async getCityByLanLng(req, res, next) {
    const { lat, lng } = req.query;
    const cities = (await ModelCity.listAllByLngLat(lng, lat)).map((city) => city.toJSON());
    if (cities.length === 0) {
      res.send(204, []);
    } else {
      res.send(cities);
    }
    return next();
  },

  async getWeatherByCityId(req, res, next) {
    const city = await ModelCity.getById(req.params.city_id);
    const weather = city.currentWeather;
    res.send(weather.toJSON());
    return next();
  },
});
<file_sep>/README.md
# stocard-be-challenge

## First steps

* install all dependencies and run app in hot refresh mode

```
yarn
yarn dev
```

## Architecture

* uses awilix container for dependency injections
* uses axios for request/response
* all dependencies are registered at server start
* create routes and add them dynamically to server
* each route contains multiple middlewares
* each middleware does one simple task
* outbound API implementations are located in ./external
* domain models are located in domains

## Testing

* jest with supertest

```
yarn test
```

## Linting

* eslint with air-bnb-base

```
yarn lint
```

##Commiting

* code quality tools running as precommit such as lintting, duplicate, tests and outdate deps

```
yarn commit
```

## Open tasks

* add proper logging (debug log every function, error log not catched errors, json output, context for loggers)
* improve func naming
* improve error handling (add error codes, own error class et)
* add swagger doc
* create server from swagger doc
* add more JSDoc

* create Dockerfile
* create docker-compose files
* create K8S files