Skip to content

Commit

Permalink
Fix router not working on some protocol
Browse files Browse the repository at this point in the history
- Added support for protocols other than http and https
- Added unit test
- Added integration test

Fixes vercel#16456
Fixes vercel#16595
  • Loading branch information
Cow258 committed Sep 11, 2020
1 parent a7a6187 commit a4d8808
Show file tree
Hide file tree
Showing 8 changed files with 255 additions and 14 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"coveralls": "3.0.3",
"cross-env": "6.0.3",
"cross-spawn": "6.0.5",
"electron": "^8.0.0",
"escape-string-regexp": "2.0.0",
"eslint": "6.8.0",
"eslint-plugin-import": "2.20.2",
Expand Down Expand Up @@ -116,6 +117,7 @@
"selenium-standalone": "6.18.0",
"selenium-webdriver": "4.0.0-alpha.7",
"shell-quote": "1.7.2",
"spectron": "^10.0.0",
"styled-components": "5.1.0",
"styled-jsx-plugin-postcss": "2.0.1",
"tailwindcss": "1.1.3",
Expand Down
29 changes: 15 additions & 14 deletions packages/next/next-server/lib/router/utils/parse-relative-url.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import { getLocationOrigin } from '../../utils'
import { searchParamsToUrlQuery } from './querystring'

