-
Notifications
You must be signed in to change notification settings - Fork 9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
test: e2e subdomain redirect tests #151
Changes from 5 commits
1161a32
3aa5fb1
208dfaa
d8a2233
9f9976b
cebaf82
857f0e8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,13 +18,15 @@ export default defineConfig({ | |
// reporter: 'html', // Uncomment to generate HTML report | ||
use: { | ||
/* Base URL to use in actions like `await page.goto('/')`. */ | ||
baseURL: 'http://127.0.0.1:3000', | ||
baseURL: 'http://localhost:3333', | ||
|
||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ | ||
trace: 'on-first-retry', | ||
|
||
// 'allow' serviceWorkers is the default, but we want to be explicit | ||
serviceWorkers: 'allow' | ||
serviceWorkers: 'allow', | ||
|
||
ignoreHTTPSErrors: true | ||
}, | ||
globalSetup: './test-e2e/global-setup.js', | ||
|
||
|
@@ -38,12 +40,22 @@ export default defineConfig({ | |
use: { ...devices['Desktop Firefox'] } | ||
} | ||
], | ||
webServer: { | ||
// need to use built assets due to service worker loading issue. | ||
// TODO: figure out how to get things working with npm run start | ||
command: 'npm run build && npx http-server --silent -p 3000 dist', | ||
port: 3000, | ||
timeout: 120 * 1000, | ||
reuseExistingServer: !process.env.CI | ||
} | ||
webServer: [ | ||
{ | ||
command: 'node test-e2e/reverse-proxy.js', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is the reverse proxy needed? Currently it only seems to add CORS headers and allow OPTIONS requests. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. without the reverse proxy, the subdomain origin isolation check will fail: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ╰─ ✔ ❯ npm run test:chrome -- -g 'subdomain-detection' --max-failures 5
> [email protected] test:chrome
> playwright test -c playwright.config.js --project chromium -g subdomain-detection --max-failures 5
[WebServer] (node:14358) [DEP0066] DeprecationWarning: OutgoingMessage.prototype._headers is deprecated
(Use `node --trace-deprecation ...` to show where the warning was created)
Running 2 tests using 1 worker
✘ 1 [chromium] › subdomain-detection.test.ts:6:3 › subdomain-detection › path requests are redirected to subdomains (898ms)
✘ 2 [chromium] › subdomain-detection.test.ts:28:3 › subdomain-detection › path requests are redirected to subdomains automatically with autoreload enabled (893ms)
1) [chromium] › subdomain-detection.test.ts:6:3 › subdomain-detection › path requests are redirected to subdomains
Error: expect(received).toBe(expected) // Object.is equality
Expected: "http://bafkqablimvwgy3y.ipfs.localhost:3333/"
Received: "http://localhost:3000/ipfs/bafkqablimvwgy3y"
11 | const initialResponse = await page.goto('/ipfs/bafkqablimvwgy3y', { waitUntil: 'commit' })
12 |
> 13 | expect(initialResponse?.url()).toBe('http://bafkqablimvwgy3y.ipfs.localhost:3333/')
| ^
14 | expect(initialResponse?.request()?.redirectedFrom()?.url()).toBe('http://localhost:3333/ipfs/bafkqablimvwgy3y')
15 |
16 | await page.waitForURL('http://bafkqablimvwgy3y.ipfs.localhost:3333')
at /Users/sgtpooki/code/work/protocol.ai/ipfs-shipyard/helia-service-worker-gateway/test-e2e/subdomain-detection.test.ts:13:36
2) [chromium] › subdomain-detection.test.ts:28:3 › subdomain-detection › path requests are redirected to subdomains automatically with autoreload enabled
Error: expect(received).toBe(expected) // Object.is equality
Expected: "http://bafkqablimvwgy3y.ipfs.localhost:3333/"
Received: "http://localhost:3000/ipfs/bafkqablimvwgy3y"
32 | const initialResponse = await page.goto('/ipfs/bafkqablimvwgy3y', { waitUntil: 'commit' })
33 |
> 34 | expect(initialResponse?.url()).toBe('http://bafkqablimvwgy3y.ipfs.localhost:3333/')
| ^
35 | expect(initialResponse?.request()?.redirectedFrom()?.url()).toBe('http://localhost:3333/ipfs/bafkqablimvwgy3y')
36 |
37 | await page.waitForURL('http://bafkqablimvwgy3y.ipfs.localhost:3333')
at /Users/sgtpooki/code/work/protocol.ai/ipfs-shipyard/helia-service-worker-gateway/test-e2e/subdomain-detection.test.ts:34:36
2 failed
[chromium] › subdomain-detection.test.ts:6:3 › subdomain-detection › path requests are redirected to subdomains
[chromium] › subdomain-detection.test.ts:28:3 › subdomain-detection › path requests are redirected to subdomains automatically with autoreload enabled There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the ports are wrong, but you can see that it's not on a subdomain |
||
timeout: 5 * 1000, | ||
env: { | ||
BACKEND_PORT: '3000', | ||
PROXY_PORT: '3333' | ||
} | ||
}, | ||
{ | ||
// need to use built assets due to service worker loading issue. | ||
// TODO: figure out how to get things working with npm run start | ||
command: 'npm run build && npx http-server --silent -p 3000 dist', | ||
port: 3000, | ||
timeout: 120 * 1000, | ||
reuseExistingServer: !process.env.CI | ||
} | ||
] | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -120,8 +120,8 @@ self.addEventListener('activate', (event) => { | |
const { action } = message.data | ||
switch (action) { | ||
case 'RELOAD_CONFIG': | ||
void updateVerifiedFetch().then(() => { | ||
channel.postMessage({ action: 'RELOAD_CONFIG_SUCCESS' }) | ||
void updateVerifiedFetch().then(async () => { | ||
channel.postMessage<any>({ action: 'RELOAD_CONFIG_SUCCESS', data: { config: await getConfig() } }) | ||
trace('sw: RELOAD_CONFIG_SUCCESS for %s', self.location.origin) | ||
}) | ||
break | ||
|
@@ -184,6 +184,11 @@ async function requestRouting (event: FetchEvent, url: URL): Promise<boolean> { | |
} else if (!isValidRequestForSW(event)) { | ||
trace('helia-sw: not a valid request for helia-sw, ignoring ', event.request.url) | ||
return false | ||
} else if (url.href.includes('bafkqaaa.ipfs')) { | ||
// TODO: we shouldn't need to do this, but sometimes verified-fetch will attempt to fetch this. | ||
SgtPooki marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// `bafkqaaa` is an empty inline CID, so this response *is* valid. | ||
event.respondWith(new Response('', { status: 200 })) | ||
return false | ||
} | ||
|
||
if (isRootRequestForContent(event) || isSubdomainRequest(event)) { | ||
|
@@ -460,7 +465,7 @@ async function fetchHandler ({ path, request, event }: FetchHandlerArg): Promise | |
const response = await verifiedFetch(verifiedFetchUrl, { | ||
signal, | ||
headers, | ||
// TODO redirect: 'manual', // enable when http urls are supported by verified-fetch: https://github.com/ipfs-shipyard/helia-service-worker-gateway/issues/62#issuecomment-1977661456 | ||
redirect: 'manual', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. finally done with this todo There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That would close #62 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i'll add an e2e test prior to closing #62 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. one last thing blocking fix of #62: ipfs/helia-verified-fetch#33 it was previously only working for subpath directories. |
||
onProgress: (e) => { | ||
trace(`${e.type}: `, e.detail) | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import type { Locator, Page } from '@playwright/test' | ||
|
||
export interface GetLocator { | ||
(page: Page): Locator | ||
} | ||
|
||
export const getHeader: GetLocator = (page) => page.locator('.e2e-header') | ||
export const getHeaderTitle: GetLocator = (page) => page.locator('.e2e-header-title') | ||
export const getConfigButton: GetLocator = (page) => page.locator('.e2e-header-config-button') | ||
export const getConfigPage: GetLocator = (page) => page.locator('.e2e-config-page') | ||
export const getConfigPageInput: GetLocator = (page) => page.locator('.e2e-config-page-input') | ||
export const getConfigPageButton: GetLocator = (page) => page.locator('.e2e-config-page-button') | ||
export const getConfigAutoReloadInput: GetLocator = (page) => page.locator('.e2e-config-page-input-autoreload') |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
/** | ||
* This fixutre is used to configure specific settings for the service worker | ||
* that are loaded in config-db.ts | ||
* | ||
* Note that this was only tested and confirmed working for subdomain pages. | ||
*/ | ||
import { waitForServiceWorker } from './wait-for-service-worker.js' | ||
import type { ConfigDb } from '../../src/lib/config-db.js' | ||
import type { Page } from '@playwright/test' | ||
|
||
// TODO: ensure that the config can be set on root and loaded properly by subdomains with playwright | ||
export async function setConfig ({ page, config }: { page: Page, config: Partial<ConfigDb> }): Promise<void> { | ||
await waitForServiceWorker(page) | ||
// we can't pass through functions we already have defined, so many of these things are copied over from <root>/src/lib/generic-db.ts | ||
await page.evaluate(async (configInPage) => { | ||
const dbName = 'helia-sw' | ||
const storeName = 'config' | ||
const openDb = async (): Promise<IDBDatabase> => new Promise((resolve, reject) => { | ||
const request = indexedDB.open(dbName, 1) | ||
request.onerror = () => { reject(request.error) } | ||
request.onsuccess = () => { resolve(request.result) } | ||
request.onupgradeneeded = (event) => { | ||
const db = request.result | ||
db.createObjectStore(storeName) | ||
} | ||
}) | ||
const db = await openDb() | ||
const put = async (key, value): Promise<void> => { | ||
const transaction = db.transaction(storeName, 'readwrite') | ||
const store = transaction.objectStore(storeName) | ||
const request = store.put(value, key) | ||
return new Promise((resolve, reject) => { | ||
request.onerror = () => { reject(request.error) } | ||
request.onsuccess = () => { resolve() } | ||
}) | ||
} | ||
|
||
// for every config value passed, make sure we set them in the db | ||
for (const [key, value] of Object.entries(configInPage)) { | ||
await put(key, value) | ||
} | ||
|
||
db.close() | ||
|
||
/** | ||
* We need to do an operation like HeliaServiceWorkerCommsChannel.messageAndWaitForResponse | ||
* see {@link HeliaServiceWorkerCommsChannel} for more information | ||
*/ | ||
const channel = new BroadcastChannel('helia:sw') | ||
const swResponsePromise = new Promise<void>((resolve, reject) => { | ||
const onSuccess = (e: MessageEvent): void => { | ||
if (e.data.action === 'RELOAD_CONFIG_SUCCESS') { | ||
resolve() | ||
channel.removeEventListener('message', onSuccess) | ||
} | ||
} | ||
channel.addEventListener('message', onSuccess) | ||
}) | ||
channel.postMessage({ target: 'SW', action: 'RELOAD_CONFIG', source: 'WINDOW' }) | ||
Comment on lines
+49
to
+59
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I hope this code here helps people understand the benefit of |
||
await swResponsePromise | ||
// TODO: we shouldn't need this. We should be able to just post a message to the service worker to reload it's config. | ||
window.postMessage({ source: 'helia-sw-config-iframe', target: 'PARENT', action: 'RELOAD_CONFIG', config: configInPage }, { targetOrigin: window.location.origin }) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is actually the only part that I confirmed that origin is getting indexedDB set properly, but autoreload doesn't get picked up by subdomain for some reason. |
||
}, config) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
/** | ||
* This is a simple reverse proxy that makes sure that any request to localhost:3333 is forwarded to localhost:3000. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this should also make it easier for devs to contribute without having to install traefik/nginx/caddy or do anything fancy |
||
* | ||
* This mimicks the type of setup you need in nginx or fronting server to the helia-service-worker-gateway in order | ||
* to handle subdomain requests and origin isolation. | ||
*/ | ||
import httpProxy from 'http-proxy' | ||
|
||
const backendPort = Number(process.env.BACKEND_PORT ?? 3000) | ||
const proxyPort = Number(process.env.PROXY_PORT ?? 3333) | ||
|
||
const proxy = httpProxy.createProxyServer({ | ||
target: { | ||
host: 'localhost', | ||
port: backendPort | ||
} | ||
}) | ||
|
||
proxy.on('proxyRes', (proxyRes, req, res) => { | ||
res.setHeader('Access-Control-Allow-Origin', '*') | ||
res.setHeader('access-control-allow-headers', 'Content-Type, Range, User-Agent, X-Requested-With') | ||
res.setHeader('access-control-allow-methods', 'GET, HEAD, OPTIONS') | ||
|
||
if (req.method === 'OPTIONS') { | ||
res.statusCode = 200 | ||
res.end() | ||
} | ||
}) | ||
|
||
proxy.listen(proxyPort) | ||
|
||
// eslint-disable-next-line no-console | ||
console.log('reverse proxy forwarding localhost traffic from port %d to %d', proxyPort, backendPort) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { test, expect } from '@playwright/test' | ||
import { setConfig } from './fixtures/set-sw-config.js' | ||
import { waitForServiceWorker } from './fixtures/wait-for-service-worker.js' | ||
|
||
test.describe('subdomain-detection', () => { | ||
test('path requests are redirected to subdomains', async ({ page, context }) => { | ||
await page.goto('/', { waitUntil: 'networkidle' }) | ||
// wait for service worker to load on main url | ||
await waitForServiceWorker(page) | ||
|
||
const initialResponse = await page.goto('/ipfs/bafkqablimvwgy3y', { waitUntil: 'commit' }) | ||
|
||
expect(initialResponse?.url()).toBe('http://bafkqablimvwgy3y.ipfs.localhost:3333/') | ||
expect(initialResponse?.request()?.redirectedFrom()?.url()).toBe('http://localhost:3333/ipfs/bafkqablimvwgy3y') | ||
|
||
await page.waitForURL('http://bafkqablimvwgy3y.ipfs.localhost:3333') | ||
const bodyTextLocator = page.locator('body') | ||
await expect(bodyTextLocator).toContainText('Registering Helia service worker') | ||
|
||
await waitForServiceWorker(page) | ||
await expect(bodyTextLocator).toContainText('Please save your changes to the config to apply them') | ||
|
||
await page.reload() | ||
|
||
await expect(bodyTextLocator).toContainText('hello') | ||
}) | ||
|
||
test('path requests are redirected to subdomains automatically with autoreload enabled', async ({ page, context }) => { | ||
await page.goto('/', { waitUntil: 'networkidle' }) | ||
await waitForServiceWorker(page) | ||
|
||
const initialResponse = await page.goto('/ipfs/bafkqablimvwgy3y', { waitUntil: 'commit' }) | ||
|
||
expect(initialResponse?.url()).toBe('http://bafkqablimvwgy3y.ipfs.localhost:3333/') | ||
expect(initialResponse?.request()?.redirectedFrom()?.url()).toBe('http://localhost:3333/ipfs/bafkqablimvwgy3y') | ||
|
||
await page.waitForURL('http://bafkqablimvwgy3y.ipfs.localhost:3333') | ||
await setConfig({ page, config: { autoReload: true } }) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ideally, |
||
|
||
const bodyTextLocator = page.locator('body') | ||
|
||
await expect(bodyTextLocator).toContainText('hello') | ||
}) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we don't actually need this anymore