Skip to content
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

Merged
merged 7 commits into from
Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"css-loader": "^6.10.0",
"eslint-config-standard-with-typescript": "^34.0.1",
"html-webpack-plugin": "^5.6.0",
"http-proxy": "^1.18.1",
"mini-css-extract-plugin": "^2.8.1",
"npm-run-all": "^4.1.5",
"patch-package": "^8.0.0",
Expand Down
32 changes: 22 additions & 10 deletions playwright.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member Author

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

Suggested change
ignoreHTTPSErrors: true

},
globalSetup: './test-e2e/global-setup.js',

Expand All @@ -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',
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The 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

Copy link
Member Author

Choose a reason for hiding this comment

The 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
}
]
})
2 changes: 1 addition & 1 deletion src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ function App (): JSX.Element {
const isSubdomainRender = isPathOrSubdomainRequest(window.location)

if (isRequestToViewConfigPage) {
if (isSubdomainRender) {
if (isSubdomainRender && window.self === window.top) {
return <RedirectPage />
}

Expand Down
2 changes: 1 addition & 1 deletion src/components/config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export default (): JSX.Element | null => {
<Collapsible collapsedLabel="View config" expandedLabel='Hide config' collapsed={isInIframe}>
<LocalStorageInput className="e2e-config-page-input" localStorageKey={LOCAL_STORAGE_KEYS.config.gateways} label='Gateways' validationFn={urlValidationFn} defaultValue='[]' />
<LocalStorageInput className="e2e-config-page-input" localStorageKey={LOCAL_STORAGE_KEYS.config.routers} label='Routers' validationFn={urlValidationFn} defaultValue='[]'/>
<LocalStorageToggle className="e2e-config-page-input" localStorageKey={LOCAL_STORAGE_KEYS.config.autoReload} onLabel='Auto Reload' offLabel='Show Config' />
<LocalStorageToggle className="e2e-config-page-input e2e-config-page-input-autoreload" localStorageKey={LOCAL_STORAGE_KEYS.config.autoReload} onLabel='Auto Reload' offLabel='Show Config' />
<LocalStorageInput className="e2e-config-page-input" localStorageKey={LOCAL_STORAGE_KEYS.config.debug} label='Debug logging' validationFn={stringValidationFn} defaultValue=''/>
<ServiceWorkerReadyButton className="e2e-config-page-button" id="save-config" label='Save Config' waitingLabel='Waiting for service worker registration...' onClick={() => { void saveConfig() }} />

Expand Down
11 changes: 8 additions & 3 deletions src/sw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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',
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

finally done with this todo

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would close #62

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'll add an e2e test prior to closing #62

Copy link
Member Author

Choose a reason for hiding this comment

The 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)
}
Expand Down
13 changes: 13 additions & 0 deletions test-e2e/fixtures/locators.ts
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')
64 changes: 64 additions & 0 deletions test-e2e/fixtures/set-sw-config.ts
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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hope this code here helps people understand the benefit of HeliaServiceWorkerCommsChannel, even if it does need cleaned up further.

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 })
Copy link
Member Author

@SgtPooki SgtPooki Mar 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is actually the only part that works triggers the config reload on subdomains.. without this, it doesn't work in the autoreload test, however, the above is needed to set the config on origin domain.

I confirmed that origin is getting indexedDB set properly, but autoreload doesn't get picked up by subdomain for some reason.

}, config)
}
13 changes: 2 additions & 11 deletions test-e2e/layout.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,5 @@
import { test, expect, type Page, type Locator } from '@playwright/test'

interface GetLocator {
(page: Page): Locator
}
const getHeader: GetLocator = (page) => page.locator('.e2e-header')
const getHeaderTitle: GetLocator = (page) => page.locator('.e2e-header-title')
const getConfigButton: GetLocator = (page) => page.locator('.e2e-header-config-button')
const getConfigPage: GetLocator = (page) => page.locator('.e2e-config-page')
const getConfigPageInput: GetLocator = (page) => page.locator('.e2e-config-page-input')
const getConfigPageButton: GetLocator = (page) => page.locator('.e2e-config-page-button')
import { test, expect } from '@playwright/test'
import { getConfigButton, getConfigPage, getConfigPageButton, getConfigPageInput, getHeader, getHeaderTitle } from './fixtures/locators.js'

test.describe('smoketests', () => {
test.beforeEach(async ({ page }) => {
Expand Down
4 changes: 2 additions & 2 deletions test-e2e/path-routing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { waitForServiceWorker } from './fixtures/wait-for-service-worker.js'

test.describe('path-routing', () => {
test('can load identity CID via path', async ({ page }) => {
await page.goto('http://127.0.0.1:3000', { waitUntil: 'networkidle' })
await page.goto('http://127.0.0.1:3333', { waitUntil: 'networkidle' })
SgtPooki marked this conversation as resolved.
Show resolved Hide resolved
// wait for service worker to load
await waitForServiceWorker(page)
const response = await page.goto('http://127.0.0.1:3000/ipfs/bafkqablimvwgy3y', { waitUntil: 'networkidle' })
const response = await page.goto('http://127.0.0.1:3333/ipfs/bafkqablimvwgy3y', { waitUntil: 'networkidle' })
SgtPooki marked this conversation as resolved.
Show resolved Hide resolved

expect(response?.status()).toBe(200)

Expand Down
33 changes: 33 additions & 0 deletions test-e2e/reverse-proxy.js
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.
Copy link
Member Author

Choose a reason for hiding this comment

The 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)
44 changes: 44 additions & 0 deletions test-e2e/subdomain-detection.test.ts
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 } })
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ideally, setConfig would occur immediately after line 30, but.. alas, subdomains in playwright are not picking up the config for some reason.


const bodyTextLocator = page.locator('body')

await expect(bodyTextLocator).toContainText('hello')
})
})
Loading