Skip to content

Commit e991256

Browse files
committed
test: cover error page rendering, cleanup guards and dev listen options
1 parent 7f02edf commit e991256

4 files changed

Lines changed: 282 additions & 2 deletions

File tree

‎packages/nuxt-cli/src/commands/dev.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,7 @@ function resolveForkPoolSize(): number | undefined {
411411
returnparsed
412412
}
413413

414-
functionparsePositiveInteger(value: string|undefined): number|undefined{
414+
exportfunctionparsePositiveInteger(value: string|undefined): number|undefined{
415415
constparsed=Number(value)
416416
if(!value||!Number.isInteger(parsed)||parsed<=0){
417417
if(value){
@@ -422,7 +422,7 @@ function parsePositiveInteger(value: string | undefined): number | undefined {
422422
returnparsed
423423
}
424424

425-
functionresolveListenOverrides(args: ParsedArgs<ArgsT>): DevListenOverrides{
425+
exportfunctionresolveListenOverrides(args: ParsedArgs<ArgsT>): DevListenOverrides{
426426
consthttpsOptions: HTTPSOptions={
427427
cert: args['https.cert']
428428
||args.sslCert
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import{afterEach,describe,expect,it,vi}from'vitest'
2+
3+
import{parsePositiveInteger,resolveListenOverrides}from'../../../src/commands/dev'
4+
5+
functionoverrides(args: Record<string,unknown>={}){
6+
returnresolveListenOverrides({_: [], ...args}asnever)
7+
}
8+
9+
afterEach(()=>{
10+
vi.unstubAllEnvs()
11+
})
12+
13+
describe('resolveListenOverrides',()=>{
14+
it('should leave the hostname unset when no host is requested',()=>{
15+
expect(overrides().hostname).toBeUndefined()
16+
})
17+
18+
it('should treat a bare --host as every interface',()=>{
19+
expect(overrides({host: true}).hostname).toBe('')
20+
expect(overrides({host: ''}).hostname).toBe('')
21+
})
22+
23+
it('should keep an explicit host',()=>{
24+
expect(overrides({host: '127.0.0.1'}).hostname).toBe('127.0.0.1')
25+
})
26+
27+
it('should prefer an explicit host over the environment',()=>{
28+
vi.stubEnv('NUXT_HOST','0.0.0.0')
29+
expect(overrides({host: '127.0.0.1'}).hostname).toBe('127.0.0.1')
30+
})
31+
32+
it('should read the host from the environment in precedence order',()=>{
33+
vi.stubEnv('HOST','from-host')
34+
expect(overrides().hostname).toBe('from-host')
35+
vi.stubEnv('NITRO_HOST','from-nitro')
36+
expect(overrides().hostname).toBe('from-nitro')
37+
vi.stubEnv('NUXT_HOST','from-nuxt')
38+
expect(overrides().hostname).toBe('from-nuxt')
39+
})
40+
41+
it('should read the port from the environment in precedence order',()=>{
42+
vi.stubEnv('PORT','4000')
43+
expect(overrides().port).toBe('4000')
44+
vi.stubEnv('NUXT_PORT','5000')
45+
expect(overrides().port).toBe('5000')
46+
expect(overrides({port: '6000'}).port).toBe('6000')
47+
})
48+
49+
it('should open the browser when only --open.url is given',()=>{
50+
expect(overrides({'open.url': '/admin'})).toMatchObject({open: true,openURL: '/admin'})
51+
expect(overrides().open).toBeFalsy()
52+
})
53+
54+
it('should split https domains and drop empty entries',()=>{
55+
expect(overrides({'https.domains': 'a.test, b.test ,'}).https).toMatchObject({domains: ['a.test','b.test']})
56+
})
57+
58+
it('should leave https domains unset when the flag is absent',()=>{
59+
expect((overrides().httpsas{domains?: string[]}).domains).toBeUndefined()
60+
})
61+
62+
it('should fall back to the deprecated ssl flags and env vars',()=>{
63+
vi.stubEnv('NITRO_SSL_KEY','/env/key.pem')
64+
expect(overrides({sslCert: '/cert.pem'}).https).toMatchObject({cert: '/cert.pem',key: '/env/key.pem'})
65+
})
66+
})
67+
68+
describe('parsePositiveInteger',()=>{
69+
it('should accept a positive integer',()=>{
70+
expect(parsePositiveInteger('30')).toBe(30)
71+
})
72+
73+
it('should reject anything else',()=>{
74+
for(constvalueof[undefined,'','0','-1','1.5','abc']){
75+
expect(parsePositiveInteger(value),String(value)).toBeUndefined()
76+
}
77+
})
78+
})
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
importtype{IncomingMessage,ServerResponse}from'node:http'
2+
3+
import{describe,expect,it}from'vitest'
4+
5+
import{renderError}from'../../../src/dev/error'
6+
import{NuxtDevServer}from'../../../src/dev/utils'
7+
8+
interfaceFakeResponseextendsServerResponse{
9+
body: string
10+
statusCode: number
11+
headers: Record<string,string>
12+
finished: Promise<void>
13+
}
14+
15+
functioncreateRequest(accept?: string,url='/'): IncomingMessage{
16+
return{ url,method: 'GET',headers: accept ? { accept } : {}}asunknownasIncomingMessage
17+
}
18+
19+
functioncreateResponse(): FakeResponse{
20+
constheaders: Record<string,string>={}
21+
letdone: ()=>void
22+
constfinished=newPromise<void>((resolve)=>{
23+
done=resolve
24+
})
25+
constres={
26+
statusCode: 200,
27+
headersSent: false,
28+
writableEnded: false,
29+
body: '',
30+
headers,
31+
finished,
32+
setHeader(name: string,value: string){
33+
headers[name.toLowerCase()]=value
34+
},
35+
once(){},
36+
end(chunk?: string){
37+
if(chunk){
38+
res.body+=chunk
39+
}
40+
res.writableEnded=true
41+
res.headersSent=true
42+
done()
43+
},
44+
}
45+
returnresasunknownasFakeResponse
46+
}
47+
48+
describe('renderError',()=>{
49+
it('should escape an error message in the html error page',async()=>{
50+
constres=createResponse()
51+
awaitrenderError(createRequest('text/html'),res,newError('<script>alert(1)</script>'))
52+
53+
expect(res.statusCode).toBe(500)
54+
expect(res.headers['content-type']).toBe('text/html')
55+
expect(res.body).not.toContain('<script>alert(1)</script>')
56+
expect(res.body).toContain('&lt;script&gt;alert(1)&lt;/script&gt;')
57+
})
58+
59+
it('should escape a reflected request url in the html error page',async()=>{
60+
constres=createResponse()
61+
awaitrenderError(createRequest('text/html','/</script><script>alert(1)</script>'),res,newError('boom'))
62+
63+
expect(res.body).not.toContain('<script>alert(1)</script>')
64+
})
65+
66+
it('should answer a non-html client with json',async()=>{
67+
constres=createResponse()
68+
awaitrenderError(createRequest('application/json'),res,newError('boom'))
69+
70+
expect(res.headers['content-type']).toBe('application/json')
71+
expect(JSON.parse(res.body)).toMatchObject({error: true,status: 500,message: 'boom'})
72+
})
73+
74+
it('should send hardening headers with the error page',async()=>{
75+
constres=createResponse()
76+
awaitrenderError(createRequest('text/html'),res,newError('boom'))
77+
78+
expect(res.headers).toMatchObject({
79+
'cache-control': 'no-store',
80+
'x-content-type-options': 'nosniff',
81+
'x-frame-options': 'DENY',
82+
'referrer-policy': 'no-referrer',
83+
})
84+
})
85+
86+
it('should not write a body once headers have been sent',async()=>{
87+
constres=createResponse()
88+
res.headersSent=true
89+
awaitrenderError(createRequest('text/html'),res,newError('boom'))
90+
91+
expect(res.body).toBe('')
92+
})
93+
94+
it('should render a non-error rejection value',async()=>{
95+
constres=createResponse()
96+
awaitrenderError(createRequest('application/json'),res,'just a string')
97+
98+
expect(JSON.parse(res.body)).toMatchObject({status: 500,message: 'Unknown error'})
99+
})
100+
})
101+
102+
describe('dev server loading screen',()=>{
103+
functioncreateDevServer(loadingTemplate?: (data: {loading?: string})=>string){
104+
returnnewNuxtDevServer({cwd: process.cwd(),dotenv: {},overrides: {}, loadingTemplate })
105+
}
106+
107+
it('should serve the loading template to a browser',async()=>{
108+
constserver=createDevServer(({ loading })=>`<p>${loading}</p>`)
109+
constres=createResponse()
110+
111+
server.handler(createRequest('text/html'),res)
112+
awaitres.finished
113+
114+
expect(res.statusCode).toBe(503)
115+
expect(res.headers['content-type']).toBe('text/html')
116+
expect(res.body).toBe('<p>Loading...</p>')
117+
})
118+
119+
it('should serve json to a non-browser client',async()=>{
120+
constserver=createDevServer(()=>'<p>ignored</p>')
121+
constres=createResponse()
122+
123+
server.handler(createRequest('application/json'),res)
124+
awaitres.finished
125+
126+
expect(res.statusCode).toBe(503)
127+
expect(JSON.parse(res.body)).toMatchObject({error: true,status: 503})
128+
})
129+
130+
it('should ask clients to retry rather than caching the placeholder',async()=>{
131+
constserver=createDevServer(()=>'loading')
132+
constres=createResponse()
133+
134+
server.handler(createRequest('text/html'),res)
135+
awaitres.finished
136+
137+
expect(res.headers).toMatchObject({'cache-control': 'no-store','refresh': '3'})
138+
})
139+
})
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import{existsSync}from'node:fs'
2+
import{mkdir,mkdtemp,rm,writeFile}from'node:fs/promises'
3+
import{tmpdir}from'node:os'
4+
import{join}from'node:path'
5+
6+
import{afterEach,beforeEach,describe,expect,it}from'vitest'
7+
8+
import{cleanupNuxtDirs,nuxtVersionToGitIdentifier}from'../../../src/utils/nuxt'
9+
10+
letroot: string
11+
12+
beforeEach(async()=>{
13+
root=awaitmkdtemp(join(tmpdir(),'nuxt-cleanup-'))
14+
})
15+
16+
afterEach(async()=>{
17+
awaitrm(root,{recursive: true,force: true})
18+
})
19+
20+
asyncfunctionseed(...dirs: string[]): Promise<void>{
21+
for(constdirofdirs){
22+
awaitmkdir(join(root,dir),{recursive: true})
23+
awaitwriteFile(join(root,dir,'file.txt'),'x')
24+
}
25+
}
26+
27+
describe('cleanupNuxtDirs',()=>{
28+
it('should remove the generated directories',async()=>{
29+
awaitseed('.nuxt','.output','dist','node_modules/.vite','node_modules/.cache','app')
30+
31+
awaitcleanupNuxtDirs(root,'.nuxt',{silent: true})
32+
33+
for(constdirof['.nuxt','.output','dist','node_modules/.vite','node_modules/.cache']){
34+
expect(existsSync(join(root,dir)),dir).toBe(false)
35+
}
36+
expect(existsSync(join(root,'app'))).toBe(true)
37+
})
38+
39+
it('should refuse a build directory that is the project root',async()=>{
40+
awaitseed('app')
41+
42+
awaitexpect(cleanupNuxtDirs(root,'.',{silent: true})).rejects.toThrow('contains the project root')
43+
expect(existsSync(join(root,'app'))).toBe(true)
44+
})
45+
46+
it('should refuse a build directory that contains the project root',async()=>{
47+
awaitseed('app')
48+
49+
awaitexpect(cleanupNuxtDirs(root,'..',{silent: true})).rejects.toThrow('contains the project root')
50+
expect(existsSync(join(root,'app'))).toBe(true)
51+
})
52+
})
53+
54+
describe('nuxtVersionToGitIdentifier',()=>{
55+
it('should use the git identifier of a nightly version',()=>{
56+
expect(nuxtVersionToGitIdentifier('3.0.0-rc.8-27677607.a3a8706')).toBe('a3a8706')
57+
})
58+
59+
it('should fall back to the release tag',()=>{
60+
expect(nuxtVersionToGitIdentifier('3.0.0-rc.8')).toBe('v3.0.0-rc.8')
61+
expect(nuxtVersionToGitIdentifier('4.1.0')).toBe('v4.1.0')
62+
})
63+
})

0 commit comments

Comments
 (0)