From 2d844dd10d6fb26a703b5504fa9623cb5c26f007 Mon Sep 17 00:00:00 2001 From: Etienne Trimaille Date: Thu, 23 Jan 2025 11:22:49 +0100 Subject: [PATCH] Tests - Migrate XSS tests to POM, check iframe --- tests/end2end/playwright/pages/project.js | 14 + tests/end2end/playwright/xss.spec.js | 50 +- tests/qgis-projects/data/iframe_pdf.geojson | 8 + tests/qgis-projects/tests/media/test.pdf | Bin 0 -> 5773 bytes tests/qgis-projects/tests/xss.qgs | 522 +++++++++++++++++++- tests/qgis-projects/tests/xss.qgs.cfg | 49 +- 6 files changed, 583 insertions(+), 60 deletions(-) create mode 100644 tests/qgis-projects/data/iframe_pdf.geojson create mode 100644 tests/qgis-projects/tests/media/test.pdf diff --git a/tests/end2end/playwright/pages/project.js b/tests/end2end/playwright/pages/project.js index df01871ce2..26c81dbddc 100644 --- a/tests/end2end/playwright/pages/project.js +++ b/tests/end2end/playwright/pages/project.js @@ -54,6 +54,11 @@ export class ProjectPage extends BasePage { * @type {Locator} */ miniDock; + /** + * Popup dock + * @type {Locator} + */ + popupContent; /** * Top search bar * @type {Locator} @@ -75,6 +80,14 @@ export class ProjectPage extends BasePage { attributeTableHtml = (name) => this.page.locator(`#attribute-layer-table-${name}`); + /** + * Editing field for the given field in the panel + * @param {string} name Name of the field + * @returns {Locator} + */ + editingField = (name) => + this.page.locator(`#jforms_view_edition input[name="${name}"]`); + /** * Constructor for a QGIS project page * @param {Page} page The playwright page @@ -89,6 +102,7 @@ export class ProjectPage extends BasePage { this.rightDock = page.locator('#right-dock'); this.bottomDock = page.locator('#bottom-dock'); this.miniDock = page.locator('#mini-dock-content'); + this.popupContent = page.locator('#popupcontent'); this.warningMessage = page.locator('#lizmap-warning-message'); this.search = page.locator('#search-query'); this.switcher = page.locator('#button-switcher'); diff --git a/tests/end2end/playwright/xss.spec.js b/tests/end2end/playwright/xss.spec.js index 0c54c0c13c..5ed57a78c3 100644 --- a/tests/end2end/playwright/xss.spec.js +++ b/tests/end2end/playwright/xss.spec.js @@ -1,15 +1,15 @@ // @ts-check import { test, expect } from '@playwright/test'; -import { gotoMap } from './globals'; +import {ProjectPage} from "./pages/project"; test.describe('XSS', () => { - test.beforeEach(async ({ page }) => { - const url = '/index.php/view/map/?repository=testsrepository&project=xss'; - await gotoMap(url, page); - }); - // Test that flawed data are sanitized before being displayed - test('No dialog from inline JS alert() appears', async ({ page }) => { + test('Flawed data are sanitized before being displayed, no dialog from inline JS alert() appears', + { + tag: ['@readonly'], + },async ({ page }) => { + const project = new ProjectPage(page, 'xss'); + await project.open(); let dialogOpens = 0; page.on('dialog', dialog => { @@ -18,43 +18,35 @@ test.describe('XSS', () => { }); // Edition: add XSS data - await page.locator('#button-edition').click(); - await page.locator('#edition-draw').click(); + await project.openEditingFormWithLayer('xss_layer'); - await page.locator('#jforms_view_edition input[name="description"]').fill(''); + await project.editingField('description').fill(''); - await page.locator('#jforms_view_edition__submit_submit').click(); + await project.editingSubmitForm(); // Open popup - await page.locator('#newOlMap').click({ - position: { - x: 415, - y: 290 - } - }); + await project.clickOnMap(415, 290); // Open attribute table - await page.locator('#button-attributeLayers').click(); - await page - .locator('button[value="xss_layer"].btn-open-attribute-layer') - .click({ force: true }); + await project.openAttributeTable('xss_layer'); expect(dialogOpens).toEqual(0); }); - test('Sanitized iframe in popup', async ({ page }) => { + test('Sanitized iframe in popup', + { + tag: ['@readonly'], + },async ({ page }) => { + const project = new ProjectPage(page, 'xss'); + await project.open(); + let getFeatureInfoRequestPromise = page.waitForRequest(request => request.method() === 'POST' && request.postData()?.includes('GetFeatureInfo') === true); // Open popup - await page.locator('#newOlMap').click({ - position: { - x: 500, - y: 285 - } - }); + await project.clickOnMap(500, 285); await getFeatureInfoRequestPromise; - await expect(page.locator('#popupcontent iframe')).toHaveAttribute('sandbox', 'allow-scripts allow-forms'); + await expect(project.popupContent.locator('iframe')).toHaveAttribute('sandbox', 'allow-scripts allow-forms'); }); }); diff --git a/tests/qgis-projects/data/iframe_pdf.geojson b/tests/qgis-projects/data/iframe_pdf.geojson new file mode 100644 index 0000000000..588e82b365 --- /dev/null +++ b/tests/qgis-projects/data/iframe_pdf.geojson @@ -0,0 +1,8 @@ +{ +"type": "FeatureCollection", +"name": "iframe_pdf", +"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } }, +"features": [ +{ "type": "Feature", "properties": { "ID": "1", "media": "media/test.pdf" }, "geometry": { "type": "Point", "coordinates": [ 3.883570434409934, 43.621029029306968 ] } } +] +} diff --git a/tests/qgis-projects/tests/media/test.pdf b/tests/qgis-projects/tests/media/test.pdf new file mode 100644 index 0000000000000000000000000000000000000000..8862034a175b779cb22781d39e845a1e21575468 GIT binary patch literal 5773 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 + - PROJCRS["RGF93 v1 / Lambert-93",BASEGEOGCRS["RGF93 v1",DATUM["Reseau Geodesique Francais 1993 v1",ELLIPSOID["GRS 1980",6378137,298.257222101,LENGTHUNIT["metre",1]]],PRIMEM["Greenwich",0,ANGLEUNIT["degree",0.0174532925199433]],ID["EPSG",4171]],CONVERSION["Lambert-93",METHOD["Lambert Conic Conformal (2SP)",ID["EPSG",9802]],PARAMETER["Latitude of false origin",46.5,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8821]],PARAMETER["Longitude of false origin",3,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8822]],PARAMETER["Latitude of 1st standard parallel",49,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8823]],PARAMETER["Latitude of 2nd standard parallel",44,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8824]],PARAMETER["Easting at false origin",700000,LENGTHUNIT["metre",1],ID["EPSG",8826]],PARAMETER["Northing at false origin",6600000,LENGTHUNIT["metre",1],ID["EPSG",8827]]],CS[Cartesian,2],AXIS["easting (X)",east,ORDER[1],LENGTHUNIT["metre",1]],AXIS["northing (Y)",north,ORDER[2],LENGTHUNIT["metre",1]],USAGE[SCOPE["Engineering survey, topographic mapping."],AREA["France - onshore and offshore, mainland and Corsica."],BBOX[41.15,-9.86,51.56,10.38]],ID["EPSG",2154]] + PROJCRS["RGF93 v1 / Lambert-93",BASEGEOGCRS["RGF93 v1",DATUM["Reseau Geodesique Francais 1993 v1",ELLIPSOID["GRS 1980",6378137,298.257222101,LENGTHUNIT["metre",1]]],PRIMEM["Greenwich",0,ANGLEUNIT["degree",0.0174532925199433]],ID["EPSG",4171]],CONVERSION["Lambert-93",METHOD["Lambert Conic Conformal (2SP)",ID["EPSG",9802]],PARAMETER["Latitude of false origin",46.5,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8821]],PARAMETER["Longitude of false origin",3,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8822]],PARAMETER["Latitude of 1st standard parallel",49,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8823]],PARAMETER["Latitude of 2nd standard parallel",44,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8824]],PARAMETER["Easting at false origin",700000,LENGTHUNIT["metre",1],ID["EPSG",8826]],PARAMETER["Northing at false origin",6600000,LENGTHUNIT["metre",1],ID["EPSG",8827]]],CS[Cartesian,2],AXIS["easting (X)",east,ORDER[1],LENGTHUNIT["metre",1]],AXIS["northing (Y)",north,ORDER[2],LENGTHUNIT["metre",1]],USAGE[SCOPE["Engineering survey, topographic mapping."],AREA["France - onshore and offshore, mainland and Corsica (France métropolitaine including Corsica)."],BBOX[41.15,-9.86,51.56,10.38]],ID["EPSG",2154]] +proj=lcc +lat_0=46.5 +lon_0=3 +lat_1=49 +lat_2=44 +x_0=700000 +y_0=6600000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs 145 2154 @@ -16,11 +16,29 @@ false + + + + + 0 + 0 + + + + + false + + + + + + + @@ -28,11 +46,13 @@ xss_3334b2fd_75f9_4301_a075_402f6dbed37b + iframe_pdf_6ddaa2e1_6eae_4355_ac46_0b22b97ba8ae + @@ -48,7 +68,7 @@ 0 - PROJCRS["RGF93 v1 / Lambert-93",BASEGEOGCRS["RGF93 v1",DATUM["Reseau Geodesique Francais 1993 v1",ELLIPSOID["GRS 1980",6378137,298.257222101,LENGTHUNIT["metre",1]]],PRIMEM["Greenwich",0,ANGLEUNIT["degree",0.0174532925199433]],ID["EPSG",4171]],CONVERSION["Lambert-93",METHOD["Lambert Conic Conformal (2SP)",ID["EPSG",9802]],PARAMETER["Latitude of false origin",46.5,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8821]],PARAMETER["Longitude of false origin",3,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8822]],PARAMETER["Latitude of 1st standard parallel",49,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8823]],PARAMETER["Latitude of 2nd standard parallel",44,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8824]],PARAMETER["Easting at false origin",700000,LENGTHUNIT["metre",1],ID["EPSG",8826]],PARAMETER["Northing at false origin",6600000,LENGTHUNIT["metre",1],ID["EPSG",8827]]],CS[Cartesian,2],AXIS["easting (X)",east,ORDER[1],LENGTHUNIT["metre",1]],AXIS["northing (Y)",north,ORDER[2],LENGTHUNIT["metre",1]],USAGE[SCOPE["Engineering survey, topographic mapping."],AREA["France - onshore and offshore, mainland and Corsica."],BBOX[41.15,-9.86,51.56,10.38]],ID["EPSG",2154]] + PROJCRS["RGF93 v1 / Lambert-93",BASEGEOGCRS["RGF93 v1",DATUM["Reseau Geodesique Francais 1993 v1",ELLIPSOID["GRS 1980",6378137,298.257222101,LENGTHUNIT["metre",1]]],PRIMEM["Greenwich",0,ANGLEUNIT["degree",0.0174532925199433]],ID["EPSG",4171]],CONVERSION["Lambert-93",METHOD["Lambert Conic Conformal (2SP)",ID["EPSG",9802]],PARAMETER["Latitude of false origin",46.5,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8821]],PARAMETER["Longitude of false origin",3,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8822]],PARAMETER["Latitude of 1st standard parallel",49,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8823]],PARAMETER["Latitude of 2nd standard parallel",44,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8824]],PARAMETER["Easting at false origin",700000,LENGTHUNIT["metre",1],ID["EPSG",8826]],PARAMETER["Northing at false origin",6600000,LENGTHUNIT["metre",1],ID["EPSG",8827]]],CS[Cartesian,2],AXIS["easting (X)",east,ORDER[1],LENGTHUNIT["metre",1]],AXIS["northing (Y)",north,ORDER[2],LENGTHUNIT["metre",1]],USAGE[SCOPE["Engineering survey, topographic mapping."],AREA["France - onshore and offshore, mainland and Corsica (France métropolitaine including Corsica)."],BBOX[41.15,-9.86,51.56,10.38]],ID["EPSG",2154]] +proj=lcc +lat_0=46.5 +lon_0=3 +lat_1=49 +lat_2=44 +x_0=700000 +y_0=6600000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs 145 2154 @@ -64,6 +84,11 @@ + + + diff --git a/tests/qgis-projects/tests/xss.qgs.cfg b/tests/qgis-projects/tests/xss.qgs.cfg index c5c9b8ee1f..4b19d56dbe 100644 --- a/tests/qgis-projects/tests/xss.qgs.cfg +++ b/tests/qgis-projects/tests/xss.qgs.cfg @@ -1,16 +1,15 @@ { "metadata": { - "qgis_desktop_version": 33412, - "lizmap_plugin_version_str": "4.4.5-alpha", - "lizmap_plugin_version": 40405, - "lizmap_web_client_target_version": 31000, - "lizmap_web_client_target_status": "Dev", - "instance_target_url": "http://localhost:8130/", - "instance_target_repository": "testsrepository" + "qgis_desktop_version": 34003, + "lizmap_plugin_version_str": "4.4.7-alpha", + "lizmap_plugin_version": 40407, + "lizmap_web_client_target_version": 30800, + "lizmap_web_client_target_status": "Stable", + "instance_target_url": "https://sandbox.lizmap.com/lizmap_3_8/" }, "warnings": {}, "debug": { - "total_time": 0.36500000000000005 + "total_time": 0.392 }, "options": { "projection": { @@ -67,6 +66,40 @@ "dataviz_drag_drop": [] }, "layers": { + "iframe_pdf": { + "id": "iframe_pdf_6ddaa2e1_6eae_4355_ac46_0b22b97ba8ae", + "name": "iframe_pdf", + "type": "layer", + "geometryType": "point", + "extent": [ + 3.883570434409934, + 43.62102902930697, + 3.883570434409934, + 43.62102902930697 + ], + "crs": "EPSG:4326", + "title": "iframe_pdf", + "abstract": "", + "link": "", + "minScale": 1, + "maxScale": 1000000000000, + "toggled": "False", + "popup": "True", + "popupSource": "qgis", + "popupTemplate": "", + "popupMaxFeatures": 10, + "popupDisplayChildren": "False", + "popup_allow_download": true, + "legend_image_option": "hide_at_startup", + "groupAsLayer": "False", + "baseLayer": "False", + "displayInLegend": "True", + "group_visibility": [], + "singleTile": "True", + "imageFormat": "image/png", + "cached": "False", + "clientCacheExpiration": 300 + }, "xss_layer": { "id": "xss_3334b2fd_75f9_4301_a075_402f6dbed37b", "name": "xss_layer",