-
Notifications
You must be signed in to change notification settings - Fork 34
fix: add e2e tests for downloading over WS/WSS/WebTransport #1097
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
Draft
aschmahmann
wants to merge
7
commits into
main
Choose a base branch
from
fix/e2e-testing
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
0111d86
test: add e2e tests for downloading over WS/WSS/WebTransport
aschmahmann 55bdbe7
fix: start verifiedFetch on initialization
aschmahmann 0e98978
todo: remove me, illustrative fixes/bugs
aschmahmann 2cf4629
fix: run WebTransport tests against Safari
aschmahmann a85296e
Merge branch 'main' into fix/e2e-testing
achingbrain 58d55bb
fix: stricter http bitswap mocking
aschmahmann 6943c67
fix: add bitswap test that fails when there's only one invalid addres…
aschmahmann File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| diff --git a/node_modules/@helia/block-brokers/dist/src/trustless-gateway/utils.js b/node_modules/@helia/block-brokers/dist/src/trustless-gateway/utils.js | ||
| index 23527a0..15ed1a3 100644 | ||
| --- a/node_modules/@helia/block-brokers/dist/src/trustless-gateway/utils.js | ||
| +++ b/node_modules/@helia/block-brokers/dist/src/trustless-gateway/utils.js | ||
| @@ -5,7 +5,7 @@ import { Uint8ArrayList } from 'uint8arraylist'; | ||
| import { TrustlessGateway } from "./trustless-gateway.js"; | ||
| export function filterNonHTTPMultiaddrs(multiaddrs, allowInsecure, allowLocal) { | ||
| return multiaddrs.filter(ma => { | ||
| - if (HTTPS.matches(ma) || (allowInsecure && HTTP.matches(ma))) { | ||
| + if (HTTPS.exactMatch(ma) || (allowInsecure && HTTP.exactMatch(ma))) { | ||
| if (allowLocal) { | ||
| return true; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| diff --git a/node_modules/@libp2p/webtransport/dist/src/index.js b/node_modules/@libp2p/webtransport/dist/src/index.js | ||
| index 5b52492..48f3757 100644 | ||
| --- a/node_modules/@libp2p/webtransport/dist/src/index.js | ||
| +++ b/node_modules/@libp2p/webtransport/dist/src/index.js | ||
| @@ -225,8 +225,12 @@ class WebTransportTransport { | ||
| if (!WebTransportMatcher.exactMatch(ma)) { | ||
| return false; | ||
| } | ||
| - const { url, certhashes } = parseMultiaddr(ma); | ||
| - return url != null && certhashes.length > 0; | ||
| + try { | ||
| + const { url, certhashes } = parseMultiaddr(ma); | ||
| + return url != null && certhashes.length > 0; | ||
| + } catch { | ||
| + return false; | ||
| + } | ||
| }); | ||
| } | ||
| } | ||
| diff --git a/node_modules/@libp2p/webtransport/src/index.ts b/node_modules/@libp2p/webtransport/src/index.ts | ||
| index 75f05ff..01eb51a 100644 | ||
| --- a/node_modules/@libp2p/webtransport/src/index.ts | ||
| +++ b/node_modules/@libp2p/webtransport/src/index.ts | ||
| @@ -306,9 +306,12 @@ class WebTransportTransport implements Transport<WebTransportDialEvents> { | ||
| return false | ||
| } | ||
|
|
||
| - const { url, certhashes } = parseMultiaddr(ma) | ||
| - | ||
| - return url != null && certhashes.length > 0 | ||
| + try { | ||
| + const { url, certhashes } = parseMultiaddr(ma) | ||
| + return url != null && certhashes.length > 0 | ||
| + } catch { | ||
| + return false | ||
| + } | ||
| }) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,208 @@ | ||
| import { create as createKuboRpcClient } from 'kubo-rpc-client' | ||
| import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' | ||
| import { WebSocket as NodeWebSocket } from 'undici' | ||
| import { test, expect } from './fixtures/config-test-fixtures.ts' | ||
| import { loadBypassingMediaViewer } from './fixtures/media-viewer.ts' | ||
| import type { Route } from '@playwright/test' | ||
| import type { MessageEvent } from 'undici' | ||
|
|
||
| /** | ||
| * The main test Kubo (http://127.0.0.1:8088) serves as both the HTTP | ||
| * trustless gateway and the bitswap peer (via its WS and WT listeners). | ||
| * Per-test: the HTTP gateway path is blocked (page.route → 500) so the SW | ||
| * falls back to bitswap, and the delegated-routing endpoint is mocked to | ||
| * return only the transport address(es) under test (WS, WSS, WT, or a mix). | ||
| * | ||
| * Playwright intercepts service-worker fetch() calls via page.route(), and | ||
| * routes registered inside a test body take LIFO priority over the catch-all | ||
| * in config-test-fixtures.ts. | ||
| */ | ||
| const MAIN_KUBO = 'http://127.0.0.1:8088' | ||
|
|
||
| test.describe('bitswap block retrieval', () => { | ||
| let testCidStr: string | ||
| let kuboPeerId: string | ||
| let wsAddr: string // /ip4/127.0.0.1/tcp/PORT/ws | ||
| let wsPort: number // TCP port extracted from wsAddr | ||
| let wssAddr: string // wsAddr with /ws → /wss; Playwright proxies wss:// to ws:// | ||
| let wtAddr: string // /ip4/127.0.0.1/udp/PORT/quic-v1/webtransport/certhash/… | ||
|
|
||
| test.beforeAll(async () => { | ||
| // Connect to the already-running main test Kubo via its RPC endpoint. | ||
| // global-setup.ts starts the node and sets process.env.KUBO_RPC. | ||
| const kuboRpc = createKuboRpcClient(process.env.KUBO_RPC) | ||
|
|
||
| const { cid } = await kuboRpc.add( | ||
| uint8ArrayFromString('hello from bitswap'), | ||
| { cidVersion: 1 } | ||
| ) | ||
| testCidStr = cid.toString() | ||
|
|
||
| const id = await kuboRpc.id() | ||
| kuboPeerId = id.id.toString() | ||
|
|
||
| const addrStrings = (await kuboRpc.swarm.localAddrs()).map(ma => ma.toString()) | ||
|
|
||
| const found = { | ||
| ws: addrStrings.find(s => s.endsWith('/ws')), | ||
| wt: addrStrings.find(s => s.includes('/quic-v1/webtransport')) | ||
| } | ||
| if (found.ws == null || found.wt == null) { | ||
| throw new Error( | ||
| `Expected WS and WebTransport addrs in localAddrs(), got:\n ${addrStrings.join('\n ')}` | ||
| ) | ||
| } | ||
| wsAddr = found.ws | ||
| wsPort = parseInt(wsAddr.split('/tcp/')[1].split('/')[0], 10) | ||
| wssAddr = wsAddr.replace(/\/ws$/, '/wss') | ||
| wtAddr = found.wt | ||
| }) | ||
|
|
||
| /** | ||
| * Block the HTTP trustless-gateway path so bitswap is the only option. | ||
| * The main Kubo has the test block, so without this the SW would succeed | ||
| * over HTTP and never exercise the bitswap path. | ||
| */ | ||
| async function mockGateway500 (page: any): Promise<void> { | ||
| await page.context().route(`${MAIN_KUBO}/ipfs/**`, async (route: Route) => { | ||
| await route.fulfill({ status: 500 }) | ||
| }) | ||
| } | ||
|
|
||
| /** | ||
| * Mock the delegated-routing endpoint so the SW discovers the main Kubo as | ||
| * a bitswap provider reachable at the given multiaddrs. Registered after | ||
| * the page fixture's catch-all route, so it takes LIFO priority. | ||
| */ | ||
| async function mockRouting (page: any, peerAddrs: string[]): Promise<void> { | ||
| const addrs = peerAddrs.map(addr => addr.includes('/p2p/') ? addr : `${addr}/p2p/${kuboPeerId}`) | ||
| const routeTarget = page.context() | ||
|
|
||
| // Mock out peer routing so it doesn't interfere with limitations imposed by | ||
| // the custom content routing | ||
| await routeTarget.route( | ||
| `${MAIN_KUBO}/routing/v1/peers/**`, | ||
| async (route: Route) => { | ||
| await route.fulfill({ status: 404 }) | ||
| } | ||
| ) | ||
|
|
||
| await routeTarget.route( | ||
| `${MAIN_KUBO}/routing/v1/providers/**`, | ||
| async (route: Route) => { | ||
| await route.fulfill({ | ||
| status: 200, | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ | ||
| Providers: [{ | ||
| Schema: 'peer', | ||
| ID: kuboPeerId, | ||
| Addrs: addrs, | ||
| Protocols: ['transport-bitswap'] | ||
| }] | ||
| }) | ||
| }) | ||
| } | ||
| ) | ||
| } | ||
|
|
||
| /** | ||
| * Intercept wss:// WebSocket connections from the service worker and proxy | ||
| * them to the plain ws:// Kubo node on the same port. | ||
| * | ||
| * Kubo does not auto-generate TLS certs for WSS listeners, so we advertise | ||
| * the plain WS port with a /wss suffix and let Playwright's routeWebSocket | ||
| * (CDP-based) terminate the "TLS" before it even reaches the network. | ||
| */ | ||
| async function mockWss (page: any, port: number): Promise<void> { | ||
| await page.routeWebSocket( | ||
| (url: URL) => url.protocol === 'wss:' && url.hostname === '127.0.0.1' && url.port === String(port), | ||
| (wsRoute: any) => { | ||
| const pending: (string | Buffer)[] = [] | ||
| const server = new NodeWebSocket(`ws://127.0.0.1:${port}`) | ||
| server.binaryType = 'arraybuffer' | ||
|
|
||
| wsRoute.onMessage((msg: string | Buffer) => { | ||
| if (server.readyState === NodeWebSocket.OPEN) { | ||
| server.send(msg) | ||
| } else { | ||
| pending.push(msg) | ||
| } | ||
| }) | ||
|
|
||
| server.addEventListener('open', () => { | ||
| for (const msg of pending) { server.send(msg) } | ||
| pending.length = 0 | ||
| server.addEventListener('message', (evt: MessageEvent<string | ArrayBuffer>) => { | ||
| const data = typeof evt.data === 'string' ? evt.data : Buffer.from(evt.data) | ||
| void wsRoute.send(data) | ||
| }) | ||
| }) | ||
|
|
||
| wsRoute.onClose(() => { server.close() }) | ||
| server.addEventListener('close', () => { void wsRoute.close() }) | ||
| server.addEventListener('error', () => { void wsRoute.close() }) | ||
| } | ||
| ) | ||
| } | ||
|
|
||
| test('retrieves content via bitswap over WS', async ({ page, baseURL }) => { | ||
| await mockGateway500(page) | ||
| await mockRouting(page, [wsAddr]) | ||
|
|
||
| const response = await loadBypassingMediaViewer(page, `${baseURL}/ipfs/${testCidStr}`) | ||
| expect(response.status()).toBe(200) | ||
| expect(await response.text()).toBe('hello from bitswap') | ||
| }) | ||
|
|
||
| test('retrieves content via bitswap over WebTransport', async ({ page, baseURL }) => { | ||
| await mockGateway500(page) | ||
| await mockRouting(page, [wtAddr]) | ||
|
|
||
| const response = await loadBypassingMediaViewer(page, `${baseURL}/ipfs/${testCidStr}`) | ||
| expect(response.status()).toBe(200) | ||
| expect(await response.text()).toBe('hello from bitswap') | ||
| }) | ||
|
|
||
| /** | ||
| * Additional transport coverage: WSS. | ||
| * | ||
| * Kubo listens on plain WS (/ws). We advertise the same port as /wss in the | ||
| * routing response. page.routeWebSocket() (CDP-based, Chromium) intercepts | ||
| * the wss:// connection from the service worker and proxies it to ws:// on | ||
| * the same port — no TLS cert infrastructure required. | ||
| * | ||
| * Skipped on non-Chromium: page.routeWebSocket() currently requires | ||
| * Chromium's CDP. WSS itself works in all browsers. | ||
| */ | ||
| test('retrieves content via bitswap over WSS (Playwright WS proxy)', async ({ page, baseURL }) => { | ||
| test.skip(test.info().project.name !== 'chromium', 'page.routeWebSocket() requires Chromium CDP') | ||
| await mockGateway500(page) | ||
| await mockWss(page, wsPort) | ||
| await mockRouting(page, [wssAddr]) | ||
|
|
||
| const response = await loadBypassingMediaViewer(page, `${baseURL}/ipfs/${testCidStr}`) | ||
| expect(response.status()).toBe(200) | ||
| expect(await response.text()).toBe('hello from bitswap') | ||
| }) | ||
|
|
||
| test('retrieves content via bitswap when routing returns both WS and WT addresses', async ({ page, baseURL }) => { | ||
| await mockGateway500(page) | ||
| await mockRouting(page, [wsAddr, wtAddr]) | ||
|
|
||
| const response = await loadBypassingMediaViewer(page, `${baseURL}/ipfs/${testCidStr}`) | ||
| expect(response.status()).toBe(200) | ||
| expect(await response.text()).toBe('hello from bitswap') | ||
| }) | ||
|
|
||
| test('fails bitswap retrieval when routing returns only invalid WT address', async ({ page, baseURL }) => { | ||
| const invalidWtAddr = wtAddr.replace(/\/certhash\/[^/]+/, '/certhash/not-a-valid-certhash') | ||
| expect(invalidWtAddr).not.toBe(wtAddr) | ||
|
|
||
| await mockGateway500(page) | ||
| await mockRouting(page, [invalidWtAddr]) | ||
|
|
||
| const response = await loadBypassingMediaViewer(page, `${baseURL}/ipfs/${testCidStr}`) | ||
| expect(response.status()).not.toBe(200) | ||
| }) | ||
| }) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
@achingbrain should we also block the routing/v1/peers endpoint to prevent discovering additional addresses for the peer and circumventing the test or does Helia not do this?
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.
Makes sense for a belt & braces approach.
js-libp2p will query the routing if the peer to dial has no known addresses so I think we are ok for now but I think there was some talk about querying if the dial fails.