terriajs
Version:
Geospatial data visualization platform.
512 lines (453 loc) • 17.2 kB
text/typescript
import { http, HttpResponse } from "msw";
import { configure, reaction, runInAction } from "mobx";
import Cartographic from "terriajs-cesium/Source/Core/Cartographic";
import GeoJsonDataSource from "terriajs-cesium/Source/DataSources/GeoJsonDataSource";
import isDefined from "../../../../lib/Core/isDefined";
import Result from "../../../../lib/Core/Result";
import TerriaError from "../../../../lib/Core/TerriaError";
import MappableMixin from "../../../../lib/ModelMixins/MappableMixin";
import CsvCatalogItem from "../../../../lib/Models/Catalog/CatalogItems/CsvCatalogItem";
import WebProcessingServiceCatalogFunction from "../../../../lib/Models/Catalog/Ows/WebProcessingServiceCatalogFunction";
import WebProcessingServiceCatalogFunctionJob from "../../../../lib/Models/Catalog/Ows/WebProcessingServiceCatalogFunctionJob";
import CommonStrata from "../../../../lib/Models/Definition/CommonStrata";
import DateTimeParameter from "../../../../lib/Models/FunctionParameters/DateTimeParameter";
import EnumerationParameter from "../../../../lib/Models/FunctionParameters/EnumerationParameter";
import GeoJsonParameter from "../../../../lib/Models/FunctionParameters/GeoJsonParameter";
import LineParameter from "../../../../lib/Models/FunctionParameters/LineParameter";
import PointParameter from "../../../../lib/Models/FunctionParameters/PointParameter";
import PolygonParameter from "../../../../lib/Models/FunctionParameters/PolygonParameter";
import RectangleParameter from "../../../../lib/Models/FunctionParameters/RectangleParameter";
import StringParameter from "../../../../lib/Models/FunctionParameters/StringParameter";
import Terria from "../../../../lib/Models/Terria";
import { worker } from "../../../mocks/browser";
import "../../../SpecHelpers";
import regionMapping from "../../../../wwwroot/data/regionMapping.json";
configure({
enforceActions: "observed",
computedRequiresReaction: true
});
import processDescriptionsXml from "../../../../wwwroot/test/WPS/ProcessDescriptions.xml";
import executeResponseXml from "../../../../wwwroot/test/WPS/ExecuteResponse.xml";
import failedExecuteResponseXml from "../../../../wwwroot/test/WPS/FailedExecuteResponse.xml";
import pendingExecuteResponseXml from "../../../../wwwroot/test/WPS/PendingExecuteResponse.xml";
describe("WebProcessingServiceCatalogFunction", function () {
let wps: WebProcessingServiceCatalogFunction;
beforeEach(function () {
const terria = initTerria();
wps = new WebProcessingServiceCatalogFunction("test", terria);
runInAction(() => {
wps.setTrait("definition", "url", "http://example.com/wps");
wps.setTrait("definition", "identifier", "someId");
});
worker.use(
http.get("http://example.com/wps", ({ request }) => {
const url = new URL(request.url);
const wpsRequest = url.searchParams.get("request");
if (wpsRequest === "DescribeProcess") {
return new HttpResponse(processDescriptionsXml, {
headers: { "Content-Type": "text/xml" }
});
}
if (wpsRequest === "Execute") {
return new HttpResponse(executeResponseXml, {
headers: { "Content-Type": "text/xml" }
});
}
throw new Error(`Unexpected WPS GET request: ${url.search}`);
}),
http.post("http://example.com/wps", ({ request }) => {
const url = new URL(request.url);
if (url.searchParams.get("request") !== "Execute")
throw new Error(`Unexpected WPS POST request: ${url.search}`);
return new HttpResponse(executeResponseXml, {
headers: { "Content-Type": "text/xml" }
});
}),
http.get("*/build/TerriaJS/data/regionMapping.json", () =>
HttpResponse.json(regionMapping)
)
);
});
it("has a type & typeName", function () {
expect(WebProcessingServiceCatalogFunction.type).toBe("wps");
expect(wps.typeName).toBe("Web Processing Service (WPS)");
});
describe("when loading", function () {
it("should correctly query the DescribeProcess endpoint", async function () {
await wps.loadMetadata();
expect(wps.functionParameters.length).toBe(2);
expect(wps.functionParameters.map(({ type }) => type)).toEqual([
"string",
"geojson"
]);
});
});
describe("when invoked", function () {
let dispose: () => void;
let disposeMapItems: () => void;
let job: WebProcessingServiceCatalogFunctionJob;
beforeEach(async function () {
dispose = reaction(
() => wps.parameters,
() => {}
);
await wps.loadMetadata();
runInAction(() => {
const param = wps.functionParameters.find(
(p) => p.type === "geojson"
) as GeoJsonParameter;
param.subtype = GeoJsonParameter.PointType;
param.setValue(CommonStrata.user, {
longitude: 2.5302435855103993,
latitude: -0.6592349301568685,
height: -1196.8235676901866
});
});
job = (await wps.submitJob()) as WebProcessingServiceCatalogFunctionJob;
disposeMapItems = reaction(
() => job.mapItems,
() => {}
);
});
afterEach(function () {
dispose();
disposeMapItems();
});
it("makes a POST request to the Execute endpoint", function () {
expect(job.identifier).toBe("someId");
// expect(job.).toMatch(/geometry=/);
expect(job.jobStatus).toBe("finished");
});
it("makes a GET request to the Execute endpoint when `executeWithHttpGet` is true", function () {
runInAction(() => wps.setTrait("definition", "executeWithHttpGet", true));
expect(job.identifier).toBe("someId");
// expect(job.).toMatch(/geometry=/);
expect(job.wpsResponse).toBeDefined();
expect(job.jobStatus).toBe("finished");
});
it("adds a ResultPendingCatalogItem to the workbench", function () {
expect(job.inWorkbench).toBeTruthy();
});
});
describe("on success", function () {
let job: WebProcessingServiceCatalogFunctionJob;
beforeEach(async function () {
let dispose: any;
job = (await wps.submitJob()) as WebProcessingServiceCatalogFunctionJob;
await new Promise<void>((resolve) => {
dispose = reaction(
() => job.downloadedResults,
() => {
if (job.downloadedResults) resolve();
},
{ fireImmediately: true }
);
});
dispose();
});
it("adds a WebProcessingServiceCatalogFunctionJob to workbench", function () {
expect(job.inWorkbench).toBeTruthy();
});
it("adds result to workbench", function () {
expect(job.results.length).toBe(2);
expect(MappableMixin.isMixedInto(job.results[0])).toBeTruthy();
expect(MappableMixin.isMixedInto(job.results[1])).toBeTruthy();
expect(job.results[0].inWorkbench).toBeTruthy();
expect(job.results[1].inWorkbench).toBeTruthy();
});
it("adds a new catalog member for the output", function () {
expect(job.results[0].type).toBe(CsvCatalogItem.type);
});
it("adds a short report", function () {
expect(job.shortReportSections[0].content).toBe(
"Chart Vegetation Cover generated."
);
});
it("returns mapItems", function () {
expect(job.mapItems.length).toBe(1);
expect(job.mapItems[0]).toEqual(jasmine.any(GeoJsonDataSource));
});
it("defines a rectangle", function () {
expect(job.rectangle).toBeDefined();
});
});
describe("otherwise if `statusLocation` is set", function () {
it("polls the statusLocation for the result", async function () {
worker.use(
http.post(
"http://example.com/wps",
() =>
new HttpResponse(pendingExecuteResponseXml, {
headers: { "Content-Type": "text/xml" }
})
),
http.get(
"http://example.com/ows",
() =>
new HttpResponse(executeResponseXml, {
headers: { "Content-Type": "text/xml" }
})
)
);
const job = await wps.submitJob();
const dispose1 = reaction(
() => job.mapItems,
() => {}
);
expect(job.jobStatus).toBe("running");
let dispose2: any;
// Wait for job to finish polling, then check if finished
await new Promise<void>((resolve) => {
dispose2 = reaction(
() => job.refreshEnabled,
() => {
if (!job.refreshEnabled) {
expect(job.jobStatus).toBe("finished");
return job.downloadResults().then(() => resolve());
}
},
{ fireImmediately: true }
);
});
dispose1();
dispose2();
});
it("stops polling if pendingItem is removed from the workbench", async function () {
spyOn(wps.terria.workbench, "add").and.callFake(() =>
Promise.resolve(Result.none())
); // do nothing
worker.use(
http.post(
"http://example.com/wps",
() =>
new HttpResponse(pendingExecuteResponseXml, {
headers: { "Content-Type": "text/xml" }
})
)
);
// Note: we don't stubRequest "http://example.com/ows?check_status/123" here - so an error will be thrown if the job polls for a result
const job = await wps.submitJob();
expect(job.jobStatus).toBe("running");
});
});
describe("on failure", function () {
let dispose: () => void;
beforeEach(async function () {
dispose = reaction(
() => wps.parameters,
() => {}
);
await wps.loadMetadata();
runInAction(() => {
const param = wps.functionParameters.find(
(p) => p.type === "geojson"
) as GeoJsonParameter;
param.subtype = GeoJsonParameter.PointType;
param.setValue(CommonStrata.user, {
longitude: 2.5302435855103993,
latitude: -0.6592349301568685,
height: -1196.8235676901866
});
});
});
afterEach(function () {
dispose();
});
it("marks the ResultPendingCatalogItem as failed - for polling results", async function () {
worker.use(
http.post(
"http://example.com/wps",
() =>
new HttpResponse(pendingExecuteResponseXml, {
headers: { "Content-Type": "text/xml" }
})
),
http.get(
"http://example.com/ows",
() =>
new HttpResponse(failedExecuteResponseXml, {
headers: { "Content-Type": "text/xml" }
})
)
);
const job =
(await wps.submitJob()) as WebProcessingServiceCatalogFunctionJob;
const dispose1 = reaction(
() => job.mapItems,
() => {}
);
let dispose2: any;
// Wait for job to finish polling, then check if failed
await new Promise<void>((resolve) => {
dispose2 = reaction(
() => job.refreshEnabled,
() => {
if (!job.refreshEnabled) {
expect(job.jobStatus).toBe("error");
expect(job.shortReport).toBeDefined();
expect(job.shortReport).toMatch(/invocation failed/i);
resolve();
}
},
{ fireImmediately: true }
);
});
dispose1();
dispose2();
});
it("marks the ResultPendingCatalogItem as failed", async function () {
worker.use(
http.post(
"http://example.com/wps",
() =>
new HttpResponse(failedExecuteResponseXml, {
headers: { "Content-Type": "text/xml" }
})
)
);
try {
const job = await wps.submitJob();
expect(job).toBeUndefined();
} catch (error: any) {
expect(error).toBeDefined();
expect(error instanceof TerriaError).toBeTruthy();
expect(error.message).toBe(
"One of the identifiers passed does not match with any of the processes offered by this server"
);
}
});
});
describe("convertInputToParameter", function () {
it("works for a simple input", function () {
const parameter = wps.convertInputToParameter(wps, {
Identifier: "geometry_id",
Title: "Geometry ID",
Abstract: "ID of the input",
LiteralData: { AnyValue: {} },
minOccurs: 1
});
expect(parameter).toBeDefined();
if (isDefined(parameter)) {
expect(parameter.id).toBe("geometry_id");
expect(parameter.name).toBe("Geometry ID");
expect(parameter.description).toBe("ID of the input");
expect(parameter.isRequired).toBe(true);
}
});
it("converts LiteralData input with `AllowedValues` to EnumerationParameter", function () {
const parameter = wps.convertInputToParameter(wps, {
Identifier: "geometry_id",
LiteralData: { AllowedValues: { Value: ["Point", "Polygon"] } }
});
expect(parameter).toEqual(jasmine.any(EnumerationParameter));
if (parameter) {
const enumParameter = parameter as EnumerationParameter;
expect(enumParameter.options).toEqual([
{ id: "Point" },
{ id: "Polygon" }
]);
}
});
it("converts LiteralData input with `AllowedValue` to EnumerationParameter", function () {
const parameter = wps.convertInputToParameter(wps, {
Identifier: "geometry_id",
LiteralData: { AllowedValue: { Value: "Point" } }
});
expect(parameter).toEqual(jasmine.any(EnumerationParameter));
if (parameter) {
const enumParameter = parameter as EnumerationParameter;
expect(enumParameter.options).toEqual([{ id: "Point" }]);
}
});
it("converts LiteralData input with `AnyValue` to StringParameter", function () {
const parameter = wps.convertInputToParameter(wps, {
Identifier: "geometry_id",
LiteralData: { AnyValue: {} }
});
expect(parameter).toEqual(jasmine.any(StringParameter));
});
it("converts ComplexData input with datetime schema to DateTimeParameter", function () {
const parameter = wps.convertInputToParameter(wps, {
Identifier: "geometry",
ComplexData: {
Default: {
Format: { Schema: "http://www.w3.org/TR/xmlschema-2/#dateTime" }
}
}
});
expect(parameter).toEqual(jasmine.any(DateTimeParameter));
});
it("converts ComplexData input with point schema to PointParameter", function () {
const parameter = wps.convertInputToParameter(wps, {
Identifier: "geometry",
ComplexData: {
Default: {
Format: { Schema: "http://geojson.org/geojson-spec.html#point" }
}
}
});
expect(parameter).toEqual(jasmine.any(PointParameter));
});
it("converts ComplexData input with line schema to LineParameter", function () {
const parameter = wps.convertInputToParameter(wps, {
Identifier: "geometry",
ComplexData: {
Default: {
Format: {
Schema: "http://geojson.org/geojson-spec.html#linestring"
}
}
}
});
expect(parameter).toEqual(jasmine.any(LineParameter));
});
it("converts ComplexData input with polygon schema to PolygonParameter", function () {
const parameter = wps.convertInputToParameter(wps, {
Identifier: "geometry",
ComplexData: {
Default: {
Format: { Schema: "http://geojson.org/geojson-spec.html#polygon" }
}
}
});
expect(parameter).toEqual(jasmine.any(PolygonParameter));
});
it("converts ComplexData input with GeoJson schema to GeoJsonParameter", function () {
const parameter = wps.convertInputToParameter(wps, {
Identifier: "geometry",
ComplexData: {
Default: {
Format: { Schema: "http://geojson.org/geojson-spec.html#geojson" }
}
}
});
expect(parameter).toEqual(jasmine.any(GeoJsonParameter));
});
it("converts input with BoundingBoxData to RectangleParameter", function () {
const parameter = wps.convertInputToParameter(wps, {
Identifier: "geometry",
BoundingBoxData: {
Default: { CRS: "crs84" }
}
});
expect(parameter).toEqual(jasmine.any(RectangleParameter));
});
});
it("can convert a parameter to data input", async function () {
const parameter = new PointParameter(wps, {
id: "foo"
});
parameter.setValue(CommonStrata.user, Cartographic.ZERO);
const input = await wps.convertParameterToInput(parameter);
expect(input).toEqual({
inputIdentifier: "foo",
inputValue:
'{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point","coordinates":[0,0,0]},"properties":{}}]}',
inputType: "ComplexData"
});
});
});
function initTerria() {
const terria = new Terria({ baseUrl: "./" });
return terria;
}