diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index e82b75a..8fe1987 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -56,7 +56,7 @@ fi # Copy servers.json for stability page (frontend/dual modes) -if [[ "$MODE" == "frontend" || "$MODE" == "dual" ]]; then +if [[ "$MODE" == "frontend" || "$MODE" == "dual" ]] && [ -f /servers.json ]; then cp /servers.json /var/www/html/servers.json fi @@ -91,6 +91,11 @@ if [[ "$MODE" == "frontend" || "$MODE" == "dual" || "$MODE" == "standalone" ]]; if [ -f /servers.json ]; then echo "using mounted /servers.json for server-list.json" cp /servers.json /var/www/html/server-list.json + elif [ -n "$SERVER_LIST_URL" ]; then + echo "no /servers.json found, relying on SERVER_LIST_URL" + elif [ "$MODE" == "frontend" ]; then + echo "ERROR: /servers.json not found and SERVER_LIST_URL is not set" >&2 + exit 1 else echo "no /servers.json found, create one for local host" # generate config for just the local server @@ -100,7 +105,7 @@ if [[ "$MODE" == "frontend" || "$MODE" == "dual" || "$MODE" == "standalone" ]]; echo "using SERVER_LIST_URL for frontend server list" SERVER_LIST_URL_ESCAPED=$(printf '%s\n' "$SERVER_LIST_URL" | sed 's/[&/\\]/\\&/g; s/\$/\\$/g') sed -i "s/var SPEEDTEST_SERVERS = \"server-list.json\";/var SPEEDTEST_SERVERS = \"$SERVER_LIST_URL_ESCAPED\";/" /var/www/html/index-modern.html - sed -i "s/var SPEEDTEST_SERVERS = \\[/var SPEEDTEST_SERVERS = \"$SERVER_LIST_URL_ESCAPED\";\\n\\t\\t\\/\\*/" /var/www/html/index-classic.html + sed -i "/var SPEEDTEST_SERVERS = \\[/,/^[[:space:]]*];/c\\\t\tvar SPEEDTEST_SERVERS = \"$SERVER_LIST_URL_ESCAPED\";" /var/www/html/index-classic.html sed -i "s/var SPEEDTEST_SERVERS = \"server-list.json\";/var SPEEDTEST_SERVERS = \"$SERVER_LIST_URL_ESCAPED\";/" /var/www/html/stability.html fi diff --git a/playwright.config.js b/playwright.config.js index bdb24a2..5f53bcd 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -19,7 +19,7 @@ module.exports = defineConfig({ globalSetup: require.resolve('./tests/e2e/global-setup.js'), globalTeardown: require.resolve('./tests/e2e/global-teardown.js'), webServer: { - command: 'python3 -m http.server 18184 --bind 127.0.0.1 --directory .', + command: 'node tests/e2e/static-server.js', url: 'http://127.0.0.1:18184', reuseExistingServer: !process.env.CI, }, diff --git a/tests/docker-compose-playwright.yml b/tests/docker-compose-playwright.yml index 7ea3296..e63a466 100644 --- a/tests/docker-compose-playwright.yml +++ b/tests/docker-compose-playwright.yml @@ -78,6 +78,19 @@ services: ports: - "18182:8080" + frontend-remote: + build: + context: .. + dockerfile: Dockerfile + depends_on: + - backend-testpoint + environment: + - MODE=frontend + - WEBPORT=8080 + - SERVER_LIST_URL=http://127.0.0.1:18184/tests/e2e/fixtures/servers-frontend-remote.json + ports: + - "18188:8080" + dual: build: context: .. diff --git a/tests/e2e/fixtures/servers-frontend-remote.json b/tests/e2e/fixtures/servers-frontend-remote.json new file mode 100644 index 0000000..bdd7e61 --- /dev/null +++ b/tests/e2e/fixtures/servers-frontend-remote.json @@ -0,0 +1,11 @@ +[ + { + "name": "Remote frontend backend", + "server": "http://127.0.0.1:18181/", + "dlURL": "garbage.php", + "ulURL": "empty.php", + "pingURL": "empty.php", + "getIpURL": "getIP.php", + "id": 1 + } +] diff --git a/tests/e2e/global-setup.js b/tests/e2e/global-setup.js index a861e0a..b95b178 100644 --- a/tests/e2e/global-setup.js +++ b/tests/e2e/global-setup.js @@ -71,5 +71,6 @@ module.exports = async () => { await waitForReady('standalone-alpine', 'http://127.0.0.1:18187/index.html', timeoutMs); await waitForReady('backend', 'http://127.0.0.1:18181/empty.php', timeoutMs); await waitForReady('frontend', 'http://127.0.0.1:18182/index-modern.html', timeoutMs); + await waitForReady('frontend-remote', 'http://127.0.0.1:18188/index-modern.html', timeoutMs); await waitForReady('dual', 'http://127.0.0.1:18183/index-modern.html', timeoutMs); }; diff --git a/tests/e2e/helpers/env.js b/tests/e2e/helpers/env.js index e935680..b76d317 100644 --- a/tests/e2e/helpers/env.js +++ b/tests/e2e/helpers/env.js @@ -3,6 +3,7 @@ const baseUrls = { backend: 'http://127.0.0.1:18181', frontend: 'http://127.0.0.1:18182', dual: 'http://127.0.0.1:18183', + frontendRemote: 'http://127.0.0.1:18188', standaloneNew: 'http://127.0.0.1:18185', standaloneApostrophe: 'http://127.0.0.1:18186', standaloneAlpine: 'http://127.0.0.1:18187', diff --git a/tests/e2e/modes.spec.js b/tests/e2e/modes.spec.js index ea37a51..e7bc51d 100644 --- a/tests/e2e/modes.spec.js +++ b/tests/e2e/modes.spec.js @@ -1,8 +1,9 @@ const { test, expect } = require('@playwright/test'); const { baseUrls } = require('./helpers/env'); -const { modernStartButton } = require('./helpers/ui'); +const { modernStartButton, classicStartButton } = require('./helpers/ui'); const defaultTagline = 'No Flash, No Java, No Websockets, No Bullsh*t'; +const frontendRemoteServerListUrl = 'http://127.0.0.1:18184/tests/e2e/fixtures/servers-frontend-remote.json'; test.describe('Runtime mode smoke coverage', () => { test('standalone exposes UI and local backend endpoints', async ({ page, request }) => { @@ -52,6 +53,34 @@ test.describe('Runtime mode smoke coverage', () => { await expect(page.locator('#selected-server')).not.toHaveText(/searching nearest server/i); }); + test('frontend starts with SERVER_LIST_URL without requiring /servers.json', async ({ page, request }) => { + const index = await request.get(`${baseUrls.frontendRemote}/index-modern.html`); + expect(index.ok()).toBeTruthy(); + await expect(await index.text()).toContain(frontendRemoteServerListUrl); + + const stability = await request.get(`${baseUrls.frontendRemote}/stability.html`); + expect(stability.ok()).toBeTruthy(); + await expect(await stability.text()).toContain(frontendRemoteServerListUrl); + + const localBackendEndpoint = await request.get(`${baseUrls.frontendRemote}/backend/empty.php`); + expect(localBackendEndpoint.status()).toBe(404); + + await page.goto(`${baseUrls.frontendRemote}/index-modern.html`); + await expect(modernStartButton(page)).toBeVisible(); + await expect(page.locator('#selected-server')).toContainText('Remote frontend backend', { timeout: 10_000 }); + }); + + test('default entrypoint loads the classic frontend with SERVER_LIST_URL', async ({ page }) => { + const pageErrors = []; + page.on('pageerror', (error) => pageErrors.push(error.message)); + + await page.goto(`${baseUrls.frontendRemote}/index.html`); + await page.waitForURL(/index-classic\.html/); + await expect(classicStartButton(page)).toBeVisible(); + await expect(page.locator('#server')).toContainText('Remote frontend backend', { timeout: 10_000 }); + expect(pageErrors).toEqual([]); + }); + test('dual combines frontend and local backend availability', async ({ page, request }) => { const serverList = await request.get(`${baseUrls.dual}/server-list.json`); expect(serverList.ok()).toBeTruthy(); diff --git a/tests/e2e/static-repository-assets.spec.js b/tests/e2e/static-repository-assets.spec.js index efa5719..bc91b29 100644 --- a/tests/e2e/static-repository-assets.spec.js +++ b/tests/e2e/static-repository-assets.spec.js @@ -32,4 +32,12 @@ test.describe("Unmodified repository static assets", () => { const missing = await request.get(`${staticRepositoryUrl}/does-not-exist`); expect(missing.status()).toBe(404); }); + + test("returns 400 for malformed percent-encoded paths without stopping the server", async ({ request }) => { + const malformed = await request.get(`${staticRepositoryUrl}/%E0%A4%A`); + expect(malformed.status()).toBe(400); + + const followingRequest = await request.get(`${staticRepositoryUrl}/settings.json`); + expect(followingRequest.status()).toBe(200); + }); }); diff --git a/tests/e2e/static-server.js b/tests/e2e/static-server.js new file mode 100644 index 0000000..5a09774 --- /dev/null +++ b/tests/e2e/static-server.js @@ -0,0 +1,57 @@ +const fs = require("node:fs"); +const http = require("node:http"); +const path = require("node:path"); + +const root = process.cwd(); +const contentTypes = { + ".css": "text/css; charset=utf-8", + ".html": "text/html; charset=utf-8", + ".js": "text/javascript; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".svg": "image/svg+xml", + ".woff2": "font/woff2" +}; + +http + .createServer((request, response) => { + response.setHeader("Access-Control-Allow-Origin", "*"); + + if (request.method === "OPTIONS") { + response.writeHead(204); + response.end(); + return; + } + + let pathname; + try { + const url = new URL(request.url, "http://127.0.0.1"); + pathname = url.pathname === "/" ? "/index.html" : url.pathname; + pathname = decodeURIComponent(pathname); + } catch { + response.writeHead(400); + response.end(); + return; + } + + const file = path.resolve(root, `.${pathname}`); + + if (!file.startsWith(`${root}${path.sep}`)) { + response.writeHead(403); + response.end(); + return; + } + + fs.readFile(file, (error, content) => { + if (error) { + response.writeHead(error.code === "ENOENT" ? 404 : 500); + response.end(); + return; + } + + response.writeHead(200, { + "Content-Type": contentTypes[path.extname(file)] || "application/octet-stream" + }); + response.end(content); + }); + }) + .listen(18184, "127.0.0.1");