| | |
| | import CspParse from 'csp-parse' |
| | import { beforeAll, describe, expect, test, vi } from 'vitest' |
| |
|
| | import enterpriseServerReleases from '@/versions/lib/enterprise-server-releases' |
| | import { get, getDOM, head, post } from '@/tests/helpers/e2etest' |
| | import { describeViaActionsOnly } from '@/tests/helpers/conditional-runs' |
| | import { loadPages } from '@/frame/lib/page-data' |
| | import { |
| | SURROGATE_ENUMS, |
| | makeLanguageSurrogateKey, |
| | } from '@/frame/middleware/set-fastly-surrogate-key' |
| |
|
| | interface Category { |
| | name: string |
| | published_articles: string[] |
| | } |
| |
|
| | describe('server', () => { |
| | vi.setConfig({ testTimeout: 60 * 1000 }) |
| |
|
| | beforeAll(async () => { |
| | |
| | |
| | |
| | const res = await get('/en') |
| | expect(res.statusCode).toBe(200) |
| | }) |
| |
|
| | test('supports HEAD requests', async () => { |
| | const res = await head('/en') |
| | expect(res.statusCode).toBe(200) |
| | expect(res.headers['content-length']).toBe('0') |
| | expect(res.body).toBe('') |
| | |
| | |
| | |
| | expect(res.headers['cache-control']).toContain('public') |
| | expect(res.headers['cache-control']).toMatch(/max-age=\d+/) |
| | }) |
| |
|
| | test('renders the homepage', async () => { |
| | const res = await get('/en') |
| | expect(res.statusCode).toBe(200) |
| | }) |
| |
|
| | test('sets Content Security Policy (CSP) headers', async () => { |
| | const res = await get('/en') |
| | expect(res.statusCode).toBe(200) |
| | expect('content-security-policy' in res.headers).toBe(true) |
| |
|
| | const csp = new CspParse(res.headers['content-security-policy']) |
| | expect(csp.get('default-src')).toBe("'none'") |
| |
|
| | expect(csp.get('font-src').includes("'self'")).toBe(true) |
| |
|
| | expect(csp.get('connect-src').includes("'self'")).toBe(true) |
| |
|
| | expect(csp.get('img-src').includes("'self'")).toBe(true) |
| |
|
| | expect(csp.get('script-src').includes("'self'")).toBe(true) |
| |
|
| | expect(csp.get('style-src').includes("'self'")).toBe(true) |
| | expect(csp.get('style-src').includes("'unsafe-inline'")).toBe(true) |
| |
|
| | expect(csp.get('manifest-src').includes("'self'")).toBe(true) |
| | }) |
| |
|
| | test('sets Fastly cache control headers', async () => { |
| | const res = await get('/en') |
| | expect(res.statusCode).toBe(200) |
| | expect(res.headers['cache-control']).toMatch(/public, max-age=/) |
| |
|
| | const surrogateKeySplit = res.headers['surrogate-key'].split(/\s/g) |
| | expect(surrogateKeySplit.includes(SURROGATE_ENUMS.DEFAULT)).toBeTruthy() |
| | expect(surrogateKeySplit.includes(makeLanguageSurrogateKey('en'))).toBeTruthy() |
| | }) |
| |
|
| | test('does not render duplicate <html> or <body> tags', async () => { |
| | const $ = await getDOM('/en') |
| | expect($('html').length).toBe(1) |
| | expect($('body').length).toBe(1) |
| | }) |
| |
|
| | test('renders a 404 page', async () => { |
| | |
| | |
| | const $ = await getDOM('/en/not-a-real-page', { allow404: true }) |
| | expect($('h1').first().text()).toBe('Ooops!') |
| | |
| | expect(($ as any).text().includes("It looks like this page doesn't exist.")).toBe(true) |
| | expect( |
| | ($ as any) |
| | .text() |
| | .includes( |
| | 'We track these errors automatically, but if the problem persists please feel free to contact us.', |
| | ), |
| | ).toBe(true) |
| | expect($.res.statusCode).toBe(404) |
| | }) |
| |
|
| | test('renders a 404 for language prefixed versioned non-existent pages', async () => { |
| | const res = await get('/en/enterprise-cloud@latest/nonexistent-page') |
| | expect(res.statusCode).toBe(404) |
| | }) |
| |
|
| | |
| | |
| | |
| | |
| | test.skip('renders a 400 for invalid paths', async () => { |
| | const $ = await getDOM('/en/%7B%') |
| | expect($.res.statusCode).toBe(400) |
| | }) |
| |
|
| | test('renders a 500 page when errors are thrown', async () => { |
| | const $ = await getDOM('/_500', { allow500s: true }) |
| | expect($('h1').first().text()).toBe('Ooops!') |
| | |
| | expect(($ as any).text().includes('It looks like something went wrong.')).toBe(true) |
| | expect( |
| | ($ as any) |
| | .text() |
| | .includes( |
| | 'We track these errors automatically, but if the problem persists please feel free to contact us.', |
| | ), |
| | ).toBe(true) |
| | expect($.res.statusCode).toBe(500) |
| | }) |
| |
|
| | test('returns a 400 when POST-ed invalid JSON', async () => { |
| | const res = await post('/', { |
| | body: 'not real JSON', |
| | headers: { |
| | 'content-type': 'application/json', |
| | }, |
| | }) |
| | expect(res.statusCode).toBe(400) |
| | }) |
| |
|
| | |
| | test('does not use cached intros in subcategories', async () => { |
| | let $ = await getDOM( |
| | '/en/get-started/importing-your-projects-to-github/importing-source-code-to-github/importing-a-git-repository-using-the-command-line', |
| | ) |
| | const articleIntro = $('[data-testid="lead"]').text() |
| | $ = await getDOM( |
| | '/en/enterprise/2.16/user/importing-your-projects-to-github/importing-source-code-to-github', |
| | ) |
| | const subcategoryIntro = $('.subcategory').first().next().text() |
| | expect(articleIntro).not.toEqual(subcategoryIntro) |
| | }) |
| |
|
| | test('serves /categories.json for support team usage', async () => { |
| | const res = await get('/categories.json') |
| | expect(res.statusCode).toBe(200) |
| |
|
| | |
| | expect(res.headers['access-control-allow-origin']).toBe('*') |
| |
|
| | |
| | expect(res.headers['set-cookie']).toBeUndefined() |
| | expect(res.headers['cache-control']).toContain('public') |
| | expect(res.headers['cache-control']).toMatch(/max-age=[1-9]/) |
| |
|
| | const categories = JSON.parse(res.body) |
| | expect(Array.isArray(categories)).toBe(true) |
| | expect(categories.length).toBeGreaterThan(1) |
| | for (const category of categories as Category[]) { |
| | expect('name' in category).toBe(true) |
| | expect('published_articles' in category).toBe(true) |
| | } |
| | }) |
| |
|
| | describeViaActionsOnly('Early Access articles', () => { |
| | test('have noindex meta tags', async () => { |
| | const allPages = await loadPages() |
| | |
| | |
| | |
| | |
| | |
| | const hiddenPages = allPages.filter( |
| | (page) => |
| | page.languageCode === 'en' && |
| | page.hidden && |
| | page.relativePath.startsWith('early-access') && |
| | !page.relativePath.endsWith('index.md'), |
| | ) |
| | for (const { href } of hiddenPages[0].permalinks) { |
| | const $ = await getDOM(href) |
| | expect($('meta[content="noindex"]').length).toBe(1) |
| | } |
| | }) |
| | }) |
| |
|
| | describe('redirects', () => { |
| | test('redirects old articles to their English URL', async () => { |
| | const res = await get('/articles/deleting-a-team', { followRedirects: false }) |
| | expect(res.statusCode).toBe(302) |
| | expect(res.headers['set-cookie']).toBeUndefined() |
| | |
| | expect(res.headers['cache-control']).toContain('public') |
| | expect(res.headers['cache-control']).toMatch(/max-age=[1-9]/) |
| | expect(res.headers.vary).toContain('accept-language') |
| | expect(res.headers.vary).toContain('x-user-language') |
| | }) |
| |
|
| | test('redirects / to /en when no language preference is specified', async () => { |
| | const res = await get('/') |
| | expect(res.statusCode).toBe(302) |
| | expect(res.headers.location).toBe('/en') |
| | expect(res.headers['set-cookie']).toBeUndefined() |
| | |
| | expect(res.headers['cache-control']).toContain('public') |
| | expect(res.headers['cache-control']).toMatch(/max-age=[1-9]/) |
| | expect(res.headers.vary).toContain('accept-language') |
| | expect(res.headers.vary).toContain('x-user-language') |
| | }) |
| |
|
| | |
| | |
| | |
| | |
| | |
| | test('redirects /en if Accept-Language header is malformed', async () => { |
| | const res = await get('/', { |
| | headers: { |
| | 'accept-language': 'ldfir;', |
| | }, |
| | followRedirects: false, |
| | }) |
| |
|
| | expect(res.statusCode).toBe(302) |
| | expect(res.headers.location).toBe('/en') |
| | expect(res.headers['set-cookie']).toBeUndefined() |
| | |
| | expect(res.headers['cache-control']).toContain('public') |
| | expect(res.headers['cache-control']).toMatch(/max-age=[1-9]/) |
| | expect(res.headers.vary).toContain('accept-language') |
| | expect(res.headers.vary).toContain('x-user-language') |
| | }) |
| |
|
| | test('redirects / to /en when unsupported language preference is specified', async () => { |
| | const res = await get('/', { |
| | headers: { |
| | |
| | 'accept-language': 'tl', |
| | }, |
| | followRedirects: false, |
| | }) |
| | expect(res.statusCode).toBe(302) |
| | expect(res.headers.location).toBe('/en') |
| | expect(res.headers['set-cookie']).toBeUndefined() |
| | |
| | expect(res.headers['cache-control']).toContain('public') |
| | expect(res.headers['cache-control']).toMatch(/max-age=[1-9]/) |
| | expect(res.headers.vary).toContain('accept-language') |
| | expect(res.headers.vary).toContain('x-user-language') |
| | }) |
| |
|
| | test('adds English prefix to old article URLs', async () => { |
| | const res = await get('/articles/deleting-a-team') |
| | expect(res.statusCode).toBe(302) |
| | expect(res.headers.location.startsWith('/en/')).toBe(true) |
| | expect(res.headers['set-cookie']).toBeUndefined() |
| | |
| | expect(res.headers['cache-control']).toContain('public') |
| | expect(res.headers['cache-control']).toMatch(/max-age=[1-9]/) |
| | expect(res.headers.vary).toContain('accept-language') |
| | expect(res.headers.vary).toContain('x-user-language') |
| | }) |
| |
|
| | test('redirects that not only injects /en/ should have cache-control', async () => { |
| | const res = await get('/en/articles/deleting-a-team') |
| | expect(res.statusCode).toBe(301) |
| | expect(res.headers['cache-control']).toContain('public') |
| | expect(res.headers['cache-control']).toMatch(/max-age=\d+/) |
| | }) |
| | }) |
| | }) |
| |
|
| | describe('static routes', () => { |
| | test('serves content from the /assets directory', async () => { |
| | const res = await get('/assets/images/site/be-social.gif') |
| | expect(res.statusCode).toBe(200) |
| | expect(res.headers['cache-control']).toContain('public') |
| | expect(res.headers['cache-control']).toMatch(/max-age=\d+/) |
| | |
| | expect(res.headers['set-cookie']).toBeUndefined() |
| | |
| | |
| | expect(res.headers['surrogate-key']).toBeTruthy() |
| | expect(res.headers.etag).toBeUndefined() |
| | expect(res.headers['last-modified']).toBeTruthy() |
| | }) |
| |
|
| | test('rewrites /assets requests from a cache-busting prefix', async () => { |
| | |
| | const res = await get('/assets/cb-123456/images/site/be-social.gif') |
| | expect(res.statusCode).toBe(200) |
| | expect(res.headers['set-cookie']).toBeUndefined() |
| | expect(res.headers['cache-control']).toContain('public') |
| | expect(res.headers['cache-control']).toMatch(/max-age=\d+/) |
| | expect(res.headers['surrogate-key']).toBe(SURROGATE_ENUMS.MANUAL) |
| | }) |
| |
|
| | test('no manual surrogate key for /assets requests without caching-busting prefix', async () => { |
| | const res = await get('/assets/images/site/be-social.gif') |
| | expect(res.statusCode).toBe(200) |
| | expect(res.headers['set-cookie']).toBeUndefined() |
| | expect(res.headers['cache-control']).toContain('public') |
| | expect(res.headers['cache-control']).toMatch(/max-age=\d+/) |
| |
|
| | const surrogateKeySplit = res.headers['surrogate-key'].split(/\s/g) |
| | expect(surrogateKeySplit.includes(SURROGATE_ENUMS.DEFAULT)).toBeTruthy() |
| | expect(surrogateKeySplit.includes(makeLanguageSurrogateKey())).toBeTruthy() |
| | }) |
| |
|
| | test('serves schema files from the /src/graphql/data directory at /public', async () => { |
| | const res = await get('/public/fpt/schema.docs.graphql') |
| | expect(res.statusCode).toBe(200) |
| | expect(res.headers['cache-control']).toContain('public') |
| | expect(res.headers['cache-control']).toMatch(/max-age=\d+/) |
| | |
| | expect(res.headers['set-cookie']).toBeUndefined() |
| | expect(res.headers.etag).toBeUndefined() |
| | expect(res.headers['last-modified']).toBeTruthy() |
| |
|
| | expect((await get(`/public/ghec/schema.docs.graphql`)).statusCode).toBe(200) |
| | expect( |
| | (await get(`/public/ghes-${enterpriseServerReleases.latest}/schema.docs-enterprise.graphql`)) |
| | .statusCode, |
| | ).toBe(200) |
| | expect( |
| | ( |
| | await get( |
| | `/public/ghes-${enterpriseServerReleases.oldestSupported}/schema.docs-enterprise.graphql`, |
| | ) |
| | ).statusCode, |
| | ).toBe(200) |
| | }) |
| |
|
| | test('does not serve repo contents that live outside the /assets directory', async () => { |
| | const paths = [ |
| | '/package.json', |
| | '/README.md', |
| | '/server.js', |
| | '/.git', |
| | '/.env', |
| | |
| | |
| | '/en/billing/.env', |
| | '/en/billing/.env.local', |
| | '/en/pages/.env_sample', |
| | '/en/pages/.env.development.local', |
| | ] |
| | for (const path of paths) { |
| | const res = await get(path) |
| | expect(res.statusCode).toBe(404) |
| | expect(res.headers['content-type']).toMatch('text/plain') |
| | expect(res.headers['cache-control']).toMatch(/max-age=[1-9]/) |
| | expect(res.headers['cache-control']).toMatch('public') |
| | } |
| | expect.assertions(4 * paths.length) |
| | }) |
| |
|
| | test('junk requests with or without query strings is 404', async () => { |
| | const paths = ['/env', '/xmlrpc.php', '/wp-login.php'] |
| | for (const path of paths) { |
| | const res = await get(`${path}?r=${Math.random()}`) |
| | expect(res.statusCode).toBe(404) |
| | expect(res.headers['content-type']).toMatch('text/plain') |
| | expect(res.headers['cache-control']).toMatch(/max-age=[1-9]/) |
| | expect(res.headers['cache-control']).toMatch('public') |
| | } |
| | expect.assertions(4 * paths.length) |
| | }) |
| | }) |
| |
|