UNPKG

@seasketch/geoprocessing

Version:

Geoprocessing and reporting framework for SeaSketch 2.0

325 lines (286 loc) • 11.1 kB
import { describe, test, expect, beforeAll } from "vitest"; import { createMetric, isMetricArray } from "../metrics/helpers.js"; import TaskModel from "./tasks.js"; import { DynamoDBDocument } from "@aws-sdk/lib-dynamodb"; import { DynamoDBClient, CreateTableCommand } from "@aws-sdk/client-dynamodb"; import deepEqual from "fast-deep-equal"; const dynamodb = new DynamoDBClient({ endpoint: "http://localhost:8000", credentials: { accessKeyId: "localaccesskey", secretAccessKey: "localsecretkey", }, region: "localregion", tls: false, }); const docClient = DynamoDBDocument.from(dynamodb); const Tasks = new TaskModel("tasks-core", "tasks-estimates", docClient); const SERVICE_NAME = "test-serviceName"; describe("DynamoDB local", () => { beforeAll(async () => { await dynamodb.send( new CreateTableCommand({ TableName: "tasks-core", KeySchema: [ { AttributeName: "id", KeyType: "HASH" }, { AttributeName: "service", KeyType: "RANGE" }, ], AttributeDefinitions: [ { AttributeName: "id", AttributeType: "S" }, { AttributeName: "service", AttributeType: "S" }, ], ProvisionedThroughput: { ReadCapacityUnits: 1, WriteCapacityUnits: 1, }, }), ); await dynamodb.send( new CreateTableCommand({ TableName: "tasks-estimates", KeySchema: [{ AttributeName: "service", KeyType: "HASH" }], AttributeDefinitions: [ { AttributeName: "service", AttributeType: "S" }, ], ProvisionedThroughput: { ReadCapacityUnits: 1, WriteCapacityUnits: 1, }, }), ); await dynamodb.send( new CreateTableCommand({ TableName: "test-websockets", KeySchema: [{ AttributeName: "connectionId", KeyType: "HASH" }], AttributeDefinitions: [ { AttributeName: "connectionId", AttributeType: "S" }, ], ProvisionedThroughput: { ReadCapacityUnits: 1, WriteCapacityUnits: 1, }, }), ); }); test("create new task", async () => { const task = await Tasks.create(SERVICE_NAME); expect(typeof task.id).toBe("string"); expect(task.status).toBe("pending"); // make sure it saves to the db const item = await docClient.get({ TableName: "tasks-core", Key: { id: task.id, service: SERVICE_NAME, }, }); expect(item && item.Item && item.Item.id).toBe(task.id); }, 10_000); test("create new task with cache disabled should have no record", async () => { const task = await Tasks.create(SERVICE_NAME, { disableCache: true }); expect(typeof task.id).toBe("string"); expect(task.status).toBe("pending"); // make sure it saves to the db const item = await docClient.get({ TableName: "tasks-core", Key: { id: task.id, service: SERVICE_NAME, }, }); // console.log(JSON.stringify(item, null, 2)); expect(item.Item).toBeUndefined(); }, 10_000); test("create new async task", async () => { const task = await Tasks.create(SERVICE_NAME, { wss: "wssabc123" }); expect(typeof task.id).toBe("string"); expect(task.status).toBe("pending"); // make sure it saves to the db const item = await docClient.get({ TableName: "tasks-core", Key: { id: task.id, service: SERVICE_NAME, }, }); expect(item && item.Item && item.Item.id).toBe(task.id); expect(item && item.Item && item.Item.wss.length).toBeGreaterThan(0); }); test("get a created task", async () => { const task = await Tasks.create(SERVICE_NAME); expect(typeof task.id).toBe("string"); const retrieved = await Tasks.get(SERVICE_NAME, task.id); expect(retrieved && retrieved.id).toBe(task.id); }); test("get return undefined for unknown ids", async () => { const retrieved = await Tasks.get(SERVICE_NAME, "unknown-id"); expect(retrieved).toBe(undefined); }); test("create task with a cacheKey id", async () => { const task = await Tasks.create(SERVICE_NAME, { id: "my-cache-key" }); expect(typeof task.id).toBe("string"); expect(task.status).toBe("pending"); // make sure it saves to the db const item = await docClient.get({ TableName: "tasks-core", Key: { id: task.id, service: SERVICE_NAME, }, }); expect(item && item.Item && item.Item.id).toBe("my-cache-key"); }); test("complete an existing task should split data into chunk item", async () => { const task = await Tasks.create(SERVICE_NAME); const response = await Tasks.complete(task, { area: 1_234_556 }); const items = await docClient.query({ TableName: "tasks-core", KeyConditionExpression: "#id = :id", ExpressionAttributeNames: { "#id": "id", }, ExpressionAttributeValues: { ":id": task.id, }, }); // Should be three items under the one partition key (task id), the root and two chunks // console.log(JSON.stringify(items, null, 2)); expect(items.Count).toBe(3); const rootItem = items.Items?.find((item) => item.service === SERVICE_NAME); expect(rootItem && rootItem.status).toBe("completed"); const chunkItem0 = items.Items?.find( (item) => item.service === `${SERVICE_NAME}-chunk-0`, ); const chunkItem1 = items.Items?.find( (item) => item.service === `${SERVICE_NAME}-chunk-1`, ); expect(response.statusCode).toBe(200); expect(chunkItem0).toBeTruthy(); expect(chunkItem0!.data).toBeTruthy(); expect(chunkItem0!.data!.chunk).toBeTruthy(); expect(typeof chunkItem0!.data!.chunk).toBe("string"); expect(chunkItem1).toBeTruthy(); expect(chunkItem1!.data).toBeTruthy(); expect(chunkItem1!.data!.chunk).toBeTruthy(); expect(typeof chunkItem1!.data!.chunk).toBe("string"); }); test("completed task should return merged result", async () => { const task = await Tasks.create(SERVICE_NAME); const metrics = [createMetric({ value: 15, sketchId: "test" })]; const response = await Tasks.complete(task, { metrics, }); expect(response.statusCode).toBe(200); const cachedResult = await Tasks.get(SERVICE_NAME, task.id); const cachedMetrics = cachedResult?.data.metrics; expect(cachedMetrics).toBeTruthy(); expect(isMetricArray(cachedMetrics)).toBe(true); expect(deepEqual(cachedMetrics, metrics)).toBe(true); }); test("completed task with disabled cached should return no result", async () => { const task = await Tasks.create(SERVICE_NAME, { disableCache: true }); const metrics = [createMetric({ value: 15, sketchId: "test" })]; const response = await Tasks.complete(task, { metrics, }); expect(response.statusCode).toBe(200); const cachedResult = await Tasks.get(SERVICE_NAME, task.id); expect(cachedResult).toBeUndefined(); }); test("tasks for multiple services should not get mixed", async () => { const task = await Tasks.create("service1"); const metrics = [createMetric({ value: 15, sketchId: "test1" })]; const response = await Tasks.complete(task, { metrics, }); expect(response.statusCode).toBe(200); const task2 = await Tasks.create("service2"); const metrics2 = [createMetric({ value: 30, sketchId: "test2" })]; const response2 = await Tasks.complete(task2, { metrics: metrics2, }); expect(response2.statusCode).toBe(200); const cachedResult = await Tasks.get("service1", task.id); const cachedMetrics = cachedResult?.data.metrics; expect(cachedMetrics.length).toBe(1); expect(isMetricArray(cachedMetrics)).toBe(true); expect(deepEqual(cachedMetrics, metrics)).toBe(true); const cachedResult2 = await Tasks.get("service2", task2.id); const cachedMetrics2 = cachedResult2?.data.metrics; expect(cachedMetrics2.length).toBe(1); expect(isMetricArray(cachedMetrics2)).toBe(true); expect(deepEqual(cachedMetrics2, metrics2)).toBe(true); }); // To use this test, uncomment and create a sampleResult.json file in the same directory as this test file // test("real task should return real result", async () => { // const task = await Tasks.create(SERVICE_NAME); // const result = fs.readJsonSync("./src/aws/sampleResult.json"); // const response = await Tasks.complete(task, result); // expect(response.statusCode).toBe(200); // const cachedResult = await Tasks.get(SERVICE_NAME, task.id); // // console.log("cachedResult", JSON.stringify(cachedResult, null, 2)); // expect(cachedResult).toBeTruthy(); // }); test("complete a task with multiple sketch metrics above size threshold should split into multiple items", async () => { const task = await Tasks.create(SERVICE_NAME); const metrics = [ createMetric({ value: 15, sketchId: "sketch1" }), createMetric({ value: 30, sketchId: "sketch2" }), ]; const response = await Tasks.complete( task, { metrics, }, { minSplitSizeBytes: 80, // set size that should split into 6 pieces }, ); expect(response.statusCode).toBe(200); // Verify created 3 items, 1 root and 2 chunks const queryResult = await docClient.query({ TableName: "tasks-core", KeyConditionExpression: "#id = :id", ExpressionAttributeNames: { "#id": "id", }, ExpressionAttributeValues: { ":id": task.id, }, }); // Remove root item. remainder, if any, is chunk items if (!queryResult.Items || queryResult.Items.length === 0) throw new Error("No items"); const rootItemIndex = queryResult.Items.findIndex( (item) => item.service === SERVICE_NAME, ); const rootItem = queryResult.Items.splice(rootItemIndex, 1)[0]; // mutates items const chunkItems = queryResult.Items.sort((a, b) => { return a.service.localeCompare(b.service); // sort by chunk index }); expect(rootItem && rootItem.status).toBe("completed"); expect(rootItem && rootItem.data.numChunks).toBe(5); expect(chunkItems.length).toBe(5); // Verify on get that metrics are re-merged with root item const cachedResult = await Tasks.get(SERVICE_NAME, task.id); const cachedMetrics = cachedResult?.data.metrics; expect(cachedMetrics).toBeTruthy(); expect(isMetricArray(cachedMetrics)).toBe(true); expect(cachedMetrics.length).toBe(2); }); test("fail a task", async () => { const task = await Tasks.create(SERVICE_NAME); const response = await Tasks.fail(task, "It broken"); const item = await docClient.get({ TableName: "tasks-core", Key: { id: task.id, service: SERVICE_NAME, }, }); expect(response.statusCode).toBe(500); expect(JSON.parse(response.body).error).toBe("It broken"); expect(item && item.Item && item.Item.status).toBe("failed"); expect(item && item.Item && item.Item.duration).toBeGreaterThan(0); }); });