@@ -8,6 +8,7 @@ import { isFile } from './lib/fs.js'
88import { NetlifyDev } from './main.js'
99
1010import { withMockApi } from '../test/mock-api.js'
11+ import { config } from 'node:process'
1112
1213describe ( 'Handling requests' , ( ) => {
1314 describe ( 'No linked site' , ( ) => {
@@ -88,6 +89,158 @@ describe('Handling requests', () => {
8889 await fixture . destroy ( )
8990 } )
9091
92+ test ( 'Headers rules matching a static file are applied' , async ( ) => {
93+ const fixture = new Fixture ( )
94+ . withFile (
95+ 'netlify.toml' ,
96+ `[build]
97+ publish = "public"
98+ [[headers]]
99+ for = "/hello.txt"
100+ [headers.values]
101+ "Vary" = "User-Agent"
102+ ` ,
103+ )
104+ . withHeadersFile ( {
105+ pathPrefix : 'public' ,
106+ headers : [ { path : '/hello.txt' , headers : [ 'Cache-Control: max-age=42' ] } ] ,
107+ } )
108+ . withFile ( 'public/hello.txt' , 'Hello from hello.txt' )
109+ . withFile ( 'public/another-path.txt' , 'Hello from another-path.txt' )
110+ const directory = await fixture . create ( )
111+ const req = new Request ( 'https://site.netlify/hello.txt' )
112+ const dev = new NetlifyDev ( {
113+ projectRoot : directory ,
114+ } )
115+ await dev . start ( )
116+
117+ const matchRes = await dev . handle ( req )
118+
119+ expect ( await matchRes ?. text ( ) ) . toBe ( 'Hello from hello.txt' )
120+ expect ( Object . fromEntries ( matchRes ?. headers ?. entries ( ) ?? [ ] ) ) . toMatchObject ( {
121+ 'cache-control' : 'max-age=42' ,
122+ vary : 'User-Agent' ,
123+ } )
124+
125+ const noMatchRes = await dev . handle ( new Request ( 'https://site.netlify/another-path.txt' ) )
126+ expect ( await noMatchRes ?. text ( ) ) . toBe ( 'Hello from another-path.txt' )
127+ expect ( Object . fromEntries ( noMatchRes ?. headers ?. entries ( ) ?? [ ] ) ) . not . toMatchObject ( {
128+ 'cache-control' : 'max-age=42' ,
129+ vary : 'User-Agent' ,
130+ } )
131+
132+ await fixture . destroy ( )
133+ } )
134+
135+ test ( 'Headers rules matching target of a rewrite to a static file are applied' , async ( ) => {
136+ const fixture = new Fixture ( )
137+ . withFile (
138+ 'netlify.toml' ,
139+ `[build]
140+ publish = "public"
141+ [[headers]]
142+ for = "/from"
143+ [headers.values]
144+ "X-Custom" = "value for from rule"
145+ "X-Custom-From" = "another value for from rule"
146+ [[headers]]
147+ for = "/to.txt"
148+ [headers.values]
149+ "X-Custom" = "value for to rule"
150+ ` ,
151+ )
152+ . withFile ( 'public/_redirects' , `/from /to.txt 200` )
153+ . withFile ( 'public/to.txt' , `to.txt content` )
154+ const directory = await fixture . create ( )
155+ const dev = new NetlifyDev ( {
156+ projectRoot : directory ,
157+ } )
158+ await dev . start ( )
159+
160+ const directRes = await dev . handle ( new Request ( 'https://site.netlify/to.txt' ) )
161+ expect ( await directRes ?. text ( ) ) . toBe ( 'to.txt content' )
162+ expect ( directRes ?. headers . get ( 'X-Custom' ) ) . toBe ( 'value for to rule' )
163+ expect ( directRes ?. headers . get ( 'X-Custom-From' ) ) . toBeNull ( )
164+
165+ const rewriteRes = await dev . handle ( new Request ( 'https://site.netlify/from' ) )
166+ expect ( await rewriteRes ?. text ( ) ) . toBe ( 'to.txt content' )
167+ expect ( rewriteRes ?. headers . get ( 'X-Custom' ) ) . toBe ( 'value for to rule' )
168+ expect ( rewriteRes ?. headers . get ( 'X-Custom-From' ) ) . toBeNull ( )
169+
170+ await fixture . destroy ( )
171+ } )
172+
173+ test ( 'Headers rules matching a static file that shadows a function are applied' , async ( ) => {
174+ const fixture = new Fixture ( )
175+ . withFile (
176+ 'netlify.toml' ,
177+ `[build]
178+ publish = "public"
179+ [[headers]]
180+ for = "/shadowed-path.html"
181+ [headers.values]
182+ "X-Custom-Header" = "custom-value"
183+ ` ,
184+ )
185+ . withFile ( 'public/shadowed-path.html' , "Hello from the static file" )
186+ . withFile (
187+ 'netlify/functions/shadowed-path.mjs' ,
188+ `export default async () => new Response("Hello from the function");
189+ export const config = { path: "/shadowed-path.html", preferStatic: true };
190+ ` ,
191+ )
192+ const directory = await fixture . create ( )
193+ const req = new Request ( 'https://site.netlify/shadowed-path.html' )
194+ const dev = new NetlifyDev ( {
195+ projectRoot : directory ,
196+ } )
197+ await dev . start ( )
198+
199+ const res = await dev . handle ( req )
200+ expect ( await res ?. text ( ) ) . toBe ( 'Hello from the static file' )
201+ expect ( Object . fromEntries ( res ?. headers ?. entries ( ) ?? [ ] ) ) . toMatchObject ( {
202+ 'x-custom-header' : 'custom-value' ,
203+ } )
204+
205+ await fixture . destroy ( )
206+ } )
207+
208+ test ( 'Headers rules matching an unshadowed function on a custom path are not applied' , async ( ) => {
209+ const fixture = new Fixture ( )
210+ . withFile (
211+ 'netlify.toml' ,
212+ `[build]
213+ publish = "public"
214+ [[headers]]
215+ for = "/hello.html"
216+ [headers.values]
217+ "X-Custom-Header" = "custom-value"
218+ ` ,
219+ )
220+ . withFile ( 'public/hello.html' , "Hello from the static file" )
221+ . withFile (
222+ 'netlify/functions/hello.mjs' ,
223+ `export default async () => new Response("Hello from the function");
224+ export const config = { path: "/hello.html" };
225+ ` ,
226+ )
227+ const directory = await fixture . create ( )
228+ const req = new Request ( 'https://site.netlify/hello.html' )
229+ const dev = new NetlifyDev ( {
230+ projectRoot : directory ,
231+ } )
232+ await dev . start ( )
233+
234+ const res = await dev . handle ( req )
235+ expect ( await res ?. text ( ) ) . toBe ( 'Hello from the function' )
236+ expect ( res ?. headers . get ( 'x-custom-header' ) ) . toBeNull ( )
237+
238+ await fixture . destroy ( )
239+ } )
240+
241+ // TODO(FRB-1834): Implement this test when edge functions are supported
242+ test . todo ( 'Headers rules matching a path are not applied to edge function responses' )
243+
91244 test ( 'Invoking a function, updating its contents and invoking it again' , async ( ) => {
92245 let fixture = new Fixture ( )
93246 . withFile (
0 commit comments