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