Skip to content

Commit 7e14b9a

Browse files
jsjoeiocode-asher
authored andcommitted
feat(http): keep slashes in queryParams in redirects (coder#4928)
* refactor(http): extract logic into constructRedirectPath This allows us to easily test our redirect path construction logic where we get the relative path, the query string and construct a redirect path. By extracting this from `redirect`, we can easily test this logic in a unit test. I did this so we could test some logic where slashes in query strings should be made human-friendly for users. * feat(testing): add tests for constructRedirectPath Co-authored-by: Asher <[email protected]>
1 parent 5714370 commit 7e14b9a

File tree

4 files changed

+67
-4
lines changed

4 files changed

+67
-4
lines changed

src/node/http.ts

+16-3
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,21 @@ export const relativeRoot = (originalUrl: string): string => {
138138
return normalize("./" + (depth > 1 ? "../".repeat(depth - 1) : ""))
139139
}
140140

141+
/**
142+
* A helper function to construct a redirect path based on
143+
* an Express Request, query and a path to redirect to.
144+
*
145+
* Redirect path is relative to `/${to}`.
146+
*/
147+
export const constructRedirectPath = (req: express.Request, query: qs.ParsedQs, to: string): string => {
148+
const relativePath = normalize(`${relativeRoot(req.originalUrl)}/${to}`, true)
149+
// %2f or %2F are both equalivent to an encoded slash /
150+
const queryString = qs.stringify(query).replace(/%2[fF]/g, "/")
151+
const redirectPath = `${relativePath}${queryString ? `?${queryString}` : ""}`
152+
153+
return redirectPath
154+
}
155+
141156
/**
142157
* Redirect relatively to `/${to}`. Query variables on the current URI will be
143158
* preserved. `to` should be a simple path without any query parameters
@@ -156,9 +171,7 @@ export const redirect = (
156171
}
157172
})
158173

159-
const relativePath = normalize(`${relativeRoot(req.originalUrl)}/${to}`, true)
160-
const queryString = qs.stringify(query)
161-
const redirectPath = `${relativePath}${queryString ? `?${queryString}` : ""}`
174+
const redirectPath = constructRedirectPath(req, query, to)
162175
logger.debug(`redirecting from ${req.originalUrl} to ${redirectPath}`)
163176
res.redirect(redirectPath)
164177
}

test/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"license": "MIT",
33
"#": "We must put jest in a sub-directory otherwise VS Code somehow picks up the types and generates conflicts with mocha.",
44
"devDependencies": {
5+
"@jest-mock/express": "^1.4.5",
56
"@playwright/test": "^1.16.3",
67
"@types/jest": "^27.0.2",
78
"@types/jsdom": "^16.2.13",

test/unit/node/http.test.ts

+45-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { relativeRoot } from "../../../src/node/http"
1+
import { getMockReq } from "@jest-mock/express"
2+
import { constructRedirectPath, relativeRoot } from "../../../src/node/http"
23

34
describe("http", () => {
45
it("should construct a relative path to the root", () => {
@@ -9,3 +10,46 @@ describe("http", () => {
910
expect(relativeRoot("/foo/bar/")).toStrictEqual("./../..")
1011
})
1112
})
13+
14+
describe("constructRedirectPath", () => {
15+
it("should preserve slashes in queryString so they are human-readable", () => {
16+
const mockReq = getMockReq({
17+
originalUrl: "localhost:8080",
18+
})
19+
const mockQueryParams = { folder: "/Users/jp/dev/coder" }
20+
const mockTo = ""
21+
const actual = constructRedirectPath(mockReq, mockQueryParams, mockTo)
22+
const expected = "./?folder=/Users/jp/dev/coder"
23+
expect(actual).toBe(expected)
24+
})
25+
it("should use an empty string if no query params", () => {
26+
const mockReq = getMockReq({
27+
originalUrl: "localhost:8080",
28+
})
29+
const mockQueryParams = {}
30+
const mockTo = ""
31+
const actual = constructRedirectPath(mockReq, mockQueryParams, mockTo)
32+
const expected = "./"
33+
expect(actual).toBe(expected)
34+
})
35+
it("should append the 'to' path relative to the originalUrl", () => {
36+
const mockReq = getMockReq({
37+
originalUrl: "localhost:8080",
38+
})
39+
const mockQueryParams = {}
40+
const mockTo = "vscode"
41+
const actual = constructRedirectPath(mockReq, mockQueryParams, mockTo)
42+
const expected = "./vscode"
43+
expect(actual).toBe(expected)
44+
})
45+
it("should append append queryParams after 'to' path", () => {
46+
const mockReq = getMockReq({
47+
originalUrl: "localhost:8080",
48+
})
49+
const mockQueryParams = { folder: "/Users/jp/dev/coder" }
50+
const mockTo = "vscode"
51+
const actual = constructRedirectPath(mockReq, mockQueryParams, mockTo)
52+
const expected = "./vscode?folder=/Users/jp/dev/coder"
53+
expect(actual).toBe(expected)
54+
})
55+
})

test/yarn.lock

+5
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,11 @@
478478
resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98"
479479
integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==
480480

481+
"@jest-mock/express@^1.4.5":
482+
version "1.4.5"
483+
resolved "https://registry.yarnpkg.com/@jest-mock/express/-/express-1.4.5.tgz#437db24ccd505d88f8c0d73e8593fa3cd6eb273b"
484+
integrity sha512-bERM1jnutyH7VMahdaOHAKy7lgX47zJ7+RTz2eMz0wlCttd9CkhsKFEyoWmJBSz/ow0nVj3lCuRqLem4QDYFkQ==
485+
481486
"@jest/console@^27.4.6":
482487
version "27.4.6"
483488
resolved "https://registry.yarnpkg.com/@jest/console/-/console-27.4.6.tgz#0742e6787f682b22bdad56f9db2a8a77f6a86107"

0 commit comments

Comments
 (0)