Skip to content
This repository has been archived by the owner on Apr 17, 2023. It is now read-only.

Commit

Permalink
Switch from moxios to axios-mock-adapter.
Browse files Browse the repository at this point in the history
I had assumed that moxios was well-supported since it's part
of the axios project on GitHub, but it's actually buggy and
unmaintained (axios/moxios#53). In
particular, I had tons of timing issues and problems getting
it to send proper responses while writing tests for the
Import view.
  • Loading branch information
derat committed Feb 5, 2020
1 parent a5b4e87 commit 5bb63ef
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 108 deletions.
24 changes: 9 additions & 15 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions package.json
Expand Up @@ -25,7 +25,6 @@
"devDependencies": {
"@types/jest": "^24.0.19",
"@types/lodash": "^4.14.149",
"@types/moxios": "^0.4.9",
"@vue/cli-plugin-babel": "^4.1.0",
"@vue/cli-plugin-eslint": "^4.1.0",
"@vue/cli-plugin-router": "^4.1.0",
Expand All @@ -35,11 +34,11 @@
"@vue/eslint-config-prettier": "^5.0.0",
"@vue/eslint-config-typescript": "^4.0.0",
"@vue/test-utils": "1.0.0-beta.29",
"axios-mock-adapter": "^1.17.0",
"eslint": "^5.16.0",
"eslint-plugin-prettier": "^3.1.1",
"eslint-plugin-vue": "^5.0.0",
"flush-promises": "^1.0.2",
"moxios": "^0.4.0",
"prettier": "^1.19.1",
"sass": "^1.19.0",
"sass-loader": "^8.0.0",
Expand Down
115 changes: 51 additions & 64 deletions src/api.test.ts
Expand Up @@ -2,27 +2,31 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import moxios from 'moxios';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';

import {
ApiRoute,
ApiTick,
getRoutes,
getRoutesUrl,
getTicks,
makeGetRoutesUrl,
makeGetTicksUrl,
getTicksUrl,
maxRoutesPerRequest,
} from './api';

// Arbitrary data to use in tests.
const email = 'user@example.org';
const apiKey = 'secret123';
const key = 'secret123';

const mockAxios = new MockAdapter(axios);
afterAll(() => {
mockAxios.restore();
});

describe('getTicks', () => {
beforeEach(() => {
moxios.install();
});
afterEach(() => {
moxios.uninstall();
mockAxios.reset();
});

// Returns |count| ApiTicks starting at the supplied tick ID and counting
Expand All @@ -45,80 +49,69 @@ describe('getTicks', () => {
});
}

// Waits for the next HTTP request. Asserts that it is a get-ticks request
// with the supplied |startPos| parameter and then returns |count| ticks
// starting with id |startId|.
function replyToGetTicks(startPos: number, startId: number, count: number) {
const req = moxios.requests.mostRecent();
expect(req.url).toEqual(makeGetTicksUrl(email, apiKey, startPos));
return req.respondWith({
status: 200,
response: {
// Sets a response for a get-ticks request with the supplied |startPos|
// parameter. Will return |count| ticks starting with id |startId|.
function handleGetTicks(startPos: number, startId: number, count: number) {
mockAxios
.onGet(getTicksUrl, { params: { email, key, startPos } })
.replyOnce(200, {
hardest: '',
average: '',
ticks: createTicks(startId, count),
success: true,
},
});
});
}

it('handles not receiving any ticks', done => {
getTicks(email, apiKey).then(ticks => {
handleGetTicks(0, -1, 0);
getTicks(email, key).then(ticks => {
expect(ticks).toEqual([]);
done();
});
moxios.wait(() => replyToGetTicks(0, -1, 0));
});

it('returns a single set of ticks', done => {
getTicks(email, apiKey).then(ticks => {
handleGetTicks(0, 100, 3);
handleGetTicks(3, -1, 0);
getTicks(email, key).then(ticks => {
expect(ticks).toEqual(createTicks(100, 3));
done();
});
moxios.wait(() => {
replyToGetTicks(0, 100, 3).then(() => replyToGetTicks(3, -1, 0));
});
});

it('aggregates multiple sets of ticks', done => {
getTicks(email, apiKey).then(ticks => {
// Return 3 ticks, then 3 more, and then 1 final tick.
handleGetTicks(0, 100, 3);
handleGetTicks(3, 97, 3);
handleGetTicks(6, 94, 1);
handleGetTicks(7, -1, 0);
getTicks(email, key).then(ticks => {
expect(ticks).toEqual(createTicks(100, 7));
done();
});
moxios.wait(() => {
// Return 3 ticks, then 3 more, and then 1 final tick.
replyToGetTicks(0, 100, 3)
.then(() => replyToGetTicks(3, 97, 3))
.then(() => replyToGetTicks(6, 94, 1))
.then(() => replyToGetTicks(7, -1, 0));
});
});

it("doesn't return already-seen ticks", done => {
getTicks(email, apiKey, 97 /* minTickId */).then(ticks => {
handleGetTicks(0, 100, 3);
handleGetTicks(3, 97, 3);
getTicks(email, key, 97 /* minTickId */).then(ticks => {
expect(ticks).toEqual(createTicks(100, 4));
done();
});
moxios.wait(() => {
replyToGetTicks(0, 100, 3).then(() => replyToGetTicks(3, 97, 3));
});
});

it("doesn't return anything if all ticks have been seen", done => {
getTicks(email, apiKey, 101 /* minTickId */).then(ticks => {
handleGetTicks(0, 100, 3);
getTicks(email, key, 101 /* minTickId */).then(ticks => {
expect(ticks).toEqual([]);
done();
});
moxios.wait(() => replyToGetTicks(0, 100, 3));
});
});

describe('getRoutes', () => {
beforeEach(() => {
moxios.install();
});
afterEach(() => {
moxios.uninstall();
mockAxios.reset();
});

// Returns an ApiRoute with supplied route ID and containing arbitrary but
Expand All @@ -143,50 +136,44 @@ describe('getRoutes', () => {
};
}

// Waits for the next HTTP request. Asserts that it is a get-routes request
// with the supplied |routeIds| parameter and then returns the requested
// routes.
function replyToGetRoutes(routeIds: number[]) {
const req = moxios.requests.mostRecent();
expect(req.url).toEqual(makeGetRoutesUrl(routeIds, apiKey));
return req.respondWith({
status: 200,
response: {
// Sets a response for a get-routes request with the supplied |routeIds|
// parameter. Will return the requested routes.
function handleGetRoutes(routeIds: number[]) {
mockAxios
.onGet(getRoutesUrl, { params: { key, routeIds: routeIds.join(',') } })
.replyOnce(200, {
routes: routeIds.map(id => createRoute(id)),
success: true,
},
});
});
}

it('returns a single set of routes', done => {
const ids = [123, 456, 789];
getRoutes(ids, apiKey).then(routes => {
handleGetRoutes(ids);
getRoutes(ids, key).then(routes => {
expect(routes).toEqual(ids.map(id => createRoute(id)));
done();
});
moxios.wait(() => replyToGetRoutes(ids));
});

it('uses a single request when possible', done => {
const ids = [...Array(maxRoutesPerRequest).keys()].map(i => i + 1);
getRoutes(ids, apiKey).then(routes => {
handleGetRoutes(ids);
getRoutes(ids, key).then(routes => {
expect(routes).toEqual(ids.map(id => createRoute(id)));
done();
});
moxios.wait(() => replyToGetRoutes(ids));
});

it('uses multiple requests when needed', done => {
const max = maxRoutesPerRequest;
const ids = [...Array(2 * max + 10).keys()].map(i => i + 1);
getRoutes(ids, apiKey).then(routes => {
handleGetRoutes(ids.slice(0, max));
handleGetRoutes(ids.slice(max, 2 * max));
handleGetRoutes(ids.slice(2 * max));
getRoutes(ids, key).then(routes => {
expect(routes).toEqual(ids.map(id => createRoute(id)));
done();
});
moxios.wait(() => {
replyToGetRoutes(ids.slice(0, max))
.then(() => replyToGetRoutes(ids.slice(max, 2 * max)))
.then(() => replyToGetRoutes(ids.slice(2 * max)));
});
});
});
49 changes: 22 additions & 27 deletions src/api.ts
Expand Up @@ -5,18 +5,8 @@
import axios from 'axios';

// Exposed for unit tests.
export function makeGetTicksUrl(email: string, key: string, startPos: number) {
return (
'https://www.mountainproject.com/data/get-ticks' +
`?email=${email}&key=${key}&startPos=${startPos}`
);
}
export function makeGetRoutesUrl(routeIds: number[], key: string) {
return (
'https://www.mountainproject.com/data/get-routes' +
`?routeIds=${routeIds.join(',')}&key=${key}`
);
}
export const getTicksUrl = 'https://www.mountainproject.com/data/get-ticks';
export const getRoutesUrl = 'https://www.mountainproject.com/data/get-routes';

// A single tick returned by the get-ticks API endpoint.
export interface ApiTick {
Expand Down Expand Up @@ -48,22 +38,24 @@ export function getTicks(
minTickId: number = 0,
ticks: ApiTick[] = []
): Promise<ApiTick[]> {
return axios.get(makeGetTicksUrl(email, key, ticks.length)).then(response => {
const result = (response.data as unknown) as GetTicksResult;
if (!result.success) throw new Error('API reported failure');
return axios
.get(getTicksUrl, { params: { email, key, startPos: ticks.length } })
.then(response => {
const result = (response.data as unknown) as GetTicksResult;
if (!result.success) throw new Error('API reported failure');

// If we're at the end of the list, quit.
if (!result.ticks.length) return ticks;
// If we're at the end of the list, quit.
if (!result.ticks.length) return ticks;

for (const tick of result.ticks) {
// If we got all of the new ticks, quit.
if (tick.tickId < minTickId) return ticks;
ticks.push(tick);
}
for (const tick of result.ticks) {
// If we got all of the new ticks, quit.
if (tick.tickId < minTickId) return ticks;
ticks.push(tick);
}

// Recurse to get additional ticks.
return getTicks(email, key, minTickId, ticks);
});
// Recurse to get additional ticks.
return getTicks(email, key, minTickId, ticks);
});
}

// A single route returned by the get-routes API endpoint.
Expand Down Expand Up @@ -106,8 +98,11 @@ export function getRoutes(
): Promise<ApiRoute[]> {
if (routeIds.length == 0) return Promise.resolve([]);

const url = makeGetRoutesUrl(routeIds.slice(0, maxRoutesPerRequest), key);
return axios.get(url).then(response => {
const params = {
key,
routeIds: routeIds.slice(0, maxRoutesPerRequest).join(','),
};
return axios.get(getRoutesUrl, { params }).then(response => {
const result = (response.data as unknown) as GetRoutesResult;
if (!result.success) throw new Error('API reported failure');

Expand Down

0 comments on commit 5bb63ef

Please sign in to comment.