Skip to content

Commit

Permalink
fix(carto): Clean up and add unit tests for requestWithParameters cac…
Browse files Browse the repository at this point in the history
…he (#8707)
  • Loading branch information
donmccurdy committed Mar 27, 2024
1 parent db26ab6 commit bd15b76
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 48 deletions.
5 changes: 0 additions & 5 deletions modules/carto/src/api/request-with-parameters.ts
Expand Up @@ -11,13 +11,11 @@ function encodeParameter(name: string, value: string | boolean | number): string

const REQUEST_CACHE = new Map<string, Promise<unknown>>();
export async function requestWithParameters<T = any>({
accessToken,
baseUrl,
parameters,
headers: customHeaders,
errorContext
}: {
accessToken?: string;
baseUrl: string;
parameters?: Record<string, string>;
headers: Record<string, string>;
Expand Down Expand Up @@ -47,9 +45,6 @@ export async function requestWithParameters<T = any>({
if (!response || !response.ok) {
throw new Error(json.error);
}
if (accessToken) {
json.accessToken = accessToken;
}
return json;
})
.catch((error: Error) => {
Expand Down
7 changes: 5 additions & 2 deletions modules/carto/src/sources/base-source.ts
Expand Up @@ -56,12 +56,15 @@ export async function baseSource<UrlParameters extends Record<string, string>>(
errorContext.requestType = 'Map data';

if (format === 'tilejson') {
return await requestWithParameters<TilejsonResult>({
accessToken,
const json = await requestWithParameters<TilejsonResult>({
baseUrl: dataUrl,
headers,
errorContext
});
if (accessToken) {
json.accessToken = accessToken;
}
return json;
}

return await requestWithParameters<GeojsonResult | JsonResult>({
Expand Down
103 changes: 103 additions & 0 deletions test/modules/carto/api/request-with-parameters.spec.ts
@@ -0,0 +1,103 @@
import test from 'tape-catch';
import {requestWithParameters} from '@deck.gl/carto/api/request-with-parameters';
import {withMockFetchMapsV3} from '../mock-fetch';
import {CartoAPIError} from '@deck.gl/carto';

test('requestWithParameters#cacheBaseURL', async t => {
await withMockFetchMapsV3(async calls => {
t.equals(calls.length, 0, '0 initial calls');

await Promise.all([
requestWithParameters({baseUrl: 'https://example.com/v1/baseURL', headers: {}}),
requestWithParameters({baseUrl: 'https://example.com/v2/baseURL', headers: {}}),
requestWithParameters({baseUrl: 'https://example.com/v2/baseURL', headers: {}})
]);

t.equals(calls.length, 2, '2 unique requests');
});
t.end();
});

test('requestWithParameters#cacheHeaders', async t => {
await withMockFetchMapsV3(async calls => {
t.equals(calls.length, 0, '0 initial calls');

await Promise.all([
requestWithParameters({baseUrl: 'https://example.com/v1/headers', headers: {a: 1}}),
requestWithParameters({baseUrl: 'https://example.com/v1/headers', headers: {a: 1}}),
requestWithParameters({baseUrl: 'https://example.com/v1/headers', headers: {b: 1}})
]);

t.equals(calls.length, 2, '2 unique requests');
});
t.end();
});

test('requestWithParameters#cacheParameters', async t => {
await withMockFetchMapsV3(async calls => {
t.equals(calls.length, 0, '0 initial calls');

await Promise.all([
requestWithParameters({
baseUrl: 'https://example.com/v1/params',
headers: {},
parameters: {}
}),
requestWithParameters({
baseUrl: 'https://example.com/v1/params',
headers: {},
parameters: {}
}),
requestWithParameters({
baseUrl: 'https://example.com/v1/params',
headers: {},
parameters: {a: 1}
})
]);

t.equals(calls.length, 2, '2 unique requests');
});
t.end();
});

test('requestWithParameters#nocacheErrorContext', async t => {
await withMockFetchMapsV3(
async calls => {
t.equals(calls.length, 0, '0 initial calls');

let error1: Error | undefined;
let error2: Error | undefined;

try {
await requestWithParameters({
baseUrl: 'https://example.com/v1/errorContext',
errorContext: {requestType: 'Map data'}
});
t.fail('request #1 should fail, but did not');
} catch (error) {
error1 = error as Error;
}

try {
await requestWithParameters({
baseUrl: 'https://example.com/v1/errorContext',
errorContext: {requestType: 'SQL'}
});
t.fail('request #2 should fail, but did not');
} catch (error) {
error2 = error as Error;
}

t.equals(calls.length, 2, '2 unique requests, failures not cached');
t.true(error1 instanceof CartoAPIError, 'error #1 type');
t.is((error1 as CartoAPIError).errorContext.requestType, 'Map data', 'error #1 context');
t.true(error2 instanceof CartoAPIError, 'error #2 type');
t.is((error2 as CartoAPIError).errorContext.requestType, 'SQL', 'error #2 context');
},
// @ts-ignore
(url: string, headers: HeadersInit) => {
return Promise.reject(new Error('404 Not Found'));
}
);
t.end();
});
1 change: 1 addition & 0 deletions test/modules/carto/index.ts
Expand Up @@ -2,6 +2,7 @@ import './api/carto-api-error.spec';
import './api/fetch-map.spec';
import './api/layer-map.spec';
import './api/parse-map.spec';
import './api/request-with-parameters.spec';
import './utils.spec';
import './layers/carto-vector-tile.spec';
import './layers/h3-tile-layer.spec';
Expand Down
94 changes: 53 additions & 41 deletions test/modules/carto/mock-fetch.ts
Expand Up @@ -46,55 +46,62 @@ export const TILESTATS_RESPONSE = {
type: 'Number'
};

const createDefaultResponse = (
url: string,
headers: HeadersInit,
cacheKey?: string
): Promise<unknown> => {
return Promise.resolve({
json: () => {
if (url.indexOf('format=tilejson') !== -1) {
return TILEJSON_RESPONSE;
}
if (url.indexOf('format=geojson') !== -1) {
return GEOJSON_RESPONSE;
}

if (url.indexOf('tileset') !== -1) {
return {
tilejson: {
url: [`https://xyz.com?format=tilejson&cache=${cacheKey}`]
}
};
}
if (url.indexOf('stats') !== -1) {
return TILESTATS_RESPONSE;
}
if (url.indexOf('query') !== -1 || url.indexOf('table')) {
return {
tilejson: {
url: [`https://xyz.com?format=tilejson&cache=${cacheKey}`]
},
geojson: {
url: [`https://xyz.com?format=geojson&cache=${cacheKey}`]
}
};
}
return null;
},
arrayBuffer: () => BINARY_TILE,
text: () => null, // Required to get loaders.gl to use arrayBuffer()
ok: true,
url,
headers: new Headers(headers)
});
};

async function setupMockFetchMapsV3(
responseFunc = createDefaultResponse,
cacheKey = btoa(Math.random().toFixed(4))
): Promise<MockFetchCall[]> {
const calls: MockFetchCall[] = [];

const mockFetch = (url: string, {headers}) => {
calls.push({url, headers});

if (url.indexOf('formatTiles=binary') !== -1) {
headers = {...headers, 'Content-Type': 'application/vnd.carto-vector-tile'};
}

return Promise.resolve({
json: () => {
if (url.indexOf('format=tilejson') !== -1) {
return TILEJSON_RESPONSE;
}
if (url.indexOf('format=geojson') !== -1) {
return GEOJSON_RESPONSE;
}

if (url.indexOf('tileset') !== -1) {
return {
tilejson: {
url: [`https://xyz.com?format=tilejson&cache=${cacheKey}`]
}
};
}
if (url.indexOf('stats') !== -1) {
return TILESTATS_RESPONSE;
}
if (url.indexOf('query') !== -1 || url.indexOf('table')) {
return {
tilejson: {
url: [`https://xyz.com?format=tilejson&cache=${cacheKey}`]
},
geojson: {
url: [`https://xyz.com?format=geojson&cache=${cacheKey}`]
}
};
}
return null;
},
arrayBuffer: () => BINARY_TILE,
text: () => null, // Required to get loaders.gl to use arrayBuffer()
ok: true,
url,
headers: new Headers(headers)
});
return responseFunc(url, headers, cacheKey);
};

globalThis.fetch = mockFetch as unknown as typeof fetch;
Expand All @@ -107,10 +114,15 @@ function teardownMockFetchMaps() {
}

export async function withMockFetchMapsV3(
testFunc: (calls: {url: string; headers: Record<string, unknown>}[]) => Promise<void>
testFunc: (calls: {url: string; headers: Record<string, unknown>}[]) => Promise<void>,
responseFunc: (
url: string,
headers: HeadersInit,
cacheKey?: string
) => Promise<unknown> = createDefaultResponse
): Promise<void> {
try {
const calls = await setupMockFetchMapsV3();
const calls = await setupMockFetchMapsV3(responseFunc);
await testFunc(calls);
} finally {
teardownMockFetchMaps();
Expand Down

0 comments on commit bd15b76

Please sign in to comment.