From ead6e2634d140d998fe6a6cd349d68e38f48586c Mon Sep 17 00:00:00 2001 From: rldhont Date: Tue, 5 Mar 2024 18:59:38 +0100 Subject: [PATCH 1/5] JS Utils: expose a fetch method that provide a successful response --- assets/src/modules/Utils.js | 115 +++++++++++++++++++++++++++--------- 1 file changed, 86 insertions(+), 29 deletions(-) diff --git a/assets/src/modules/Utils.js b/assets/src/modules/Utils.js index 27f62e3bf5..ccdf662aa0 100644 --- a/assets/src/modules/Utils.js +++ b/assets/src/modules/Utils.js @@ -8,12 +8,15 @@ import { NetworkError, HttpError, ResponseError } from './Errors.js'; /** + * The main utils methods * @class * @name Utils */ export default class Utils { /** + * Download a file provided as a string + * @static * @param {string} text - file content * @param {string} fileType - file's MIME type * @param {string} fileName - file'name with extension @@ -34,9 +37,10 @@ export default class Utils { /** * Send an ajax POST request to download a file - * @param {string} url - * @param {Array} parameters - * @param {Function} callback optionnal callback executed when download ends + * @static + * @param {string} url - A string or any other object with a stringifier — including a URL object — that provides the URL of the resource to send the request to. + * @param {Array} parameters - Parameters that will be serialize as a Query string + * @param {Function} callback - optional callback executed when download ends */ static downloadFile(url, parameters, callback) { var xhr = new XMLHttpRequest(); @@ -84,48 +88,93 @@ export default class Utils { xhr.send($.param(parameters, true)); } - static fetchJSON(resource, options) { + /** + * Fetching a resource from the network, returning a promise that is fulfilled once the response is successful. + * @static + * @param {string} resource - This defines the resource that you wish to fetch. A string or any other object with a stringifier — including a URL object — that provides the URL of the resource you want to fetch. + * @param {object} options - An object containing any custom settings you want to apply to the request. + * @returns {Promise} A Promise that resolves to a successful Response object (status in the range 200 – 299) + * @throws {HttpError} In case of not successful response (status not in the range 200 – 299) + * @throws {NetworkError} In case of catch exceptions + * @see https://developer.mozilla.org/en-US/docs/Web/API/fetch + * @see https://developer.mozilla.org/en-US/docs/Web/API/Response + */ + static fetch(resource, options) { return fetch(resource, options).then(response => { if (response.ok) { - const contentType = response.headers.get('Content-Type') || ''; - - if (contentType.includes('application/json') || - contentType.includes('application/vnd.geo+json')) { - return response.json().catch(error => { - return Promise.reject(new ResponseError('Invalid JSON: ' + error.message, response, resource, options)); - }); - } - - return Promise.reject(new ResponseError('Invalid content type: ' + contentType, response, resource, options)); + return response; } return Promise.reject(new HttpError('HTTP error: ' + response.status, response.status, resource, options)); }).catch(error => { + if (error instanceof NetworkError) { + return Promise.reject(error); + } return Promise.reject(new NetworkError(error.message, resource, options)); }); } - static fetchHTML(resource, options) { - return fetch(resource, options).then(response => { - if (response.ok) { - const contentType = response.headers.get('Content-Type') || ''; + /** + * Fetching a resource from the network, which is JSON or GeoJSON, returning a promise that resolves with the result of parsing the response body text as JSON. + * @static + * @param {string} resource - This defines the resource that you wish to fetch. A string or any other object with a stringifier — including a URL object — that provides the URL of the resource you want to fetch. + * @param {object} options - An object containing any custom settings you want to apply to the request. + * @returns {Promise} A Promise that resolves with the result of parsing the response body text as JSON. + * @throws {ResponseError} In case of invalid content type (not application/json or application/vnd.geo+json) or Invalid JSON + * @throws {HttpError} In case of not successful response (status not in the range 200 – 299) + * @throws {NetworkError} In case of catch exceptions + * @see https://developer.mozilla.org/en-US/docs/Web/API/fetch + * @see https://developer.mozilla.org/en-US/docs/Web/API/Response + */ + static fetchJSON(resource, options) { + return Utils.fetch(resource, options).then(response => { + const contentType = response.headers.get('Content-Type') || ''; + + if (contentType.includes('application/json') || + contentType.includes('application/vnd.geo+json')) { + return response.json().catch(error => { + return Promise.reject(new ResponseError('Invalid JSON: ' + error.message, response, resource, options)); + }); + } - if (contentType.includes('text/html')) { - return response.text().catch(error => { - return Promise.reject(new ResponseError('HTML error: ' + error.message, response, resource, options)); - }); - } + return Promise.reject(new ResponseError('Invalid content type: ' + contentType, response, resource, options)); + }).catch(error => {return Promise.reject(error)}); + } - return Promise.reject(new ResponseError('Invalid content type: ' + contentType, response, resource, options)); + /** + * Fetching a resource from the network, which is HTML, returning a promise that resolves with a text representation of the response body. + * @static + * @param {string} resource - This defines the resource that you wish to fetch. A string or any other object with a stringifier — including a URL object — that provides the URL of the resource you want to fetch. + * @param {object} options - An object containing any custom settings you want to apply to the request. + * @returns {Promise} A Promise that resolves with a text representation of the response body. + * @throws {ResponseError} In case of invalid content type (not text/html) + * @throws {HttpError} In case of not successful response (status not in the range 200 – 299) + * @throws {NetworkError} In case of catch exceptions + * @see https://developer.mozilla.org/en-US/docs/Web/API/fetch + * @see https://developer.mozilla.org/en-US/docs/Web/API/Response + */ + static fetchHTML(resource, options) { + return Utils.fetch(resource, options).then(response => { + const contentType = response.headers.get('Content-Type') || ''; + + if (contentType.includes('text/html')) { + return response.text().catch(error => { + return Promise.reject(new ResponseError('HTML error: ' + error.message, response, resource, options)); + }); } - return Promise.reject(new HttpError('HTTP error: ' + response.status, response.status, resource, options)); - }).catch(error => { - return Promise.reject(new NetworkError(error.message, resource, options)); - }); + return Promise.reject(new ResponseError('Invalid content type: ' + contentType, response, resource, options)); + }).catch(error => {return Promise.reject(error)}); } - // Source: https://github.com/openlayers/ol2/blob/master/lib/OpenLayers/Util.js#L1101 + /** + * Get the corresponding resolution for the scale with meters per unit + * @static + * @param {number} scale - The scale + * @param {number} metersPerUnit - The meters per unit + * @returns {number} The corresponding resolution + * @see https://github.com/openlayers/ol2/blob/master/lib/OpenLayers/Util.js#L1101 + */ static getResolutionFromScale(scale, metersPerUnit) { const inchesPerMeter = 1000 / 25.4; const DPI = 96; @@ -133,6 +182,14 @@ export default class Utils { return resolution; } + /** + * Get the corresponding scale for the resolution with meters per unit + * @static + * @param {number} resolution - The scale + * @param {number} metersPerUnit - The meters per unit + * @returns {number} The corresponding scale + * @see getResolutionFromScale + */ static getScaleFromResolution(resolution, metersPerUnit) { const inchesPerMeter = 1000 / 25.4; const DPI = 96; From de3bb2ef89147f3b4f8b9cb6b7f66a74dcc28453 Mon Sep 17 00:00:00 2001 From: rldhont Date: Tue, 5 Mar 2024 19:00:56 +0100 Subject: [PATCH 2/5] JS main Lizmap: fixing the JS Docs --- assets/src/modules/Lizmap.js | 119 ++++++++++++++++++++++++++++------- 1 file changed, 96 insertions(+), 23 deletions(-) diff --git a/assets/src/modules/Lizmap.js b/assets/src/modules/Lizmap.js index b592e534cb..5b14543a58 100644 --- a/assets/src/modules/Lizmap.js +++ b/assets/src/modules/Lizmap.js @@ -26,14 +26,21 @@ import Permalink from './Permalink.js'; import Search from './Search.js'; import WMSCapabilities from 'ol/format/WMSCapabilities.js'; -import {intersects as extentIntersects} from 'ol/extent.js'; -import { transform as transformOL, transformExtent as transformExtentOL, get as getProjection, clearAllProjections } from 'ol/proj.js'; +import { Coordinate as olCoordinate } from 'ol/coordinate.js' +import { Extent as olExtent, intersects as olExtentIntersects} from 'ol/extent.js'; +import { Projection as olProjection, transform as olTransform, transformExtent as olTransformExtent, get as getProjection, clearAllProjections } from 'ol/proj.js'; import { register } from 'ol/proj/proj4.js'; import proj4 from 'proj4'; import ProxyEvents from './ProxyEvents.js'; /** + * A projection as Projection, SRS identifier string or undefined. + * @typedef {olProjection|string|undefined} ProjectionLike + */ + +/** + * The main Lizmap definition * @class * @name Lizmap */ @@ -47,9 +54,10 @@ export default class Lizmap { // The initialConfig has been cloned because it will be freezed this._initialConfig = new Config(structuredClone(configs.initialConfig), wmsCapabilities); this._state = new State(this._initialConfig, configs.startupFeatures); + this._utils = Utils; // Register projections if unknown - for (const [ref, def] of Object.entries(lizProj4)) { + for (const [ref, def] of Object.entries(window.lizProj4)) { if (ref !== "" && !proj4.defs(ref)) { proj4.defs(ref, def); } @@ -78,7 +86,7 @@ export default class Lizmap { break; } // Transform geographic extent to project projection - const extent = transformExtentOL(wmsCapabilities.Capability.Layer.EX_GeographicBoundingBox, 'CRS:84', bbox.crs); + const extent = olTransformExtent(wmsCapabilities.Capability.Layer.EX_GeographicBoundingBox, 'CRS:84', bbox.crs); // Check closest coordinates if (Math.abs(extent[0] - bbox.extent[1]) < Math.abs(extent[0] - bbox.extent[0]) && Math.abs(extent[1] - bbox.extent[0]) < Math.abs(extent[1] - bbox.extent[1]) @@ -91,9 +99,9 @@ export default class Lizmap { break; } // Transform extent from project projection to CRS:84 - const geoExtent = transformExtentOL(bbox.extent, bbox.crs, 'CRS:84'); + const geoExtent = olTransformExtent(bbox.extent, bbox.crs, 'CRS:84'); // Check intersects between transform extent and provided extent by WMS Capapbilities - if (!extentIntersects(geoExtent, wmsCapabilities.Capability.Layer.EX_GeographicBoundingBox)) { + if (!olExtentIntersects(geoExtent, wmsCapabilities.Capability.Layer.EX_GeographicBoundingBox)) { // if extents do not intersect, we have to update the projection definition proj4.defs(configProj.ref, configProj.proj4+' +axis=neu'); clearAllProjections(); @@ -138,7 +146,6 @@ export default class Lizmap { this.proxyEvents = new ProxyEvents(); this.wfs = new WFS(); this.wms = new WMS(); - this.utils = Utils; this.action = new Action(); this.featureStorage = new FeatureStorage(); this.popup = new Popup(); @@ -153,11 +160,16 @@ export default class Lizmap { }); } + /** + * Is new OL map on top of OL2 one? + * @type {boolean} + */ get newOlMap() { return this.map._newOlMap; } /** + * Setting if the new OL map is on top of OL2 one * @param {boolean} mode - switch new OL map on top of OL2 one */ set newOlMap(mode){ @@ -165,6 +177,10 @@ export default class Lizmap { document.getElementById('newOlMap').style.zIndex = mode ? 750 : 'auto'; } + /** + * The old lizmap object + * @type {object} + */ get lizmap3() { return this._lizmap3; } @@ -180,47 +196,88 @@ export default class Lizmap { /** * The lizmap user interface state - * @type {Config} + * @type {State} */ get state() { return this._state; } + /** + * The Utils class + * @type {Utils} + */ + get utils() { + return this._utils; + } + + /** + * The old lizmap config object + * @type {object} + */ get config() { return this._lizmap3.config; } + /** + * The lizmap map OL2 projection + * @type {object} + */ get projection() { return this._lizmap3.map.getProjection(); } + /** + * The QGIS Project crs authentification id + * @type {string} + */ get qgisProjectProjection(){ return this.config.options.qgisProjectProjection.ref; } + /** + * The list of XML FeatureType Elements + * @type {Array} + */ get vectorLayerFeatureTypes() { return this._lizmap3.getVectorLayerFeatureTypes(); } + /** + * The list of format for file export + * @type {string[]} + */ get vectorLayerResultFormat() { return this._lizmap3.getVectorLayerResultFormat(); } + /** + * The Lizmap service URL + * @type {string} + */ get serviceURL() { return lizUrls.wms + '?' + (new URLSearchParams(lizUrls.params).toString()); } + /** + * The Lizmap media URL + * @type {string} + */ get mediaURL() { return lizUrls.media + '?' + (new URLSearchParams(lizUrls.params).toString()); } + /** + * The map center + * @type {number[]} + */ get center() { const center = this._lizmap3.map.getCenter(); return [center.lon, center.lat]; } /** - * @param {Array} lonlat - lonlat to center to. + * Setting the map center + * @param {number[]} lonlat - lonlat to center to. */ set center(lonlat) { this.map.getView().setCenter(lonlat); @@ -228,7 +285,7 @@ export default class Lizmap { /** * The view extent - an array with left, bottom, right, top - * @type {Array} + * @type {number[]} */ get extent() { return this.map.getView().calculateExtent(); @@ -242,17 +299,33 @@ export default class Lizmap { this.map.getView().fit(bounds, {nearest: true}); } + /** + * Getting the layer name from the WFS typeName + * @param {string} typeName - the WFS typeName + * @returns {string} the layer name corresponding to the WFS typeName + */ getNameByTypeName(typeName) { return this._lizmap3.getNameByTypeName(typeName); } + /** + * Getting the layer name from the Lizmap cleanName + * @param {string} cleanName - the Lizmap cleanName + * @returns {string} the layer name corresponding to the Lizmap cleanName + */ getLayerNameByCleanName(cleanName) { return this._lizmap3.getLayerNameByCleanName(cleanName); } - // Display message on screen for users - displayMessage(message, type, close) { - this._lizmap3.addMessage(message, type, close); + /** + * Display message on screen for users + * @param {string} message - the message to display to the user + * @param {string} type - the message type: 'info', 'error' or 'success'; default 'info' + * @param {boolean} close - add a close button; default false + * @param {number} delay - The time, in milliseconds that the message will stay on the screen + */ + displayMessage(message, type, close, delay) { + this._lizmap3.addMessage(message, type, close, delay); } /** @@ -263,25 +336,25 @@ export default class Lizmap { * See {@link module:ol/proj.transformExtent} for extent transformation. * See the transform method of {@link module:ol/geom/Geometry~Geometry} and its * subclasses for geometry transforms. - * @param {import("./coordinate.js").Coordinate} coordinate Coordinate. - * @param {ProjectionLike} source Source projection-like. - * @param {ProjectionLike} destination Destination projection-like. - * @returns {import("./coordinate.js").Coordinate} Coordinate. + * @param {olCoordinate} coordinate - Coordinate. + * @param {ProjectionLike} source - Source projection-like. + * @param {ProjectionLike} destination - Destination projection-like. + * @returns {olCoordinate} Coordinate. */ transform(coordinate, source, destination) { - return transformOL(coordinate, source, destination); + return olTransform(coordinate, source, destination); } /** * Expose OpenLayers transformExtent method for external JS. * Transforms an extent from source projection to destination projection. This * returns a new extent (and does not modify the original). - * @param {import("./extent.js").Extent} extent The extent to transform. - * @param {ProjectionLike} source Source projection-like. - * @param {ProjectionLike} destination Destination projection-like. - * @returns {import("./extent.js").Extent} The transformed extent. + * @param {olExtent} extent - The extent to transform. + * @param {ProjectionLike} source - Source projection-like. + * @param {ProjectionLike} destination - Destination projection-like. + * @returns {olExtent} The transformed extent. */ transformExtent(extent, source, destination){ - return transformExtentOL(extent, source, destination); + return olTransformExtent(extent, source, destination); } } From 977acdc7935fbcf0bc5a1d662349455c8149395c Mon Sep 17 00:00:00 2001 From: rldhont Date: Thu, 7 Mar 2024 10:48:51 +0100 Subject: [PATCH 3/5] Tests JS units: mock request with undici to check fetch --- tests/js-units/node/utils.test.js | 326 ++++++++++++++++++++++++++++++ tests/js-units/package-lock.json | 22 +- tests/js-units/package.json | 3 +- 3 files changed, 349 insertions(+), 2 deletions(-) create mode 100644 tests/js-units/node/utils.test.js diff --git a/tests/js-units/node/utils.test.js b/tests/js-units/node/utils.test.js new file mode 100644 index 0000000000..8282c8016b --- /dev/null +++ b/tests/js-units/node/utils.test.js @@ -0,0 +1,326 @@ +import { expect } from 'chai'; + +import { MockAgent, setGlobalDispatcher, Agent } from 'undici'; + +import Utils from '../../../assets/src/modules/Utils.js'; + +const agent = new MockAgent(); +const client = agent.get('http://localhost:8130'); + +const replyGet = (options) => { + const url = new URL(options.path, options.origin); + let params = {}; + for (const [key, value] of url.searchParams) { + params[key] = value; + } + return { + method: options.method, + origin: url.origin, + pathname: url.pathname, + params: params, + }; +}; + +const replyPost = (options) => { + const url = new URL(options.path, options.origin); + let params = {}; + for (const [key, value] of url.searchParams) { + params[key] = value; + } + let body = options.body; + if (options.headers['content-type'].includes('text/plain')) { + body = {}; + const bodyParams = new URLSearchParams(options.body); + for (const [key, value] of bodyParams) { + body[key] = value; + } + } + return { + method: options.method, + origin: url.origin, + pathname: url.pathname, + params: params, + body: body, + }; +}; + +describe('Utils', function () { + before(function () { + // runs once before the first test in this block + agent.disableNetConnect(); + setGlobalDispatcher(agent); + }); + + after(async function () { + // runs once after the last test in this block + await agent.close(); + setGlobalDispatcher(new Agent()); + }); + + it('fetch', async function () { + client + .intercept({ + path: /\/index.php\/lizmap\/service/, + method: 'GET', + }) + .reply(200, replyGet); + + let data = await Utils.fetch('http://localhost:8130/index.php/lizmap/service?repository=test&project=test').then((res) => res.json()); + expect(data).to.deep.eq({ + "method": "GET", + "origin": "http://localhost:8130", + "params": { + "project": "test", + "repository": "test", + }, + "pathname": "/index.php/lizmap/service", + }); + + client + .intercept({ + path: /\/index.php\/lizmap\/service/, + method: 'POST', + }) + .reply(200, replyPost); + + data = await Utils.fetch( + 'http://localhost:8130/index.php/lizmap/service', + { + method:'POST', + body: 'repository=test&project=test', + }).then((res) => res.json()); + expect(data).to.deep.eq({ + "body": { + "project": "test", + "repository": "test", + }, + "method": "POST", + "origin": "http://localhost:8130", + "params": {}, + "pathname": "/index.php/lizmap/service", + }); + }); + + it('fetch error', async function () { + // 500 error + client + .intercept({ + path: (path) => path.includes('internal/server/error'), + method: 'GET', + }) + .reply(500, { + message: 'Internal server error', + status: 'error', + code: 500, + }); + /* + client + .intercept({ + path: (path) => path.includes('bad/gateway'), + method: 'GET', + }) + .reply(502, { + message: 'Bad gateway', + status: 'error', + code: 502, + }); + client + .intercept({ + path: (path) => path.includes('gateway/timeout'), + method: 'GET', + }) + .reply(504, { + message: 'Gateway timeout', + status: 'error', + code: 504, + }); + */ + try { + await Utils.fetch('http://localhost:8130/internal/server/error') + } catch(error) { + expect(error.name).to.be.eq('HttpError'); + expect(error.statusCode).to.be.eq(500); + expect(error.message).to.be.eq('HTTP error: 500'); + expect(error.resource).to.be.eq('http://localhost:8130/internal/server/error'); + expect(error.options).to.be.undefined; + } + + // 404 error + client + .intercept({ + path: (path) => !path.includes('index.php/lizmap/service'), + method: 'GET', + }) + .reply(404, { + message: 'Not found', + status: 'error', + code: 404, + }); + try { + await Utils.fetch('http://localhost:8130/index.php/lizmap/unknown?repository=test&project=test') + } catch(error) { + expect(error.name).to.be.eq('HttpError'); + expect(error.statusCode).to.be.eq(404); + expect(error.message).to.be.eq('HTTP error: 404'); + expect(error.resource).to.be.eq('http://localhost:8130/index.php/lizmap/unknown?repository=test&project=test'); + expect(error.options).to.be.undefined; + } + + // fetch POST failed + client + .intercept({ + path: (path) => !path.includes('index.php/lizmap/service'), + method: 'POST', + }) + .reply(404, { + message: 'Not found', + status: 'error', + code: 404, + }); + try { + await Utils.fetch( + 'http://localhost:8130/index.php/lizmap/unknown', + { + method:'POST', + body: 'repository=test&project=test', + }) + } catch(error) { + expect(error.name).to.be.eq('HttpError'); + expect(error.statusCode).to.be.eq(404); + expect(error.message).to.be.eq('HTTP error: 404'); + expect(error.resource).to.be.eq('http://localhost:8130/index.php/lizmap/unknown'); + expect(error.options).to.deep.eq({ + method:'POST', + body: 'repository=test&project=test', + }); + } + + // Network error + try { + await Utils.fetch('http://localhost:8130/index.php/lizmap/unknown?repository=test&project=test') + } catch(error) { + expect(error.name).to.be.eq('NetworkError'); + expect(error.message).to.be.eq('fetch failed'); + expect(error.resource).to.be.eq('http://localhost:8130/index.php/lizmap/unknown?repository=test&project=test'); + expect(error.options).to.be.undefined; + } + }); + + it('fetchJSON', async function () { + // JSON content type + client + .intercept({ + path: /\/index.php\/lizmap\/service/, + method: 'GET', + }) + .reply(200, replyGet, {headers: {'content-type': 'application/json'}}); + let data = await Utils.fetchJSON('http://localhost:8130/index.php/lizmap/service?repository=test&project=test'); + expect(data).to.deep.eq({ + "method": "GET", + "origin": "http://localhost:8130", + "params": { + "project": "test", + "repository": "test", + }, + "pathname": "/index.php/lizmap/service", + }); + + client + .intercept({ + path: /\/index.php\/lizmap\/service/, + method: 'POST', + }) + .reply(200, replyPost, {headers: {'content-type': 'application/json'}}); + data = await Utils.fetchJSON( + 'http://localhost:8130/index.php/lizmap/service', + { + method:'POST', + body: 'repository=test&project=test', + }); + expect(data).to.deep.eq({ + "body": { + "project": "test", + "repository": "test", + }, + "method": "POST", + "origin": "http://localhost:8130", + "params": {}, + "pathname": "/index.php/lizmap/service", + }); + + // GeoJSON content type + client + .intercept({ + path: /\/index.php\/lizmap\/service/, + method: 'GET', + }) + .reply(200, replyGet, {headers: {'content-type': 'application/vnd.geo+json'}}); + data = await Utils.fetchJSON('http://localhost:8130/index.php/lizmap/service?repository=test&project=test'); + expect(data).to.deep.eq({ + "method": "GET", + "origin": "http://localhost:8130", + "params": { + "project": "test", + "repository": "test", + }, + "pathname": "/index.php/lizmap/service", + }); + }); + + it('fetchJSON response error', async function () { + // Error content type + client + .intercept({ + path: /\/index.php\/lizmap\/service/, + method: 'GET', + }) + .reply(200, replyGet, {headers: {'content-type': 'text/html'}}); + + try { + await Utils.fetchJSON('http://localhost:8130/index.php/lizmap/service?repository=test&project=test'); + } catch(error) { + expect(error.name).to.be.eq('ResponseError'); + expect(error.message).to.be.eq('Invalid content type: text/html'); + expect(error.resource).to.be.eq('http://localhost:8130/index.php/lizmap/service?repository=test&project=test'); + expect(error.options).to.be.undefined; + } + }); + + it('fetchHTML', async function () { + // JSON content type + client + .intercept({ + path: /\/index.php\/lizmap\/service/, + method: 'GET', + }) + .reply(200, replyGet, {headers: {'content-type': 'text/html'}}); + let data = await Utils.fetchHTML('http://localhost:8130/index.php/lizmap/service?repository=test&project=test'); + expect(JSON.parse(data)).to.deep.eq({ + "method": "GET", + "origin": "http://localhost:8130", + "params": { + "project": "test", + "repository": "test", + }, + "pathname": "/index.php/lizmap/service", + }); + }); + + it('fetchHTML Response error', async function () { + // JSON content type + client + .intercept({ + path: /\/index.php\/lizmap\/service/, + method: 'GET', + }) + .reply(200, replyGet, {headers: {'content-type': 'application/json'}}); + try { + await Utils.fetchHTML('http://localhost:8130/index.php/lizmap/service?repository=test&project=test'); + } catch(error) { + expect(error.name).to.be.eq('ResponseError'); + expect(error.message).to.be.eq('Invalid content type: application/json'); + expect(error.resource).to.be.eq('http://localhost:8130/index.php/lizmap/service?repository=test&project=test'); + expect(error.options).to.be.undefined; + } + }); +}) diff --git a/tests/js-units/package-lock.json b/tests/js-units/package-lock.json index 22cf351c58..0435c45c7a 100644 --- a/tests/js-units/package-lock.json +++ b/tests/js-units/package-lock.json @@ -8,12 +8,21 @@ "name": "js-test", "version": "1.0.0", "dependencies": { - "mocha": "^10.0.0" + "mocha": "^10.0.0", + "undici": "^6.7.0" }, "devDependencies": { "chai": "^4.3.6" } }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "engines": { + "node": ">=14" + } + }, "node_modules/ansi-colors": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", @@ -789,6 +798,17 @@ "node": ">=4" } }, + "node_modules/undici": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.7.0.tgz", + "integrity": "sha512-IcWssIyDN1gk6Mcae44q04oRoWTKrW8OKz0effVK1xdWwAgMPnfpxhn9RXUSL5JlwSikO18R7Ibk7Nukz6kxWA==", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=18.0" + } + }, "node_modules/workerpool": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", diff --git a/tests/js-units/package.json b/tests/js-units/package.json index 6bb786320e..4fea2113f0 100644 --- a/tests/js-units/package.json +++ b/tests/js-units/package.json @@ -8,7 +8,8 @@ }, "author": "3Liz", "dependencies": { - "mocha": "^10.0.0" + "mocha": "^10.0.0", + "undici": "^6.7.0" }, "devDependencies": { "chai": "^4.3.6" From d42e579671fcb1e36c1225830ba987217f5e9c03 Mon Sep 17 00:00:00 2001 From: rldhont Date: Thu, 7 Mar 2024 11:20:04 +0100 Subject: [PATCH 4/5] Tests JS units: mock WFS requests --- tests/js-units/node/wfs.test.js | 106 ++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 tests/js-units/node/wfs.test.js diff --git a/tests/js-units/node/wfs.test.js b/tests/js-units/node/wfs.test.js new file mode 100644 index 0000000000..7554a394b2 --- /dev/null +++ b/tests/js-units/node/wfs.test.js @@ -0,0 +1,106 @@ +import { expect } from 'chai'; + +import { MockAgent, setGlobalDispatcher, Agent } from 'undici'; + +import WFS from '../../../assets/src/modules/WFS.js'; + +const agent = new MockAgent(); +const client = agent.get('http://localhost:8130'); + +const replyPost = (options) => { + const url = new URL(options.path, options.origin); + let params = {}; + for (const [key, value] of url.searchParams) { + params[key] = value; + } + let body = options.body; + if (options.headers['content-type'].includes('text/plain') + || options.headers['content-type'].includes('application/x-www-form-urlencoded')) { + body = {}; + const bodyParams = new URLSearchParams(options.body); + for (const [key, value] of bodyParams) { + body[key] = value; + } + } + return { + method: options.method, + origin: url.origin, + pathname: url.pathname, + params: params, + body: body, + }; +}; + +globalThis.lizUrls = { + params: { + "repository": "test", + "project": "test" + }, + wms: "http://localhost:8130/index.php/lizmap/service", +} + +const wfs = new WFS(); + +describe('WFS', function () { + before(function () { + // runs once before the first test in this block + agent.disableNetConnect(); + setGlobalDispatcher(agent); + }); + + after(async function () { + // runs once after the last test in this block + await agent.close(); + setGlobalDispatcher(new Agent()); + }); + + it('describeFeatureType', async function () { + client + .intercept({ + path: /\/index.php\/lizmap\/service/, + method: 'POST', + }) + .reply(200, replyPost, {headers: {'content-type': 'application/json'}}); + const data = await wfs.describeFeatureType({typeName:'test'}); + expect(data).to.deep.eq({ + "body": { + SERVICE: 'WFS', + REQUEST: 'DescribeFeatureType', + VERSION: '1.0.0', + OUTPUTFORMAT: 'JSON', + "project": "test", + "repository": "test", + "typeName": "test", + }, + "method": "POST", + "origin": "http://localhost:8130", + "params": {}, + "pathname": "/index.php/lizmap/service", + }); + }) + + it('GetFeature', async function () { + client + .intercept({ + path: /\/index.php\/lizmap\/service/, + method: 'POST', + }) + .reply(200, replyPost, {headers: {'content-type': 'application/vnd.geo+json'}}); + const data = await wfs.getFeature({typeName:'test'}); + expect(data).to.deep.eq({ + "body": { + SERVICE: 'WFS', + REQUEST: 'GetFeature', + VERSION: '1.0.0', + OUTPUTFORMAT: 'GeoJSON', + "project": "test", + "repository": "test", + "typeName": "test", + }, + "method": "POST", + "origin": "http://localhost:8130", + "params": {}, + "pathname": "/index.php/lizmap/service", + }); + }) +}) From be767c7eb412e0221a675ae898bbd173299eb76a Mon Sep 17 00:00:00 2001 From: rldhont Date: Thu, 7 Mar 2024 11:21:03 +0100 Subject: [PATCH 5/5] Tests JS units: mock WMS requests --- tests/js-units/node/wms.test.js | 121 ++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 tests/js-units/node/wms.test.js diff --git a/tests/js-units/node/wms.test.js b/tests/js-units/node/wms.test.js new file mode 100644 index 0000000000..c4830c2ed1 --- /dev/null +++ b/tests/js-units/node/wms.test.js @@ -0,0 +1,121 @@ +import { expect } from 'chai'; + +import { MockAgent, setGlobalDispatcher, Agent } from 'undici'; + +import WMS from '../../../assets/src/modules/WMS.js'; + +const agent = new MockAgent(); +const client = agent.get('http://localhost:8130'); + +const replyGet = (options) => { + const url = new URL(options.path, options.origin); + let params = {}; + for (const [key, value] of url.searchParams) { + params[key] = value; + } + return { + method: options.method, + origin: url.origin, + pathname: url.pathname, + params: params, + }; +}; + +const replyPost = (options) => { + const url = new URL(options.path, options.origin); + let params = {}; + for (const [key, value] of url.searchParams) { + params[key] = value; + } + let body = options.body; + if (options.headers['content-type'].includes('text/plain') + || options.headers['content-type'].includes('application/x-www-form-urlencoded')) { + body = {}; + const bodyParams = new URLSearchParams(options.body); + for (const [key, value] of bodyParams) { + body[key] = value; + } + } + return { + method: options.method, + origin: url.origin, + pathname: url.pathname, + params: params, + body: body, + }; +}; + +globalThis.lizUrls = { + params: { + "repository": "test", + "project": "test" + }, + wms: "http://localhost:8130/index.php/lizmap/service", +} + +const wms = new WMS(); + +describe('WMS', function () { + before(function () { + // runs once before the first test in this block + agent.disableNetConnect(); + setGlobalDispatcher(agent); + }); + + after(async function () { + // runs once after the last test in this block + await agent.close(); + setGlobalDispatcher(new Agent()); + }); + + it('getFeatureInfo', async function () { + client + .intercept({ + path: /\/index.php\/lizmap\/service/, + method: 'POST', + }) + .reply(200, replyPost, {headers: {'content-type': 'text/html'}}); + const data = await wms.getFeatureInfo({Name:'test'}); + expect(JSON.parse(data)).to.deep.eq({ + "body": { + SERVICE: 'WMS', + REQUEST: 'GetFeatureInfo', + VERSION: '1.3.0', + CRS: 'EPSG:4326', + INFO_FORMAT: 'text/html', + "project": "test", + "repository": "test", + "Name": "test", + }, + "method": "POST", + "origin": "http://localhost:8130", + "params": {}, + "pathname": "/index.php/lizmap/service", + }); + }) + + it('getLegendGraphic', async function () { + client + .intercept({ + path: /\/index.php\/lizmap\/service/, + method: 'POST', + }) + .reply(200, replyPost, {headers: {'content-type': 'application/json'}}); + const data = await wms.getLegendGraphic({Name:'test'}); + expect(data).to.deep.eq({ + "body": { + SERVICE: 'WMS', + REQUEST: 'GetLegendGraphic', + VERSION: '1.3.0', + FORMAT: 'application/json', + "project": "test", + "repository": "test", + "Name": "test", + }, + "method": "POST", + "origin": "http://localhost:8130", + "params": {}, + "pathname": "/index.php/lizmap/service", + }); + }) +})