Archived
1
0

feat(http): keep slashes in queryParams in redirects (#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 <ash@coder.com>
This commit is contained in:
Joe Previte 2022-03-01 12:11:56 -07:00 committed by GitHub
parent 1465d8d510
commit 506d3f43ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 67 additions and 4 deletions

View File

@ -138,6 +138,21 @@ export const relativeRoot = (originalUrl: string): string => {
return normalize("./" + (depth > 1 ? "../".repeat(depth - 1) : "")) return normalize("./" + (depth > 1 ? "../".repeat(depth - 1) : ""))
} }
/**
* A helper function to construct a redirect path based on
* an Express Request, query and a path to redirect to.
*
* Redirect path is relative to `/${to}`.
*/
export const constructRedirectPath = (req: express.Request, query: qs.ParsedQs, to: string): string => {
const relativePath = normalize(`${relativeRoot(req.originalUrl)}/${to}`, true)
// %2f or %2F are both equalivent to an encoded slash /
const queryString = qs.stringify(query).replace(/%2[fF]/g, "/")
const redirectPath = `${relativePath}${queryString ? `?${queryString}` : ""}`
return redirectPath
}
/** /**
* Redirect relatively to `/${to}`. Query variables on the current URI will be * Redirect relatively to `/${to}`. Query variables on the current URI will be
* preserved. `to` should be a simple path without any query parameters * preserved. `to` should be a simple path without any query parameters
@ -156,9 +171,7 @@ export const redirect = (
} }
}) })
const relativePath = normalize(`${relativeRoot(req.originalUrl)}/${to}`, true) const redirectPath = constructRedirectPath(req, query, to)
const queryString = qs.stringify(query)
const redirectPath = `${relativePath}${queryString ? `?${queryString}` : ""}`
logger.debug(`redirecting from ${req.originalUrl} to ${redirectPath}`) logger.debug(`redirecting from ${req.originalUrl} to ${redirectPath}`)
res.redirect(redirectPath) res.redirect(redirectPath)
} }

View File

@ -2,6 +2,7 @@
"license": "MIT", "license": "MIT",
"#": "We must put jest in a sub-directory otherwise VS Code somehow picks up the types and generates conflicts with mocha.", "#": "We must put jest in a sub-directory otherwise VS Code somehow picks up the types and generates conflicts with mocha.",
"devDependencies": { "devDependencies": {
"@jest-mock/express": "^1.4.5",
"@playwright/test": "^1.16.3", "@playwright/test": "^1.16.3",
"@types/jest": "^27.0.2", "@types/jest": "^27.0.2",
"@types/jsdom": "^16.2.13", "@types/jsdom": "^16.2.13",

View File

@ -1,4 +1,5 @@
import { relativeRoot } from "../../../src/node/http" import { getMockReq } from "@jest-mock/express"
import { constructRedirectPath, relativeRoot } from "../../../src/node/http"
describe("http", () => { describe("http", () => {
it("should construct a relative path to the root", () => { it("should construct a relative path to the root", () => {
@ -9,3 +10,46 @@ describe("http", () => {
expect(relativeRoot("/foo/bar/")).toStrictEqual("./../..") expect(relativeRoot("/foo/bar/")).toStrictEqual("./../..")
}) })
}) })
describe("constructRedirectPath", () => {
it("should preserve slashes in queryString so they are human-readable", () => {
const mockReq = getMockReq({
originalUrl: "localhost:8080",
})
const mockQueryParams = { folder: "/Users/jp/dev/coder" }
const mockTo = ""
const actual = constructRedirectPath(mockReq, mockQueryParams, mockTo)
const expected = "./?folder=/Users/jp/dev/coder"
expect(actual).toBe(expected)
})
it("should use an empty string if no query params", () => {
const mockReq = getMockReq({
originalUrl: "localhost:8080",
})
const mockQueryParams = {}
const mockTo = ""
const actual = constructRedirectPath(mockReq, mockQueryParams, mockTo)
const expected = "./"
expect(actual).toBe(expected)
})
it("should append the 'to' path relative to the originalUrl", () => {
const mockReq = getMockReq({
originalUrl: "localhost:8080",
})
const mockQueryParams = {}
const mockTo = "vscode"
const actual = constructRedirectPath(mockReq, mockQueryParams, mockTo)
const expected = "./vscode"
expect(actual).toBe(expected)
})
it("should append append queryParams after 'to' path", () => {
const mockReq = getMockReq({
originalUrl: "localhost:8080",
})
const mockQueryParams = { folder: "/Users/jp/dev/coder" }
const mockTo = "vscode"
const actual = constructRedirectPath(mockReq, mockQueryParams, mockTo)
const expected = "./vscode?folder=/Users/jp/dev/coder"
expect(actual).toBe(expected)
})
})

View File

@ -478,6 +478,11 @@
resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98"
integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==
"@jest-mock/express@^1.4.5":
version "1.4.5"
resolved "https://registry.yarnpkg.com/@jest-mock/express/-/express-1.4.5.tgz#437db24ccd505d88f8c0d73e8593fa3cd6eb273b"
integrity sha512-bERM1jnutyH7VMahdaOHAKy7lgX47zJ7+RTz2eMz0wlCttd9CkhsKFEyoWmJBSz/ow0nVj3lCuRqLem4QDYFkQ==
"@jest/console@^27.4.6": "@jest/console@^27.4.6":
version "27.4.6" version "27.4.6"
resolved "https://registry.yarnpkg.com/@jest/console/-/console-27.4.6.tgz#0742e6787f682b22bdad56f9db2a8a77f6a86107" resolved "https://registry.yarnpkg.com/@jest/console/-/console-27.4.6.tgz#0742e6787f682b22bdad56f9db2a8a77f6a86107"