Skip to content

Commit fcda086

Browse files
authored
make enterprise 3.0 redirects work (github#26041)
* make enterprise 3.0 redirects work * improve tests and better variable name
1 parent 994340b commit fcda086

File tree

5 files changed

+181
-12
lines changed

5 files changed

+181
-12
lines changed

lib/enterprise-server-releases.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,18 @@ export const next = '3.5'
1414
export const nextNext = '3.6'
1515

1616
export const supported = ['3.4', '3.3', '3.2', '3.1']
17+
18+
// This indicates the point where we started treating redirect lookups
19+
// to be a *function* rather than a big *lookup object*.
20+
// This is important distinguish because we need to leverage that
21+
// when dealing with redirects specifically in these archived
22+
// enterprise versions.
23+
// When you're archiving a version, add the new archived number to this
24+
// array and you should never need to touch the `deprecated` array
25+
// on the line just below.
26+
export const deprecatedWithFunctionalRedirects = ['3.0']
1727
export const deprecated = [
18-
'3.0',
28+
...deprecatedWithFunctionalRedirects,
1929
'2.22',
2030
'2.21',
2131
'2.20',

lib/get-redirect.js

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,32 @@ import { languageKeys } from './languages.js'
22
import nonEnterpriseDefaultVersion from './non-enterprise-default-version.js'
33

44
import { allVersions } from './all-versions.js'
5-
import { latest, supported } from './enterprise-server-releases.js'
5+
import {
6+
latest,
7+
supported,
8+
deprecatedWithFunctionalRedirects,
9+
} from './enterprise-server-releases.js'
610

711
const languagePrefixRegex = new RegExp(`^/(${languageKeys.join('|')})/`)
812
const nonEnterpriseDefaultVersionPrefix = `/${nonEnterpriseDefaultVersion}`
913

10-
// Return the new URI if there is one, otherwise return undefined.
11-
export default function getRedirect(uri, context) {
12-
const { redirects, userLanguage } = context
14+
const supportedAndRecentlyDeprecated = [...supported, ...deprecatedWithFunctionalRedirects]
1315

16+
export function splitPathByLanguage(uri, userLanguage) {
1417
let language = userLanguage || 'en'
1518
let withoutLanguage = uri
1619
if (languagePrefixRegex.test(uri)) {
1720
language = uri.match(languagePrefixRegex)[1]
1821
withoutLanguage = uri.replace(languagePrefixRegex, '/')
1922
}
23+
return [language, withoutLanguage]
24+
}
25+
26+
// Return the new URI if there is one, otherwise return undefined.
27+
export default function getRedirect(uri, context) {
28+
const { redirects, userLanguage } = context
29+
30+
const [language, withoutLanguage] = splitPathByLanguage(uri, userLanguage)
2031

2132
let destination
2233

@@ -54,7 +65,6 @@ export default function getRedirect(uri, context) {
5465
if (withoutLanguage === '/enterprise-server') {
5566
return basicCorrection
5667
}
57-
// console.log({ basicCorrection })
5868
} else if (withoutLanguage.startsWith('/enterprise-server@latest')) {
5969
// E.g. '/enterprise-server@latest' or '/enterprise-server@latest/3.3/foo'
6070
basicCorrection =
@@ -67,9 +77,9 @@ export default function getRedirect(uri, context) {
6777
}
6878
} else if (
6979
withoutLanguage.startsWith('/enterprise/') &&
70-
supported.includes(withoutLanguage.split('/')[2])
80+
supportedAndRecentlyDeprecated.includes(withoutLanguage.split('/')[2])
7181
) {
72-
// E.g. '/enterprise/3.3' or '/enterprise/3.3/foo'
82+
// E.g. '/enterprise/3.3' or '/enterprise/3.3/foo' or '/enterprise/3.0/foo
7383

7484
// If the URL is without a language, and no redirect is necessary,
7585
// but it has as version prefix, the language has to be there

middleware/archived-enterprise-versions.js

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import statsd from '../lib/statsd.js'
44
import {
55
firstVersionDeprecatedOnNewSite,
66
lastVersionWithoutArchivedRedirectsFile,
7+
deprecatedWithFunctionalRedirects,
78
} from '../lib/enterprise-server-releases.js'
89
import patterns from '../lib/patterns.js'
910
import versionSatisfiesRange from '../lib/version-satisfies-range.js'
@@ -13,6 +14,7 @@ import got from 'got'
1314
import { readCompressedJsonFileFallbackLazily } from '../lib/read-json-file.js'
1415
import { cacheControlFactory } from './cache-control.js'
1516
import { pathLanguagePrefixed, languagePrefixPathRegex } from '../lib/languages.js'
17+
import getRedirect, { splitPathByLanguage } from '../lib/get-redirect.js'
1618

1719
function splitByLanguage(uri) {
1820
let language = null
@@ -35,6 +37,7 @@ const archivedFrontmatterFallbacks = readCompressedJsonFileFallbackLazily(
3537
)
3638

3739
const cacheControl = cacheControlFactory(60 * 60 * 24 * 365)
40+
const noCacheControl = cacheControlFactory(0)
3841

3942
// Combine all the things you need to make sure the response is
4043
// aggresively cached.
@@ -99,6 +102,39 @@ export default async function archivedEnterpriseVersions(req, res, next) {
99102

100103
const redirectCode = pathLanguagePrefixed(req.path) ? 301 : 302
101104

105+
if (deprecatedWithFunctionalRedirects.includes(requestedVersion)) {
106+
const redirectTo = getRedirect(req.path, req.context)
107+
if (redirectTo) {
108+
if (redirectCode === 301) {
109+
cacheControl(res)
110+
} else {
111+
noCacheControl(res)
112+
}
113+
res.removeHeader('set-cookie')
114+
return res.redirect(redirectCode, redirectTo)
115+
}
116+
117+
const redirectJson = await getRemoteJSON(getProxyPath('redirects.json', requestedVersion), {
118+
retry: retryConfiguration,
119+
// This is allowed to be different compared to the other requests
120+
// we make because downloading the `redirects.json` once is very
121+
// useful because it caches so well.
122+
// And, as of 2021 that `redirects.json` is 10MB so it's more likely
123+
// to time out.
124+
timeout: 1000,
125+
})
126+
const [language, withoutLanguage] = splitPathByLanguage(req.path, req.context.userLanguage)
127+
const newRedirectTo = redirectJson[withoutLanguage]
128+
if (newRedirectTo) {
129+
if (redirectCode === 301) {
130+
cacheControl(res)
131+
} else {
132+
noCacheControl(res)
133+
}
134+
res.removeHeader('set-cookie')
135+
return res.redirect(redirectCode, `/${language}${newRedirectTo}`)
136+
}
137+
}
102138
// redirect language-prefixed URLs like /en/enterprise/2.10 -> /enterprise/2.10
103139
// (this only applies to versions <2.13)
104140
if (
@@ -135,7 +171,10 @@ export default async function archivedEnterpriseVersions(req, res, next) {
135171
}
136172
}
137173

138-
if (versionSatisfiesRange(requestedVersion, `>${lastVersionWithoutArchivedRedirectsFile}`)) {
174+
if (
175+
versionSatisfiesRange(requestedVersion, `>${lastVersionWithoutArchivedRedirectsFile}`) &&
176+
!deprecatedWithFunctionalRedirects.includes(requestedVersion)
177+
) {
139178
const redirectJson = await getRemoteJSON(getProxyPath('redirects.json', requestedVersion), {
140179
retry: retryConfiguration,
141180
// This is allowed to be different compared to the other requests

tests/routing/deprecated-enterprise-versions.js

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import supertest from 'supertest'
2+
import { describe, jest, test } from '@jest/globals'
3+
14
import createApp from '../../lib/app.js'
25
import enterpriseServerReleases from '../../lib/enterprise-server-releases.js'
36
import { get, getDOM } from '../helpers/supertest.js'
47
import { SURROGATE_ENUMS } from '../../middleware/set-fastly-surrogate-key.js'
5-
import supertest from 'supertest'
6-
import { jest } from '@jest/globals'
8+
import { PREFERRED_LOCALE_COOKIE_NAME } from '../../middleware/detect-language.js'
79

810
jest.useFakeTimers('legacy')
911

@@ -89,6 +91,73 @@ describe('enterprise deprecation', () => {
8991
})
9092
})
9193

94+
// Starting with the deprecation of 3.0, it's the first time we deprecate
95+
// enterprise versions since redirects is a *function* rather than a
96+
// lookup in a big object.
97+
describe('recently deprecated redirects', () => {
98+
test('basic enterprise 3.0 redirects', async () => {
99+
const res = await get('/enterprise/3.0')
100+
expect(res.statusCode).toBe(302)
101+
expect(res.headers.location).toBe('/en/[email protected]')
102+
expect(res.headers['set-cookie']).toBeUndefined()
103+
// Deliberately no cache control because it is user-dependent
104+
expect(res.headers['cache-control']).toBe('private, no-store')
105+
})
106+
test('basic enterprise 3.0 redirects by cookie', async () => {
107+
const res = await get('/enterprise/3.0', {
108+
headers: {
109+
Cookie: `${PREFERRED_LOCALE_COOKIE_NAME}=ja`,
110+
},
111+
})
112+
expect(res.statusCode).toBe(302)
113+
expect(res.headers.location).toBe('/ja/[email protected]')
114+
})
115+
test('already languaged enterprise 3.0 redirects', async () => {
116+
const res = await get('/en/enterprise/3.0')
117+
expect(res.statusCode).toBe(301)
118+
expect(res.headers.location).toBe('/en/[email protected]')
119+
// 301 redirects are safe to cache aggressively
120+
expect(res.headers['set-cookie']).toBeUndefined()
121+
expect(res.headers['cache-control']).toContain('public')
122+
expect(res.headers['cache-control']).toMatch(/max-age=\d+/)
123+
})
124+
test('redirects enterprise-server 3.0 with actual redirect without language', async () => {
125+
const res = await get(
126+
'/[email protected]/github/getting-started-with-github/githubs-products'
127+
)
128+
expect(res.statusCode).toBe(302)
129+
expect(res.headers['set-cookie']).toBeUndefined()
130+
// Deliberately no cache control because it is user-dependent
131+
expect(res.headers['cache-control']).toBe('private, no-store')
132+
// This is based on
133+
// https://github.com/github/help-docs-archived-enterprise-versions/blob/master/3.0/redirects.json
134+
expect(res.headers.location).toBe(
135+
'/en/[email protected]/get-started/learning-about-github/githubs-products'
136+
)
137+
})
138+
test('redirects enterprise-server 3.0 with actual redirect with language', async () => {
139+
const res = await get(
140+
'/ja/[email protected]/github/getting-started-with-github/githubs-products'
141+
)
142+
expect(res.statusCode).toBe(301)
143+
expect(res.headers['set-cookie']).toBeUndefined()
144+
expect(res.headers['cache-control']).toContain('public')
145+
expect(res.headers['cache-control']).toMatch(/max-age=\d+/)
146+
// This is based on
147+
// https://github.com/github/help-docs-archived-enterprise-versions/blob/master/3.0/redirects.json
148+
expect(res.headers.location).toBe(
149+
'/ja/[email protected]/get-started/learning-about-github/githubs-products'
150+
)
151+
})
152+
test('follow redirects enterprise-server 3.0 with actual redirect without language', async () => {
153+
const res = await get(
154+
'/[email protected]/github/getting-started-with-github/githubs-products',
155+
{ followAllRedirects: true }
156+
)
157+
expect(res.statusCode).toBe(200)
158+
})
159+
})
160+
92161
describe('deprecation banner', () => {
93162
test('renders a deprecation warning banner on oldest supported Enterprise version', async () => {
94163
const $ = await getDOM(`/en/enterprise/${enterpriseServerReleases.oldestSupported}`)

tests/unit/get-redirect.js

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,36 @@
1-
import getRedirect from '../../lib/get-redirect.js'
1+
import { describe, expect, test } from '@jest/globals'
2+
3+
import getRedirect, { splitPathByLanguage } from '../../lib/get-redirect.js'
24
import { latest } from '../../lib/enterprise-server-releases.js'
35

6+
describe('splitPathByLanguage', () => {
7+
test('basic', () => {
8+
const [language, withoutLanguage] = splitPathByLanguage('/foo/')
9+
expect(language).toBe('en')
10+
expect(withoutLanguage).toBe('/foo/')
11+
})
12+
test('already has /en in it', () => {
13+
const [language, withoutLanguage] = splitPathByLanguage('/en/foo/')
14+
expect(language).toBe('en')
15+
expect(withoutLanguage).toBe('/foo/')
16+
})
17+
test('basic with different fallback', () => {
18+
const [language, withoutLanguage] = splitPathByLanguage('/foo/', 'ja')
19+
expect(language).toBe('ja')
20+
expect(withoutLanguage).toBe('/foo/')
21+
})
22+
test('already has /en different fallback', () => {
23+
const [language, withoutLanguage] = splitPathByLanguage('/en/foo/', 'ja')
24+
expect(language).toBe('en')
25+
expect(withoutLanguage).toBe('/foo/')
26+
})
27+
test('unrecognized prefix is ignored', () => {
28+
const [language, withoutLanguage] = splitPathByLanguage('/sv/foo/')
29+
expect(language).toBe('en')
30+
expect(withoutLanguage).toBe('/sv/foo/')
31+
})
32+
})
33+
434
describe('getRedirect basics', () => {
535
it('should sometimes not correct the version prefix', () => {
636
// This essentially tests legacy entries that come from the
@@ -162,4 +192,15 @@ describe('getRedirect basics', () => {
162192
ctx.userLanguage = null
163193
expect(getRedirect('/foo', ctx)).toBe(`/en/bar`)
164194
})
195+
196+
it('should work for some deprecated enterprise-server URLs too', () => {
197+
// Starting with enterprise-server 3.0, we have made redirects become
198+
// a *function* rather than a lookup on a massive object.
199+
const ctx = {
200+
pages: {},
201+
redirects: {},
202+
}
203+
expect(getRedirect('/enterprise/3.0', ctx)).toBe('/en/[email protected]')
204+
expect(getRedirect('/enterprise/3.0/foo', ctx)).toBe('/en/[email protected]/foo')
205+
})
165206
})

0 commit comments

Comments
 (0)