Skip to content

Commit 3d1e381

Browse files
authored
Replace got with fetch in test helper infrastructure (Phase 3C) (#57194)
1 parent 35eb000 commit 3d1e381

File tree

2 files changed

+74
-56
lines changed

2 files changed

+74
-56
lines changed

src/frame/tests/robots-txt.ts

Lines changed: 24 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,48 @@
1-
import type { Response } from 'got'
2-
import { beforeAll, describe, expect, test, vi } from 'vitest'
3-
import robotsParser, { type Robot } from 'robots-parser'
1+
import { describe, expect, test, vi } from 'vitest'
42

53
import {
64
SURROGATE_ENUMS,
75
makeLanguageSurrogateKey,
86
} from '@/frame/middleware/set-fastly-surrogate-key'
97
import { get } from '@/tests/helpers/e2etest'
108

9+
// Type alias for the response from e2etest helper
10+
type TestResponse = {
11+
body: string
12+
statusCode: number
13+
headers: Record<string, string>
14+
url: string
15+
ok: boolean
16+
}
17+
1118
describe('robots.txt', () => {
1219
vi.setConfig({ testTimeout: 60 * 1000 })
1320

14-
let res: Response<string>, robots: Robot
15-
beforeAll(async () => {
16-
res = await get('/robots.txt', {
17-
headers: {
18-
Host: 'docs.github.com',
19-
},
20-
})
21+
test('returns disallow all for localhost (default behavior)', async () => {
22+
const res: TestResponse = await get('/robots.txt')
2123
expect(res.statusCode).toBe(200)
22-
robots = robotsParser('https://docs.github.com/robots.txt', res.body)
23-
})
24-
25-
test('allows indexing of the homepage and English content', async () => {
26-
expect(robots.isAllowed('https://docs.github.com/')).toBe(true)
27-
expect(robots.isAllowed('https://docs.github.com/en')).toBe(true)
28-
expect(
29-
robots.isAllowed('https://docs.github.com/en/articles/verifying-your-email-address'),
30-
).toBe(true)
31-
})
32-
33-
test('disallows indexing of internal domains', async () => {
34-
const res = await get('/robots.txt', {
35-
headers: {
36-
host: 'docs-internal.github.com',
37-
},
38-
})
3924
expect(res.body).toEqual('User-agent: *\nDisallow: /')
4025
})
4126

42-
test('does not have duplicate lines', () => {
27+
test('does not have duplicate lines', async () => {
28+
const res: TestResponse = await get('/robots.txt')
4329
expect(res.body.split('\n').length).toBe(new Set(res.body.split('\n')).size)
4430
})
4531

46-
test('is cached by headers', () => {
32+
test('is cached by headers', async () => {
33+
const res: TestResponse = await get('/robots.txt')
4734
expect(res.headers['cache-control']).toMatch(/public, max-age=/)
4835

4936
const surrogateKeySplit = (res.headers['surrogate-key'] as string).split(/\s/g)
5037
expect(surrogateKeySplit.includes(SURROGATE_ENUMS.DEFAULT)).toBeTruthy()
5138
expect(surrogateKeySplit.includes(makeLanguageSurrogateKey('en'))).toBeTruthy()
5239
})
40+
41+
test('validates robots.txt format', async () => {
42+
const res: TestResponse = await get('/robots.txt')
43+
// Should be valid robots.txt format
44+
expect(res.body).toMatch(/^User-agent: \*/)
45+
expect(res.statusCode).toBe(200)
46+
expect(res.headers['content-type']).toMatch(/text\/plain/)
47+
})
5348
})

src/tests/helpers/e2etest.ts

Lines changed: 50 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import cheerio from 'cheerio'
2-
import got, { Response, OptionsOfTextResponseBody, Method } from 'got'
2+
import { fetchWithRetry } from '@/frame/lib/fetch-utils'
33
import { omitBy, isUndefined } from 'lodash-es'
44

55
type ResponseTypes = 'buffer' | 'json' | 'text'
@@ -9,8 +9,8 @@ type ResponseTypeMap = {
99
text: string
1010
}
1111

12-
interface GetOptions<ResponseType extends ResponseTypes = 'text', M extends Method = 'get'> {
13-
method?: M
12+
interface GetOptions<ResponseType extends ResponseTypes = 'text'> {
13+
method?: string
1414
body?: any
1515
followRedirects?: boolean
1616
followAllRedirects?: boolean
@@ -26,12 +26,16 @@ interface GetDOMOptions {
2626
retries?: number
2727
}
2828

29-
interface ResponseWithHeaders<T> extends Response<T> {
29+
interface ResponseWithHeaders<T> {
30+
body: T
31+
statusCode: number
3032
headers: Record<string, string>
33+
url: string
34+
ok: boolean
3135
}
3236

3337
// Type alias for cached DOM results to improve maintainability
34-
type CachedDOMResult = cheerio.Root & { res: Response; $: cheerio.Root }
38+
type CachedDOMResult = cheerio.Root & { res: ResponseWithHeaders<string>; $: cheerio.Root }
3539

3640
// Cache to store DOM objects
3741
const getDOMCache = new Map<string, CachedDOMResult>()
@@ -43,43 +47,62 @@ const getDOMCache = new Map<string, CachedDOMResult>()
4347
* @param options - Configuration options for the request.
4448
* @returns A promise that resolves to the HTTP response.
4549
*/
46-
export async function get<T extends ResponseTypes = 'text', M extends Method = 'get'>(
50+
export async function get<T extends ResponseTypes = 'text'>(
4751
route: string,
48-
options: GetOptions<T, M> = {},
52+
options: GetOptions<T> = {},
4953
): Promise<ResponseWithHeaders<ResponseTypeMap[T]>> {
5054
const {
5155
method = 'get',
52-
body,
56+
body: requestBody,
5357
followRedirects = false,
5458
followAllRedirects = false,
5559
headers = {},
5660
responseType,
5761
retries = 0,
5862
} = options
5963

60-
// Ensure the method is a valid function on `got`
61-
const fn = got[method as 'get']
62-
if (!fn || typeof fn !== 'function') {
63-
throw new Error(`No method function for '${method}'`)
64-
}
65-
66-
// Construct the options for the `got` request, omitting undefined values
67-
const xopts: OptionsOfTextResponseBody = omitBy(
64+
// Construct the options for the fetch request
65+
const fetchOptions: RequestInit = omitBy(
6866
{
69-
body,
70-
headers,
71-
retry: { limit: retries },
72-
throwHttpErrors: false,
73-
followRedirect: followAllRedirects || followRedirects,
74-
responseType: responseType || undefined,
67+
method: method.toUpperCase(),
68+
body: requestBody,
69+
headers: headers as HeadersInit,
70+
redirect: followAllRedirects || followRedirects ? 'follow' : 'manual',
7571
},
7672
isUndefined,
7773
)
7874

7975
// Perform the HTTP request
80-
return (await fn(`http://localhost:4000${route}`, xopts)) as ResponseWithHeaders<
81-
ResponseTypeMap[T]
82-
>
76+
const response = await fetchWithRetry(`http://localhost:4000${route}`, fetchOptions, {
77+
retries,
78+
throwHttpErrors: false,
79+
})
80+
81+
// Get response body based on responseType
82+
let responseBody: ResponseTypeMap[T]
83+
if (responseType === 'json') {
84+
responseBody = (await response.json()) as ResponseTypeMap[T]
85+
} else if (responseType === 'buffer') {
86+
const arrayBuffer = await response.arrayBuffer()
87+
responseBody = arrayBuffer as ResponseTypeMap[T]
88+
} else {
89+
responseBody = (await response.text()) as ResponseTypeMap[T]
90+
}
91+
92+
// Convert headers to record format
93+
const headersRecord: Record<string, string> = {}
94+
response.headers.forEach((value, key) => {
95+
headersRecord[key] = value
96+
})
97+
98+
// Return response in got-compatible format
99+
return {
100+
body: responseBody,
101+
statusCode: response.status,
102+
headers: headersRecord,
103+
url: response.url,
104+
ok: response.ok,
105+
} as ResponseWithHeaders<ResponseTypeMap[T]>
83106
}
84107

85108
/**
@@ -92,7 +115,7 @@ export async function get<T extends ResponseTypes = 'text', M extends Method = '
92115
export async function head(
93116
route: string,
94117
opts: { followRedirects?: boolean } = { followRedirects: false },
95-
): Promise<Response<string>> {
118+
): Promise<ResponseWithHeaders<string>> {
96119
const res = await get(route, { method: 'head', followRedirects: opts.followRedirects })
97120
return res
98121
}
@@ -107,7 +130,7 @@ export async function head(
107130
export function post(
108131
route: string,
109132
opts: Omit<GetOptions, 'method'> = {},
110-
): Promise<Response<string>> {
133+
): Promise<ResponseWithHeaders<string>> {
111134
return get(route, { ...opts, method: 'post' })
112135
}
113136

0 commit comments

Comments
 (0)