@seasketch/geoprocessing
Version:
Geoprocessing and reporting framework for SeaSketch 2.0
443 lines (441 loc) • 17.2 kB
JavaScript
import { describe, test, expect } from "vitest";
import { GeoprocessingHandler } from "./GeoprocessingHandler.js";
import Tasks, { GeoprocessingTaskStatus } from "./tasks.js";
import { v4 as uuid } from "uuid";
// @ts-expect-error does not exist
import fetchMock from "fetch-mock-jest";
import deepEqual from "fast-deep-equal";
// Mock task methods, using actual implementation for init
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const init = Tasks.prototype.init;
// jest.mock("./tasks");
// const TasksActual = jest.requireActual("./tasks").default;
// @ts-expect-error does not exist
// Tasks.prototype.create.mockImplementation(
// async (service: string, cacheKey: string) => {
// const task = TasksActual.prototype.init(service, cacheKey);
// return task;
// }
// );
Tasks.prototype.fail.mockImplementation(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async (task, errorDescription, error) => {
task.status = GeoprocessingTaskStatus.Failed;
task.duration = Date.now() - new Date(task.startedAt).getTime();
task.error = errorDescription;
return {
statusCode: 500,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Credentials": true,
},
body: JSON.stringify(task),
};
});
/** Simple in-memory cache for last saved task */
let lastSavedTask;
/**
* Implements mock for Tasks.complete that returns the last saved task
*/
// @ts-expect-error does not exist
Tasks.prototype.complete.mockImplementation(async (task, results) => {
task.data = results;
task.status = GeoprocessingTaskStatus.Completed;
task.duration = Date.now() - new Date(task.startedAt).getTime();
lastSavedTask = task;
return {
statusCode: 200,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Credentials": true,
},
body: JSON.stringify(task),
};
});
const exampleSketch = {
type: "Feature",
properties: {
id: uuid(),
sketchClassId: uuid(),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
name: "My Sketch",
},
geometry: {
type: "Point",
coordinates: [1, 2],
},
};
const exampleResponse = {
foo: "bar",
id: exampleSketch.properties.id,
};
const exampleFeature = {
type: "Feature",
properties: {
id: uuid(),
},
geometry: {
type: "Point",
coordinates: [1, 2],
},
};
const exampleFeatureResponse = {
foo: "bar",
id: exampleFeature.properties.id,
};
fetchMock.get("https://example.com/geom/123", JSON.stringify(exampleSketch));
describe("GeoprocessingHandler", () => {
test.skip("Handler can be constructed and run simple async geoprocessing", async () => {
/*
process.env.RUN_HANDLER_FUNCTION_NAME = "MockLambda";
const handler = new GeoprocessingHandler(
async (sketch) => {
return { foo: "bar", id: sketch.properties.id };
},
{
title: "TestGP",
description: "Test gp function",
executionMode: "async",
memory: 128,
requiresProperties: [],
timeout: 100,
}
);
expect(handler.options.title).toBe("TestGP");
// @ts-expect-error does not exist
Tasks.prototype.get.mockResolvedValueOnce(false);
const result = await handler.lambdaHandler(
({
body: JSON.stringify({
geometryUri: "https://example.com/geom/123",
cacheKey: "abc123",
wss: "wss://localhost:1234",
}),
} as unknown) as APIGatewayProxyEvent,
// @ts-expect-error does not exist
{ awsRequestId: "foo" }
);
//expect(result.statusCode).toBe(200);
const task = JSON.parse(result.body) as GeoprocessingTask;
console.log("task-->>>> ", task);
expect(task.status).toBe(GeoprocessingTaskStatus.Failed);
expect(task.data.foo).toBe("bar");
// make sure cors headers are set
expect(result.headers!["Access-Control-Allow-Origin"]).toBe("*");
expect(result.headers!["Access-Control-Allow-Credentials"]).toBe(true);
expect(task.data.id).toBe(exampleSketch.properties.id);
*/
});
test.skip("Sketch handler can be constructed and run simple geoprocessing", async () => {
const handler = new GeoprocessingHandler(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async (sketch) => exampleResponse, {
title: "TestGP",
description: "Test gp function",
executionMode: "sync",
memory: 128,
requiresProperties: [],
timeout: 100,
});
expect(handler.options.title).toBe("TestGP");
// @ts-expect-error does not exist
Tasks.prototype.get.mockResolvedValueOnce(false);
const result = await handler.lambdaHandler({
body: JSON.stringify({
geometry: "https://example.com/geom/123",
cacheKey: "abc123",
}),
},
// @ts-expect-error does not exist
{ awsRequestId: "foo" });
expect(result.statusCode).toBe(200);
const task = JSON.parse(result.body);
expect(task.status).toBe(GeoprocessingTaskStatus.Completed);
expect(task.data.foo).toBe("bar");
// make sure cors headers are set
expect(result.headers["Access-Control-Allow-Origin"]).toBe("*");
expect(result.headers["Access-Control-Allow-Credentials"]).toBe(true);
expect(task.data.id).toBe(exampleSketch.properties.id);
});
test.skip("Feature handler can be constructed and run simple geoprocessing", async () => {
const handler = new GeoprocessingHandler(async () => exampleFeatureResponse, {
title: "TestGP",
description: "Test gp function",
executionMode: "sync",
memory: 128,
requiresProperties: [],
timeout: 100,
});
expect(handler.options.title).toBe("TestGP");
// @ts-expect-error does not exist
Tasks.prototype.get.mockResolvedValueOnce(false);
const result = await handler.lambdaHandler({
body: JSON.stringify({
geometryUri: "https://example.com/geom/123",
cacheKey: "abc123",
}),
},
// @ts-expect-error does not exist
{ awsRequestId: "foo" });
expect(result.statusCode).toBe(200);
const task = JSON.parse(result.body);
expect(task.status).toBe(GeoprocessingTaskStatus.Completed);
expect(task.data.foo).toBe("bar");
// make sure cors headers are set
expect(result.headers["Access-Control-Allow-Origin"]).toBe("*");
expect(result.headers["Access-Control-Allow-Credentials"]).toBe(true);
expect(task.data.id).toBe(exampleFeature.properties.id);
});
test.skip("Repeated requests should be 'cancelled'", async () => {
const handler = new GeoprocessingHandler(async () => exampleResponse, {
title: "TestGP",
description: "Test gp function",
executionMode: "sync",
memory: 128,
requiresProperties: [],
timeout: 100,
});
// @ts-expect-error does not exist
Tasks.prototype.get.mockResolvedValueOnce(false);
const result = await handler.lambdaHandler({
body: JSON.stringify({
geometryUri: "https://example.com/geom/123",
cacheKey: "abc123",
}),
},
// @ts-expect-error does not exist
{ awsRequestId: "foo" });
const result2 = await handler.lambdaHandler({
body: JSON.stringify({
geometryUri: "https://example.com/geom/123",
cacheKey: "abc123",
}),
},
// @ts-expect-error does not exist
{ awsRequestId: "foo" });
expect(result.body.length).toBeGreaterThan(1);
expect(result2.body.length).toBe(0);
expect(result2.statusCode).toBe(200);
});
//these are dumb copies of the sync calls, just want to make
//sure that the async ones follow the same behavior for caching and
//cancelling repeats
test.skip("Repeated requests should be 'cancelled' for async tasks", async () => {
const handler = new GeoprocessingHandler(async () => exampleResponse, {
title: "TestGP",
description: "Test gp function",
executionMode: "async",
memory: 128,
requiresProperties: [],
timeout: 100,
});
// @ts-expect-error does not exist
Tasks.prototype.get.mockResolvedValueOnce(false);
const result = await handler.lambdaHandler({
body: JSON.stringify({
geometryUri: "https://example.com/geom/123",
cacheKey: "abc123",
}),
},
// @ts-expect-error does not exist
{ awsRequestId: "foo" });
const result2 = await handler.lambdaHandler({
body: JSON.stringify({
geometryUri: "https://example.com/geom/123",
cacheKey: "abc123",
}),
},
// @ts-expect-error does not exist
{ awsRequestId: "foo" });
expect(result.body.length).toBeGreaterThan(1);
expect(result2.body.length).toBe(0);
expect(result2.statusCode).toBe(200);
});
test.skip("Results are cached using request.cacheKey", async () => {
const handler = new GeoprocessingHandler(async () => exampleResponse, {
title: "TestGP",
description: "Test gp function",
executionMode: "sync",
memory: 128,
requiresProperties: [],
timeout: 100,
});
// @ts-expect-error does not exist
Tasks.prototype.get.mockImplementation(async (service, cacheKey) => {
if (cacheKey === "abc123") {
return lastSavedTask;
}
else {
return false;
}
});
const result = await handler.lambdaHandler({
body: JSON.stringify({
geometryUri: "https://example.com/geom/123",
cacheKey: "abc123",
}),
},
// @ts-expect-error does not exist
{ awsRequestId: "foo" });
const result2 = await handler.lambdaHandler({
body: JSON.stringify({
geometryUri: "https://example.com/geom/123",
cacheKey: "abc123",
}),
},
// @ts-expect-error does not exist
{ awsRequestId: "bar" });
// @ts-expect-error does not exist
Tasks.prototype.get.mockReset();
expect(result.body.length).toBeGreaterThan(1);
expect(result2.body.length).toBeGreaterThan(1);
const task1 = JSON.parse(result.body);
const task2 = JSON.parse(result2.body);
const task1ms = new Date(task1.startedAt).valueOf();
const task2ms = new Date(task2.startedAt).valueOf();
expect(task2ms - task1ms).toBeLessThanOrEqual(50);
});
test.skip("Results are cached using request.cacheKey for asynchronous tasks", async () => {
const handler = new GeoprocessingHandler(async () => exampleResponse, {
title: "TestGP",
description: "Test gp function",
executionMode: "async",
memory: 128,
requiresProperties: [],
timeout: 100,
});
// @ts-expect-error does not exist
Tasks.prototype.get.mockImplementation(async (service, cacheKey) => {
if (cacheKey === "abc123") {
return lastSavedTask;
}
else {
return false;
}
});
const result = await handler.lambdaHandler({
body: JSON.stringify({
geometryUri: "https://example.com/geom/123",
cacheKey: "abc123",
}),
},
// @ts-expect-error does not exist
{ awsRequestId: "foo" });
const result2 = await handler.lambdaHandler({
body: JSON.stringify({
geometryUri: "https://example.com/geom/123",
cacheKey: "abc123",
}),
},
// @ts-expect-error does not exist
{ awsRequestId: "bar" });
// @ts-expect-error does not exist
Tasks.prototype.get.mockReset();
expect(result.body.length).toBeGreaterThan(1);
expect(result2.body.length).toBeGreaterThan(1);
const task1 = JSON.parse(result.body);
const task2 = JSON.parse(result2.body);
const task1ms = new Date(task1.startedAt).valueOf();
const task2ms = new Date(task2.startedAt).valueOf();
expect(task2ms - task1ms).toBeLessThanOrEqual(50);
});
test.skip("extraParams can be used", async () => {
const handler = new GeoprocessingHandler(async (sketch, extraParams) => {
return {
extraParams,
};
}, {
title: "paramsGP",
description: "Test gp function",
executionMode: "sync",
memory: 128,
requiresProperties: [],
timeout: 100,
});
expect(handler.options.title).toBe("paramsGP");
// @ts-expect-error does not exist
Tasks.prototype.get.mockResolvedValueOnce(false);
const extraParams = { geography: "nearshore" };
const result = await handler.lambdaHandler({
body: JSON.stringify({
geometry: {},
cacheKey: "abc123",
extraParams,
}),
},
// @ts-expect-error does not exist
{ awsRequestId: "foo" });
expect(result.statusCode).toBe(200);
const task = JSON.parse(result.body);
expect(task.status).toBe(GeoprocessingTaskStatus.Completed);
expect(deepEqual(task.data.extraParams, extraParams)).toBe(true);
// make sure cors headers are set
expect(result.headers["Access-Control-Allow-Origin"]).toBe("*");
expect(result.headers["Access-Control-Allow-Credentials"]).toBe(true);
});
test.skip("Failed geometryUri fetches are communicated to requester", async () => {
fetchMock.get("https://example.com/geom/abc123", 500);
const handler = new GeoprocessingHandler(async () => exampleResponse, {
title: "TestGP",
description: "Test gp function",
executionMode: "sync",
memory: 128,
requiresProperties: [],
timeout: 100,
});
expect(handler.options.title).toBe("TestGP");
// @ts-expect-error does not exist
Tasks.prototype.get.mockResolvedValueOnce(false);
const result = await handler.lambdaHandler({
body: JSON.stringify({
geometryUri: "https://example.com/geom/abc123",
cacheKey: "abc123",
}),
},
// @ts-expect-error does not exist
{ awsRequestId: "foo" });
expect(result.statusCode).toBe(500);
const task = JSON.parse(result.body);
expect(task.error).toContain("geometry");
// make sure cors headers are set still for errors
expect(result.headers["Access-Control-Allow-Origin"]).toBe("*");
expect(result.headers["Access-Control-Allow-Credentials"]).toBe(true);
});
test.skip("Exceptions in geoprocessing function are passed to requester", async () => {
const handler = new GeoprocessingHandler(async (sketch) => {
// @ts-expect-error does not exist
return { foo: sketch.something.doesntexist() };
}, {
title: "TestGP",
description: "Test gp function",
executionMode: "sync",
memory: 128,
requiresProperties: [],
timeout: 100,
});
expect(handler.options.title).toBe("TestGP");
// @ts-expect-error does not exist
Tasks.prototype.get.mockResolvedValueOnce(false);
const result = await handler.lambdaHandler({
body: JSON.stringify({
geometryUri: "https://example.com/geom/123",
cacheKey: "abc123",
}),
},
// @ts-expect-error does not exist
{ awsRequestId: "foo" });
expect(result.statusCode).toBe(500);
const task = JSON.parse(result.body);
expect(task.error).toContain("Failed");
// make sure cors headers are set still for errors
expect(result.headers["Access-Control-Allow-Origin"]).toBe("*");
expect(result.headers["Access-Control-Allow-Credentials"]).toBe(true);
});
// TODO: requiresProperties verification
// TODO: async executionMode
// TODO: Rate limiting
// TODO: Authorization
// TODO: Container tasks
});
//# sourceMappingURL=GeoprocessingHandler.test.js.map