Skip to content
Snippets Groups Projects
Commit f61015d5 authored by Stephanie Freitag's avatar Stephanie Freitag
Browse files

Merge branch 'HOLI-9693-geolocation-lookup-retry' into 'main'

HOLI-9693: retry geometry lookup using place name when receiving point

See merge request app/holi-app-volunteering!46
parents 0ab41375 0a30df53
No related branches found
No related tags found
No related merge requests found
......@@ -3,14 +3,20 @@ import { GraphQLServer } from "./server.ts";
export type ResponsePayload = Record<string, unknown> | Array<unknown> | Error;
export const stubFetch = (response: ResponsePayload) => {
export const stubFetchWithResponses = (responses: ResponsePayload[]) => {
return stub(
globalThis,
"fetch",
returnsNext([Promise.resolve(new Response(JSON.stringify(response)))]),
returnsNext(
responses.map((response) => Promise.resolve(new Response(JSON.stringify(response)))),
),
);
};
export const stubFetch = (response: ResponsePayload) => {
return stubFetchWithResponses([response]);
};
export const processGqlRequest = (
graphQLServer: GraphQLServer,
query: string,
......
export {
assertSpyCall,
assertSpyCalls,
restore,
returnsNext,
type Spy,
spy,
type Stub,
stub,
} from "https://deno.land/std@0.155.0/testing/mock.ts";
export type { Stub } from "https://deno.land/std@0.155.0/testing/mock.ts";
export { assertEquals, assertExists, assertRejects } from "https://deno.land/std@0.155.0/testing/asserts.ts";
export { afterEach, describe, it } from "https://deno.land/std@0.155.0/testing/bdd.ts";
export { beforeEach, describe, it } from "https://deno.land/std@0.155.0/testing/bdd.ts";
import { GeolocationCoordinates, GeolocationGeometry } from "./types.ts";
import { GeolocationCoordinates, GeolocationGeometry, GeolocationProperties } from "./types.ts";
import { logger } from "./logging.ts";
type GeoAPIResponse = {
data?: {
placeDetails?: {
geolocation?: {
properties?: Partial<GeolocationCoordinates>;
properties?: Partial<GeolocationProperties>;
geometry?: Partial<GeolocationGeometry>;
};
};
};
};
type GeoAPIPlace = {
id?: string;
};
type GeoAPIAutocompleteResponse = {
data?: {
placesAutocomplete?: GeoAPIPlace[];
};
};
export class GeoAPIClient {
private readonly geoAPIEndpointUrl: string;
......@@ -33,6 +44,21 @@ export class GeoAPIClient {
return response.json() as GeoAPIResponse;
}
private async resolvePlaceIdByName(name: string): Promise<string | undefined> {
const graphQLQuery = `{ placesAutocomplete(text: "${name}", limit: 1) { id } }`;
const response = await fetch(this.geoAPIEndpointUrl, {
"body": JSON.stringify({ query: graphQLQuery }),
"headers": {
"Accept": "application/graphql-response+json, application/json",
"Content-Type": "application/json",
},
"method": "POST",
});
const { data } = await response.json() as GeoAPIAutocompleteResponse;
const [place] = data?.placesAutocomplete || [];
return place?.id;
}
async resolveCoordinates(
geolocationId: string,
): Promise<GeolocationCoordinates> {
......@@ -50,11 +76,22 @@ export class GeoAPIClient {
async resolveGeometry(
geolocationId: string,
{
allowPoints = false,
}: { allowPoints?: boolean } = {},
): Promise<GeolocationGeometry> {
const responseJSON = await this.resolveGeolocationViaGeoAPI(geolocationId);
const { type, coordinates } = responseJSON.data?.placeDetails?.geolocation
?.geometry || {};
const { geometry, properties } = responseJSON.data?.placeDetails?.geolocation || {};
const { type, coordinates } = geometry || {};
if (type && coordinates) {
const locationName = properties?.formatted;
if (type === "Point" && !allowPoints && locationName) {
logger.debug(`Retrieved point as geolocation, will retry with lookup for location ${locationName}`);
const placeId = await this.resolvePlaceIdByName(locationName);
if (placeId && placeId !== geolocationId) {
return this.resolveGeometry(placeId, { allowPoints: true });
}
}
return { type, coordinates };
} else {
return Promise.reject(
......
import { stubFetch, stubFetchWithResponses } from "./common_test.ts";
import { assertRejects, restore } from "./dev_deps.ts";
import { assertEquals, beforeEach, describe, it } from "./dev_deps.ts";
import { GeoAPIClient } from "./geo_api_client.ts";
const testEndpointUrl = "https://endpoint.geo";
const testPlaceId = "place123";
const geometryMultipolygon = {
"type": "MultiPolygon",
"coordinates": [
[
[
[8.1044993, 54.025049999],
[8.2109892, 53.980715499],
],
],
],
};
const geometryPoint = {
type: "Point",
coordinates: [10.000654, 53.550340999],
};
const geolocationProperties = {
formatted: "Hamburg, Germany",
};
const geometryResponseMultipolygon = {
data: {
placeDetails: {
geolocation: {
properties: geolocationProperties,
geometry: geometryMultipolygon,
},
},
},
};
const geometryResponsePoint = {
data: {
placeDetails: {
geolocation: {
properties: geolocationProperties,
geometry: geometryPoint,
},
},
},
};
const autocompleteResponse = {
data: {
placesAutocomplete: [{
id: "place1234",
}],
},
};
const invalidResponse = {};
describe("GeoAPIClient", () => {
let client: GeoAPIClient;
beforeEach(() => {
restore();
client = new GeoAPIClient(testEndpointUrl);
});
describe("resolveGeometry", () => {
it("fetches geometry for place id", async () => {
stubFetch(geometryResponseMultipolygon);
const response = await client.resolveGeometry(testPlaceId);
assertEquals(response, geometryMultipolygon);
});
it("throws error for invalid response when fetching geometry", async () => {
stubFetch(invalidResponse);
await assertRejects(() => client.resolveGeometry(testPlaceId));
});
it("retries fetching geometry if geometry is only a point", async () => {
stubFetchWithResponses([geometryResponsePoint, autocompleteResponse, geometryResponseMultipolygon]);
const response = await client.resolveGeometry(testPlaceId);
assertEquals(response, geometryMultipolygon);
});
it("does not retry fetching geometry if points are allowed", async () => {
stubFetchWithResponses([geometryResponsePoint]);
const response = await client.resolveGeometry(testPlaceId, { allowPoints: true });
assertEquals(response, geometryPoint);
});
it("only retries fetching geometry if geometry is only a point once", async () => {
stubFetchWithResponses([geometryResponsePoint, autocompleteResponse, geometryResponsePoint]);
const response = await client.resolveGeometry(testPlaceId);
assertEquals(response, geometryPoint);
});
it("only retries fetching geometry if autocomplete request results in a different place", async () => {
const autocompleteResponseSamePlaceId = [{ id: testPlaceId }];
stubFetchWithResponses([geometryResponsePoint, autocompleteResponseSamePlaceId]);
const response = await client.resolveGeometry(testPlaceId);
assertEquals(response, geometryPoint);
});
it("handles empty autocomplete response when retrying fetching geometry", async () => {
const emptyAutocompleteResponse = { data: { placesAutocomplete: [] } };
stubFetchWithResponses([geometryResponsePoint, emptyAutocompleteResponse]);
const response = await client.resolveGeometry(testPlaceId);
assertEquals(response, geometryPoint);
});
it("handles invalid autocomplete response when retrying fetching geometry", async () => {
stubFetchWithResponses([geometryResponsePoint, invalidResponse]);
const response = await client.resolveGeometry(testPlaceId);
assertEquals(response, geometryPoint);
});
it("throws error for invalid geometry response when retrying fetching geometry", async () => {
stubFetchWithResponses([geometryResponsePoint, autocompleteResponse, invalidResponse]);
await assertRejects(() => client.resolveGeometry(testPlaceId));
});
});
});
......@@ -80,8 +80,12 @@ export type GeolocationCoordinates = {
lon: number;
};
export type GeolocationProperties = {
formatted: string;
} & GeolocationCoordinates;
export type GeolocationGeoJSON = {
properties: GeolocationCoordinates;
properties: GeolocationProperties;
geometry: GeolocationGeometry;
};
......
import {
afterEach,
assertEquals,
assertRejects,
assertSpyCall,
beforeEach,
describe,
it,
returnsNext,
......@@ -94,7 +94,7 @@ const queryEngagement = async (
const queryCategories = async (
graphQLServer: GraphQLServer,
): Promise<CategoriesResponse> => {
const promise = processGqlRequest(
const response = await processGqlRequest(
graphQLServer,
`
query categories {
......@@ -107,7 +107,7 @@ const queryCategories = async (
{},
);
return (await promise)?.categories as CategoriesResponse;
return response?.categories as CategoriesResponse;
};
const voltasticsConfigMock: VoltasticsConfig = {
......@@ -156,7 +156,7 @@ const noCacheServerConfig = {
describe("voltastics", () => {
let fetchStub: Stub;
afterEach(() => {
beforeEach(() => {
fetchStub?.restore();
});
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment