From aaf282e6bfb9471b3b099a5a0c9f41db83c30ca4 Mon Sep 17 00:00:00 2001 From: galta <48960890+galta95@users.noreply.github.com> Date: Thu, 3 Dec 2020 14:33:26 +0200 Subject: [PATCH 1/2] feat(apiv6): implemented upload Changeset --- src/api/v6/index.ts | 37 +++++- src/lib/constants.ts | 2 + src/lib/endpoints.ts | 1 + src/lib/errors.ts | 14 +++ tests/unit/apiv6.test.ts | 188 +++++++++++++++++++++++++++++- tests/unit/config/tests-config.ts | 2 - 6 files changed, 236 insertions(+), 8 deletions(-) diff --git a/src/api/v6/index.ts b/src/api/v6/index.ts index 6ff3962..7de543d 100644 --- a/src/api/v6/index.ts +++ b/src/api/v6/index.ts @@ -1,6 +1,6 @@ import axios, { AxiosError, AxiosInstance, AxiosResponse } from 'axios'; import StatusCodes from 'http-status-codes'; -import { createChangesetEndPoint, closeChangesetEndPoint } from '../../lib/endpoints'; +import { createChangesetEndPoint, closeChangesetEndPoint, uploadChangesetEndPoint } from '../../lib/endpoints'; import { UnauthorizedError, BadXmlError, @@ -8,8 +8,10 @@ import { OwnerMismatchError, NotAllowedError, ChangesetAlreadyClosedError, + MismatchChangesetError, + ChangesetOrDiffElementsNotFoundError, } from '../../lib/errors'; -import { OWNER_MISMATCH } from '../../lib/constants'; +import { OWNER_MISMATCH, CHANGESET_MISMATCH, CHANGESET_ALREADY_CLOSED } from '../../lib/constants'; class Apiv6 { private readonly httpClient: AxiosInstance; @@ -61,6 +63,37 @@ class Apiv6 { } } } + + public async uploadChangeset(id: number, data: string): Promise { + let res: AxiosResponse; + try { + res = await this.httpClient.post(uploadChangesetEndPoint(id), data); + } catch (e) { + const axiosError = e as AxiosError; + + if (axiosError.response?.status === StatusCodes.BAD_REQUEST) { + throw new BadXmlError(axiosError); + } else if (axiosError.response?.status === StatusCodes.NOT_FOUND) { + throw new ChangesetOrDiffElementsNotFoundError(axiosError); + } else if (axiosError.response?.status === StatusCodes.CONFLICT) { + if (axiosError.response?.data.includes(CHANGESET_ALREADY_CLOSED) === true) { + throw new ChangesetAlreadyClosedError(axiosError); + } else if (axiosError.response?.data.includes(OWNER_MISMATCH) === true) { + throw new OwnerMismatchError(axiosError); + } else if (axiosError.response?.data.includes(CHANGESET_MISMATCH) === true) { + throw new MismatchChangesetError(axiosError); + } else { + throw new Error(e); + } + } else if (axiosError.response?.status === StatusCodes.UNAUTHORIZED) { + throw new UnauthorizedError(axiosError); + } else { + throw new Error(e); + } + } + const { data: diffResult } = res; + return diffResult; + } } export default Apiv6; diff --git a/src/lib/constants.ts b/src/lib/constants.ts index f1ee6c0..d6d9311 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1 +1,3 @@ export const OWNER_MISMATCH = "The user doesn't own that changeset"; +export const CHANGESET_MISMATCH = 'Changeset mismatch'; +export const CHANGESET_ALREADY_CLOSED = 'was closed'; diff --git a/src/lib/endpoints.ts b/src/lib/endpoints.ts index 7e50ff8..9aa9975 100644 --- a/src/lib/endpoints.ts +++ b/src/lib/endpoints.ts @@ -1,2 +1,3 @@ export const createChangesetEndPoint = '/api/0.6/changeset/create'; export const closeChangesetEndPoint = (id: number): string => `/api/0.6/changeset/${id}/close`; +export const uploadChangesetEndPoint = (id: number): string => `/api/0.6/changeset/${id}/upload`; diff --git a/src/lib/errors.ts b/src/lib/errors.ts index 1fe981a..e0d1e64 100644 --- a/src/lib/errors.ts +++ b/src/lib/errors.ts @@ -28,6 +28,13 @@ export class ChangesetNotFoundError extends HttpErrorHandler { } } +export class ChangesetOrDiffElementsNotFoundError extends HttpErrorHandler { + public constructor(error: AxiosError) { + super(error); + Object.setPrototypeOf(this, ChangesetOrDiffElementsNotFoundError.prototype); + } +} + export class ChangesetAlreadyClosedError extends HttpErrorHandler { public constructor(error: AxiosError) { super(error); @@ -48,3 +55,10 @@ export class NotAllowedError extends HttpErrorHandler { Object.setPrototypeOf(this, NotAllowedError.prototype); } } + +export class MismatchChangesetError extends HttpErrorHandler { + public constructor(error: AxiosError) { + super(error); + Object.setPrototypeOf(this, MismatchChangesetError.prototype); + } +} diff --git a/tests/unit/apiv6.test.ts b/tests/unit/apiv6.test.ts index fbbb2dc..322876d 100644 --- a/tests/unit/apiv6.test.ts +++ b/tests/unit/apiv6.test.ts @@ -2,10 +2,18 @@ import { expect } from 'chai'; import nock = require('nock'); import Apiv6 from '../../src/index'; -import { createChangesetEndPoint, closeChangesetEndPoint } from '../../src/lib/endpoints'; -import { UnauthorizedError, BadXmlError, ChangesetNotFoundError, ChangesetAlreadyClosedError, OwnerMismatchError } from '../../src/lib/errors'; +import { createChangesetEndPoint, closeChangesetEndPoint, uploadChangesetEndPoint } from '../../src/lib/endpoints'; +import { + UnauthorizedError, + BadXmlError, + ChangesetNotFoundError, + ChangesetAlreadyClosedError, + OwnerMismatchError, + MismatchChangesetError, + ChangesetOrDiffElementsNotFoundError, +} from '../../src/lib/errors'; import { testConf } from './config/tests-config'; -const { baseUrl, username, password, changeSetNumber } = testConf; +const { baseUrl, username, password } = testConf; describe('apiv6', function () { describe('/changeset/create', function () { @@ -70,13 +78,16 @@ describe('apiv6', function () { }); }); }); - describe('/changeset/{id}/close}', function () { + + describe('/changeset/#id/close', function () { describe('happy flow', function () { describe('with opened changeset', function () { it('should close the changset', async function () { const apiv6 = new Apiv6(baseUrl, username, password); + const changeSetNumber = 12; nock(baseUrl).put(closeChangesetEndPoint(changeSetNumber)).reply(200); + const res = await apiv6.closeChangeset(changeSetNumber); expect(res).to.be.equal(undefined); }); @@ -86,6 +97,7 @@ describe('apiv6', function () { describe('with mismatch user', function () { it('should return OwnerMismatchError', async function () { const apiv6 = new Apiv6(baseUrl, username, password); + const changeSetNumber = 12; nock(baseUrl).put(closeChangesetEndPoint(changeSetNumber)).reply(409, "The user doesn't own that changeset"); @@ -100,6 +112,7 @@ describe('apiv6', function () { describe('with already closed changeset', function () { it('should return ChangesetAlreadyClosedError', async function () { const apiv6 = new Apiv6(baseUrl, username, password); + const changeSetNumber = 12; nock(baseUrl).put(closeChangesetEndPoint(changeSetNumber)).reply(409, `changeset ${changeSetNumber} was closed at`); @@ -117,6 +130,7 @@ describe('apiv6', function () { describe('with not exsits changeset', function () { it('should return ChangesetNotFoundError', async function () { const apiv6 = new Apiv6(baseUrl, username, password); + const changeSetNumber = 12; nock(baseUrl).put(closeChangesetEndPoint(changeSetNumber)).reply(404); @@ -130,4 +144,170 @@ describe('apiv6', function () { }); }); }); + + describe('/changeset/#id/upload', function () { + describe('happy flow', function () { + describe('with valid osm change', function () { + it('should return 200 and diff result', async function () { + const apiv6 = new Apiv6(baseUrl, username, password); + const changeSetNumber = 12; + + const mockRes = ` + + + `; + nock(baseUrl).post(uploadChangesetEndPoint(changeSetNumber)).reply(200, mockRes); + + const xmlData = ` + + + + + + `; + + const res = await apiv6.uploadChangeset(changeSetNumber, xmlData); + + expect(res).to.be.equal(mockRes); + }); + }); + }); + describe('sad flow', function () { + describe('with bad xml', function () { + it('should return BadXmlError', async function () { + const apiv6 = new Apiv6(baseUrl, username, password); + const changeSetNumber = 12; + + nock(baseUrl).post(uploadChangesetEndPoint(changeSetNumber)).reply(400, "Document element should be 'osmChange'"); + + const xmlData = ` + + + + + + `; + try { + await apiv6.uploadChangeset(changeSetNumber, xmlData); + } catch (e) { + return expect(e).to.be.instanceof(BadXmlError); + } + throw new Error('should have thrown an error'); + }); + }); + describe('with not exsits changeset', function () { + it('should return ChangesetOrDiffElementsNotFoundError', async function () { + const apiv6 = new Apiv6(baseUrl, username, password); + const changeSetNumber = 7000; + + nock(baseUrl).post(uploadChangesetEndPoint(changeSetNumber)).reply(404); + + const xmlData = ` + + + + + + `; + try { + await apiv6.uploadChangeset(changeSetNumber, xmlData); + } catch (e) { + return expect(e).to.be.instanceof(ChangesetOrDiffElementsNotFoundError); + } + throw new Error('should have thrown an error'); + }); + }); + describe('with already closed changeset', function () { + it('should return ChangesetAlreadyClosedError', async function () { + const apiv6 = new Apiv6(baseUrl, username, password); + const changeSetNumber = 12; + + nock(baseUrl).post(uploadChangesetEndPoint(changeSetNumber)).reply(409, `changeset ${changeSetNumber} was closed at`); + + const xmlData = ` + + + + + + `; + try { + await apiv6.uploadChangeset(changeSetNumber, xmlData); + } catch (e) { + return expect(e).to.be.instanceof(ChangesetAlreadyClosedError); + } + throw new Error('should have thrown an error'); + }); + }); + describe('with mismatch user', function () { + it('should return OwnerMismatchError', async function () { + const apiv6 = new Apiv6(baseUrl, username, password); + const changeSetNumber = 12; + + nock(baseUrl).post(uploadChangesetEndPoint(changeSetNumber)).reply(409, "The user doesn't own that changeset"); + + const xmlData = ` + + + + + + `; + try { + await apiv6.uploadChangeset(changeSetNumber, xmlData); + } catch (e) { + return expect(e).to.be.instanceof(OwnerMismatchError); + } + throw new Error('should have thrown an error'); + }); + }); + describe('with mismatch changeset', function () { + it('should return MismatchChangesetError', async function () { + const apiv6 = new Apiv6(baseUrl, username, password); + const changeSetNumber = 12; + const mismatchChangesetNumber = 13; + + nock(baseUrl) + .post(uploadChangesetEndPoint(changeSetNumber)) + .reply(409, `Changeset mismatch: Provided ${mismatchChangesetNumber} but only ${changeSetNumber} is allowed`); + + const xmlData = ` + + + + + + `; + try { + await apiv6.uploadChangeset(changeSetNumber, xmlData); + } catch (e) { + return expect(e).to.be.instanceof(MismatchChangesetError); + } + throw new Error('should have thrown an error'); + }); + }); + describe('with unregisterd user', function () { + it('should return UnauthorizedError', async function () { + const apiv6 = new Apiv6(baseUrl, username, password); + const changeSetNumber = 12; + + nock(baseUrl).post(uploadChangesetEndPoint(changeSetNumber)).reply(401, "Couldn't authenticate you"); + + const xmlData = ` + + + + + + `; + try { + await apiv6.uploadChangeset(changeSetNumber, xmlData); + } catch (e) { + return expect(e).to.be.instanceof(UnauthorizedError); + } + throw new Error('should have thrown an error'); + }); + }); + }); + }); }); diff --git a/tests/unit/config/tests-config.ts b/tests/unit/config/tests-config.ts index 1aab28d..8115f35 100644 --- a/tests/unit/config/tests-config.ts +++ b/tests/unit/config/tests-config.ts @@ -2,10 +2,8 @@ export const testConf: { baseUrl: string; username: string; password: string; - changeSetNumber: number; } = { baseUrl: 'http://test.com:8080', username: 'USERNAME', password: 'PASSWORD', - changeSetNumber: 12, }; From efac1dba56d54fb2e4d2ad6a36d1047c49039be8 Mon Sep 17 00:00:00 2001 From: galta <48960890+galta95@users.noreply.github.com> Date: Thu, 3 Dec 2020 16:27:23 +0200 Subject: [PATCH 2/2] refactor(apiv06): switch case in upload route instead of if else --- src/api/v6/index.ts | 56 ++++++++++++++++++++++++++++++--------------- src/lib/errors.ts | 2 ++ 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/src/api/v6/index.ts b/src/api/v6/index.ts index 7de543d..4e369aa 100644 --- a/src/api/v6/index.ts +++ b/src/api/v6/index.ts @@ -10,8 +10,10 @@ import { ChangesetAlreadyClosedError, MismatchChangesetError, ChangesetOrDiffElementsNotFoundError, + ConflictErrorType, } from '../../lib/errors'; import { OWNER_MISMATCH, CHANGESET_MISMATCH, CHANGESET_ALREADY_CLOSED } from '../../lib/constants'; + class Apiv6 { private readonly httpClient: AxiosInstance; @@ -69,31 +71,49 @@ class Apiv6 { try { res = await this.httpClient.post(uploadChangesetEndPoint(id), data); } catch (e) { - const axiosError = e as AxiosError; + const axiosError = e as AxiosError; + let error; - if (axiosError.response?.status === StatusCodes.BAD_REQUEST) { - throw new BadXmlError(axiosError); - } else if (axiosError.response?.status === StatusCodes.NOT_FOUND) { - throw new ChangesetOrDiffElementsNotFoundError(axiosError); - } else if (axiosError.response?.status === StatusCodes.CONFLICT) { - if (axiosError.response?.data.includes(CHANGESET_ALREADY_CLOSED) === true) { - throw new ChangesetAlreadyClosedError(axiosError); - } else if (axiosError.response?.data.includes(OWNER_MISMATCH) === true) { - throw new OwnerMismatchError(axiosError); - } else if (axiosError.response?.data.includes(CHANGESET_MISMATCH) === true) { - throw new MismatchChangesetError(axiosError); - } else { - throw new Error(e); + switch (axiosError.response?.status) { + case StatusCodes.BAD_REQUEST: { + error = new BadXmlError(axiosError); + break; + } + case StatusCodes.NOT_FOUND: { + error = new ChangesetOrDiffElementsNotFoundError(axiosError); + break; + } + case StatusCodes.CONFLICT: { + const data = axiosError.response.data; + error = this.conflictErrorFactory(data, axiosError); + break; + } + case StatusCodes.UNAUTHORIZED: { + error = new UnauthorizedError(axiosError); + break; + } + default: { + error = new Error(e); + break; } - } else if (axiosError.response?.status === StatusCodes.UNAUTHORIZED) { - throw new UnauthorizedError(axiosError); - } else { - throw new Error(e); } + throw error; } const { data: diffResult } = res; return diffResult; } + + private conflictErrorFactory(data: string, axiosError: AxiosError): ConflictErrorType { + if (data.includes(CHANGESET_ALREADY_CLOSED)) { + return new ChangesetAlreadyClosedError(axiosError); + } else if (data.includes(OWNER_MISMATCH)) { + return new OwnerMismatchError(axiosError); + } else if (data.includes(CHANGESET_MISMATCH)) { + return new MismatchChangesetError(axiosError); + } else { + return new Error((axiosError as unknown) as string); + } + } } export default Apiv6; diff --git a/src/lib/errors.ts b/src/lib/errors.ts index e0d1e64..0ffc8d0 100644 --- a/src/lib/errors.ts +++ b/src/lib/errors.ts @@ -1,5 +1,7 @@ import { AxiosError } from 'axios'; +export type ConflictErrorType = ChangesetAlreadyClosedError | MismatchChangesetError | ChangesetAlreadyClosedError | Error; + class HttpErrorHandler extends Error { public constructor(error: AxiosError) { super(error.response?.data);