From 6c478f35c86e4a325eb215ea0630ea876ca0be12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Iv=C3=A1n=20Vieitez=20Parra?= <3857362+corrideat@users.noreply.github.com> Date: Sun, 23 Jun 2024 20:36:23 +0000 Subject: [PATCH] Tests: more general file upload & enable Safari --- .../workflows/integration-tests-github.yml | 9 +-- src/utils/server.ts | 80 ++++++++++++++++--- test/e2e/basic-functionality.test.ts | 2 +- test/e2e/lib/dragAndDropFile.ts | 10 +-- test/e2e/lib/navigateToFile.ts | 36 +++++---- 5 files changed, 96 insertions(+), 41 deletions(-) diff --git a/.github/workflows/integration-tests-github.yml b/.github/workflows/integration-tests-github.yml index 6873b5f..15f90f4 100644 --- a/.github/workflows/integration-tests-github.yml +++ b/.github/workflows/integration-tests-github.yml @@ -22,12 +22,9 @@ jobs: - chrome - firefox - MicrosoftEdge - # include: - # # TODO: Currently, testing on Safari doesn't work. It seems - # # like the the 'about:blank' form submission could be the - # # issue. - # - os: macos-latest - # browser: safari + include: + - os: macos-latest + browser: safari runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 diff --git a/src/utils/server.ts b/src/utils/server.ts index 2a8f14a..2b369d2 100644 --- a/src/utils/server.ts +++ b/src/utils/server.ts @@ -21,6 +21,9 @@ import { fileURLToPath } from 'node:url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); +const boundaryMatchRegex = + /;\s*boundary=(?:"([0-9a-zA-Z'()+_,\-./:=? ]{0,69}[0-9a-zA-Z'()+_,\-./:=?])"|([0-9a-zA-Z'+_\-.]{0,69}[0-9a-zA-Z'+_\-.]))/; + const server = http.createServer((req, res) => { const okHandler = (data: Buffer) => { const firstScriptIndex = Buffer.from(data).indexOf(' { ['content-type', 'text/html; charset=UTF-8'], [ 'content-security-policy', - "default-src 'none'; script-src 'self' 'unsafe-eval' data:; script-src-elem blob: data:; script-src-attr 'none'; style-src data:; child-src blob:; connect-src blob: data:; frame-ancestors 'self'; form-action data:", + "default-src 'none'; script-src 'self' 'unsafe-eval' data:; script-src-elem blob: data:; script-src-attr 'none'; style-src data:; child-src blob:; connect-src blob: data:; frame-ancestors 'self'; form-action 'self' data:", ], [ 'permissions-policy', @@ -92,7 +95,37 @@ const server = http.createServer((req, res) => { ), ); } catch (e) { - console.error('Error sending index', e); + console.error('Error sending /blank', e); + res.writeHead( + 500, + (e instanceof Error && e?.message) || + String(e) || + 'Unknown error', + ); + } finally { + res.end(); + } + } else if (req.method === 'GET' && req.url === '/echo-document') { + try { + okHandler( + Buffer.from( + '' + + '' + + '' + + '' + + 'Echo document' + + '' + + '' + + '
' + + '' + + '' + + '
' + + '' + + '', + ), + ); + } catch (e) { + console.error('Error sending /echo-document', e); res.writeHead( 500, (e instanceof Error && e?.message) || @@ -102,7 +135,23 @@ const server = http.createServer((req, res) => { } finally { res.end(); } - } else if (req.method === 'POST' && req.url === '/') { + } else if (req.method === 'POST' && req.url === '/echo-document') { + if ( + !req.headers['content-type'] || + !req.headers['content-type'].startsWith('multipart/form-data;') + ) { + res.writeHead(415).end(); + return; + } + + const boundaryMatch = + req.headers['content-type'].match(boundaryMatchRegex); + if (!boundaryMatch || (!boundaryMatch[1] && !boundaryMatch[2])) { + res.writeHead(422).end(); + return; + } + const boundary = boundaryMatch[1] || boundaryMatch[2]; + new Promise((resolve) => { const chunks: Buffer[] = []; req.on('data', (chunk: Buffer) => { @@ -111,18 +160,25 @@ const server = http.createServer((req, res) => { req.on('end', () => { const result = Buffer.concat(chunks); - // Special handling to allow 'text/plain' form submissions - if (req.headers['content-type'] === 'text/plain') { - const idx = result.indexOf('__TEXT__='); - if (idx >= 0) { - resolve(result.subarray(idx + '__TEXT__='.length)); - return; - } - } - resolve(result); }); }) + .then((buffer) => { + const startMultipartBody = buffer.indexOf('--' + boundary); + const startMultipartData = buffer.indexOf( + '\r\n\r\n', + startMultipartBody + 2 + boundary.length, + ); + const endMultipartData = buffer.indexOf( + '\r\n--' + boundary + '--', + startMultipartData, + ); + + return buffer.subarray( + startMultipartData + 4, + endMultipartData, + ); + }) .then(okHandler) .catch((e) => { console.error('Error sending index', e); diff --git a/test/e2e/basic-functionality.test.ts b/test/e2e/basic-functionality.test.ts index 613bfb7..b9ca0b3 100644 --- a/test/e2e/basic-functionality.test.ts +++ b/test/e2e/basic-functionality.test.ts @@ -127,7 +127,7 @@ test('Basic functionality', async (t) => { }); await t.test('Can decrypt a file', { ['skip']: !file }, async () => { - navigateToFile(driver, baseUrl, file as File); + await navigateToFile(driver, baseUrl, file as File); await driver.wait(until.elementLocated(By.css('form'))); diff --git a/test/e2e/lib/dragAndDropFile.ts b/test/e2e/lib/dragAndDropFile.ts index d232fcd..0891a3e 100644 --- a/test/e2e/lib/dragAndDropFile.ts +++ b/test/e2e/lib/dragAndDropFile.ts @@ -19,30 +19,30 @@ import type { WebElement } from 'selenium-webdriver'; // Based on: // * , // * -const JS_DROP_FILE = `const element = arguments[0], +const JS_DROP_FILE = `const target$ = arguments[0], contents = new Uint8Array(arguments[1]), filename = arguments[2], type = arguments[3], offsetX = arguments[4], offsetY = arguments[5]; -const doc = element.ownerDocument || document; +const doc = target$.ownerDocument || document; let target, clientX, clientY; for (let i = 0; ; ) { -const box = element.getBoundingClientRect(), +const box = target$.getBoundingClientRect(), clientX = box.left + (offsetX || box.width / 2), clientY = box.top + (offsetY || box.height / 2); target = doc.elementFromPoint(clientX, clientY); -if (target && element.contains(target)) break; +if (target && target$.contains(target)) break; if (++i > 1) { throw new Error('Element not interactable'); } -element.scrollIntoView({ +target$.scrollIntoView({ behavior: 'instant', block: 'center', inline: 'center', diff --git a/test/e2e/lib/navigateToFile.ts b/test/e2e/lib/navigateToFile.ts index 985e32e..4ada777 100644 --- a/test/e2e/lib/navigateToFile.ts +++ b/test/e2e/lib/navigateToFile.ts @@ -14,34 +14,36 @@ */ import type { WebDriver, WebElement } from 'selenium-webdriver'; -import { until } from 'selenium-webdriver'; +import { By, until } from 'selenium-webdriver'; import waitUntilReadyStateComplete from './waitUntilReadyStateComplete.js'; const navigateToFile_ = async (driver: WebDriver, url: URL, file: File) => { - driver.get('about:blank'); + driver.get(new URL('echo-document', url).toString()); + await driver.executeScript( + 'document.documentElement.style.setProperty("display", "none", "important");', + ); await waitUntilReadyStateComplete(driver); const document$: WebElement = await driver.executeScript( 'return document.documentElement;', ); + const fileInput$ = await driver.findElement(By.css('input[type="file"]')); await driver.executeScript( ` - const ns = 'http://www.w3.org/1999/xhtml'; - const form$ = document.createElementNS(ns, 'form'); - form$.setAttribute('action', arguments[0]); - form$.setAttribute('enctype', 'text/plain'); - form$.setAttribute('method', 'POST'); - const textarea$ = document.createElementNS(ns, 'textarea'); - textarea$.setAttribute('name', '__TEXT__'); - textarea$.value = arguments[1]; - form$.appendChild(textarea$); - form$.style.setProperty('transform', 'scale(0)', 'important'); - document.body.appendChild(form$); - form$.submit(); - `, - url.toString(), - Buffer.from(await file.arrayBuffer()).toString('utf-8'), + const target$ = arguments[0], + contents = new Uint8Array(arguments[1]), + filename = arguments[2], + type = arguments[3]; + const dataTransfer = new DataTransfer(); + dataTransfer.effectAllowed = 'all'; + dataTransfer.items.add(new File([contents], filename, { type })); + target$.files = dataTransfer.files; + `, + fileInput$, + Array.from(new Uint8Array(await file.arrayBuffer())), + file.name, file.type, ); + await driver.findElement(By.css('form')).submit(); // Wait until navigation happens await driver.wait(until.stalenessOf(document$)); };