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

feat(apiv6): implemented upload Changeset #14

Merged
merged 2 commits into from
Dec 3, 2020
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
57 changes: 55 additions & 2 deletions src/api/v6/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
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,
ChangesetNotFoundError,
OwnerMismatchError,
NotAllowedError,
ChangesetAlreadyClosedError,
MismatchChangesetError,
ChangesetOrDiffElementsNotFoundError,
ConflictErrorType,
} 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;

Expand Down Expand Up @@ -61,6 +65,55 @@ class Apiv6 {
}
}
}

public async uploadChangeset(id: number, data: string): Promise<string> {
let res: AxiosResponse<string>;
try {
res = await this.httpClient.post<string>(uploadChangesetEndPoint(id), data);
} catch (e) {
const axiosError = e as AxiosError<string>;
let error;

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;
}
}
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;
2 changes: 2 additions & 0 deletions src/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -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';
1 change: 1 addition & 0 deletions src/lib/endpoints.ts
Original file line number Diff line number Diff line change
@@ -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`;
16 changes: 16 additions & 0 deletions src/lib/errors.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand Down Expand Up @@ -28,6 +30,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);
Expand All @@ -48,3 +57,10 @@ export class NotAllowedError extends HttpErrorHandler {
Object.setPrototypeOf(this, NotAllowedError.prototype);
syncush marked this conversation as resolved.
Show resolved Hide resolved
}
}

export class MismatchChangesetError extends HttpErrorHandler {
public constructor(error: AxiosError) {
super(error);
Object.setPrototypeOf(this, MismatchChangesetError.prototype);
}
}
188 changes: 184 additions & 4 deletions tests/unit/apiv6.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down Expand Up @@ -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);
});
Expand All @@ -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");

Expand All @@ -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`);

Expand All @@ -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);

Expand All @@ -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 = `<?xml version="1.0" encoding="UTF-8"?>
<diffResult version="0.6" generator="OpenStreetMap server" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
<node old_id="57" new_id="57" new_version="2"/>
</diffResult>`;
nock(baseUrl).post(uploadChangesetEndPoint(changeSetNumber)).reply(200, mockRes);

const xmlData = `<osmChange version="0.6" generator="iD">
<create/>
<modify>
<node id="57" lon="34.957795931916124" lat="32.82084301679048" version="1" changeset=${changeSetNumber}/>
</modify>
<delete if-unused="true"/>
</osmChange>`;

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 = `<BAD version="0.6" generator="iD">
<create/>
<modify>
<node id="57" lon="34.957795931916124" lat="32.82084301679048" version="1" changeset=${changeSetNumber}/>
</modify>
<delete if-unused="true"/>
</BAD>`;
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 = `<osmChange version="0.6" generator="iD">
<create/>
<modify>
<node id="57" lon="34.957795931916124" lat="32.82084301679048" version="1" changeset=${changeSetNumber}/>
</modify>
<delete if-unused="true"/>
</osmChange>`;
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 = `<osmChange version="0.6" generator="iD">
<create/>
<modify>
<node id="57" lon="34.957795931916124" lat="32.82084301679048" version="1" changeset=${changeSetNumber}/>
</modify>
<delete if-unused="true"/>
</osmChange>`;
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 = `<osmChange version="0.6" generator="iD">
<create/>
<modify>
<node id="57" lon="34.957795931916124" lat="32.82084301679048" version="1" changeset=${changeSetNumber}/>
</modify>
<delete if-unused="true"/>
</osmChange>`;
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 = `<osmChange version="0.6" generator="iD">
<create/>
<modify>
<node id="57" lon="34.957795931916124" lat="32.82084301679048" version="1" changeset=${mismatchChangesetNumber}/>
</modify>
<delete if-unused="true"/>
</osmChange>`;
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 = `<osmChange version="0.6" generator="iD">
<create/>
<modify>
<node id="57" lon="34.957795931916124" lat="32.82084301679048" version="1" changeset=${changeSetNumber}/>
</modify>
<delete if-unused="true"/>
</osmChange>`;
try {
await apiv6.uploadChangeset(changeSetNumber, xmlData);
} catch (e) {
return expect(e).to.be.instanceof(UnauthorizedError);
}
throw new Error('should have thrown an error');
});
});
});
});
});
2 changes: 0 additions & 2 deletions tests/unit/config/tests-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};