const DUMMY_BASE = new URL(
let DUMMY_BASE = new URL(
typeof window === 'undefined' ? 'http://n' : getLocationOrigin()
)

/**
* Refresh DUMMY_BASE for unit test
*/
export function refreshDummyBase() {
DUMMY_BASE = new URL(
typeof window === 'undefined' ? 'http://n' : getLocationOrigin()
)
}

/**
* Parses path-relative urls (e.g. `/hello/world?foo=bar`). If url isn't path-relative
* (e.g. `./hello`) then at least base must be.
Expand All @@ -13,19 +22,11 @@ const DUMMY_BASE = new URL(
*/
export function parseRelativeUrl(url: string, base?: string) {
const resolvedBase = base ? new URL(base, DUMMY_BASE) : DUMMY_BASE
const {
pathname,
searchParams,
search,
hash,
href,
origin,
protocol,
} = new URL(url, resolvedBase)
if (
origin !== DUMMY_BASE.origin ||
(protocol !== 'http:' && protocol !== 'https:')
) {
const { pathname, searchParams, search, hash, href, origin } = new URL(
url,
resolvedBase
)
if (origin !== DUMMY_BASE.origin) {
throw new Error('invariant: invalid relative URL')
}
return {
Expand Down
6 changes: 6 additions & 0 deletions test/integration/parse-relative-url/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const path = require('path')
const outdir = path.join(__dirname, 'out')

module.exports = {
basePath: outdir,
}
10 changes: 10 additions & 0 deletions test/integration/parse-relative-url/pages/about.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Link from 'next/link'

export default () => (
<div id="about-page">
<p>This is the about page</p>
<Link href="/">
<a>Go Back</a>
</Link>
</div>
)
19 changes: 19 additions & 0 deletions test/integration/parse-relative-url/pages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Link from 'next/link'
import Router from 'next/router'

function routeToAbout(e) {
e.preventDefault()
Router.push('/about')
}

export default () => (
<div id="home-page">
<p>This is the home page</p>
<Link href="/about">
<a id="about-via-link">about via Link</a>
</Link>
<a id="about-via-router" onClick={routeToAbout} href="#">
about via Router
</a>
</div>
)
31 changes: 31 additions & 0 deletions test/integration/parse-relative-url/public/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const { join } = require('path')
const { app, BrowserWindow } = require('electron')
const url = require('url')

function createWindow() {
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: false,
},
})
mainWindow.loadURL(
url.format({
pathname: join(__dirname, 'index.html'),
protocol: 'file:',
slashes: true,
})
)
}

app.whenReady().then(() => {
createWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})

app.on('window-all-closed', () => {
app.quit()
})
65 changes: 65 additions & 0 deletions test/integration/parse-relative-url/test/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/* eslint-env jest */

import { join } from 'path'
import { nextBuild, nextExport } from 'next-test-utils'
import { Application } from 'spectron'
import electron from 'electron'

jest.setTimeout(1000 * 60 * 5)

const nextdir = join(__dirname, '../')
const outdir = join(nextdir, 'out')
const appdir = join(outdir, 'main.js')
let app = null

describe('Parse Relative Url', () => {
describe('File Protocol via Electron', () => {
beforeAll(async () => {
await nextBuild(nextdir)
await nextExport(nextdir, { outdir })

app = new Application({
path: electron,
args: [appdir],
})
await app.start()
})

afterAll(async () => {
if (app && app.isRunning()) {
await app.stop()
}
})

it('app init', async () => {
const count = await app.client.getWindowCount()
expect(count).toEqual(1)
})

it('should render the home page', async () => {
const text = await app.client.$('#home-page p').getText()
expect(text).toBe('This is the home page')
})

it('should do navigations via Link', async () => {
await app.client.$('#about-via-link').click()
const text = await app.client.$('#about-page p').getText()

expect(text).toBe('This is the about page')
})

it('should do back to home page via Link', async () => {
await app.client.$('#about-page a').click()
const text = await app.client.$('#home-page p').getText()

expect(text).toBe('This is the home page')
})

it('should do navigations via Router', async () => {
await app.client.$('#about-via-router').click()
const text = await app.client.$('#about-page p').getText()

expect(text).toBe('This is the about page')
})
})
})
107 changes: 107 additions & 0 deletions test/unit/parse-relative-url.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/* eslint-env jest */
import {
parseRelativeUrl,
refreshDummyBase,
} from 'next/dist/next-server/lib/router/utils/parse-relative-url'

// convenience function so tests can be aligned neatly
// and easy to eyeball
const check = (windowUrl, targetUrl, expected) => {
window.location = new URL(windowUrl)
refreshDummyBase()
if (typeof expected === 'string') {
expect(() => parseRelativeUrl(targetUrl)).toThrow(expected)
} else {
const parsedUrl = parseRelativeUrl(targetUrl)
expect(parsedUrl.pathname).toBe(expected.pathname)
expect(parsedUrl.search).toBe(expected.search)
expect(parsedUrl.hash).toBe(expected.hash)
}
}

describe('parseRelativeUrl', () => {
beforeAll(() => {
global.window = {
location: {},
}
})

afterAll(() => {
delete global.window
})

it('should parse relative url', () => {
check(
'http://example.com:3210/someA/pathB?fooC=barD#hashE',
'/someF/pathG?fooH=barI#hashJ',
{
pathname: '/someF/pathG',
search: '?fooH=barI',
hash: '#hashJ',
}
)
})

it('should parse relative url when start with dot', () => {
check(
'http://example.com:3210/someA/pathB?fooC=barD#hashE',
'./someF/pathG?fooH=barI#hashJ',
{
pathname: '/someF/pathG',
search: '?fooH=barI',
hash: '#hashJ',
}
)
})

it('should parse relative url on special protocol', () => {
check(
'ionic://localhost/someA/pathB?fooC=barD#hashE',
'/someF/pathG?fooH=barI#hashJ',
{
pathname: '/someF/pathG',
search: '?fooH=barI',
hash: '#hashJ',
}
)
check(
'file:///someA/pathB?fooC=barD#hashE',
'/someF/pathG?fooH=barI#hashJ',
{
pathname: '/someF/pathG',
search: '?fooH=barI',
hash: '#hashJ',
}
)
})

it('should throw when parsing the full url', () => {
check(
'http://example.com:3210/someA/pathB?fooC=barD#hashE',
'http://example.com:3210/someF/pathG?fooH=barI#hashJ',
{
pathname: '/someF/pathG',
search: '?fooH=barI',
hash: '#hashJ',
}
)
})

it('should throw when parsing the special prefix', () => {
check(
'http://example.com:3210/someA/pathB?fooC=barD#hashE',
'mailto:foo@example.com',
'invariant: invalid relative URL'
)
check(
'http://example.com:3210/someA/pathB?fooC=barD#hashE',
'tel:+123456789',
'invariant: invalid relative URL'
)
check(
'http://example.com:3210/someA/pathB?fooC=barD#hashE',
'sms:+123456789',
'invariant: invalid relative URL'
)
})
})

0 comments on commit a4d8808

Please sign in to comment.