11/**
2- * #1583 — the static plugin served nothing when `urlPath` was configured: the routing chain
3- * strips the mount prefix from req.pathname while the file map was keyed by the full entry
4- * URL path, and a slash-less `urlPath` (e.g. 'assets') never matched the route at all.
2+ * Static plugin `urlPath` mount coverage. One suite per mount shape — a mount lives in the
3+ * fixture app's config.yaml and an app has a single `static` block, so each shape needs its
4+ * own fixture/instance:
55 *
6- * The fixture is the issue's minimal repro: `static: { files: 'web/**', urlPath: 'assets' }`.
6+ * - Subpath mount (#1583): the plugin served nothing when `urlPath` was configured — the
7+ * routing chain strips the mount prefix from req.pathname while the file map was keyed by
8+ * the full entry URL path, and a slash-less `urlPath` (e.g. 'assets') never matched the
9+ * route at all. Fixture: `static: { files: 'web/**', urlPath: 'assets' }`.
10+ * - Root mount (#1766): `urlPath: '/'` matched only the exact path '/' — normalizeUrlPath
11+ * kept '/' as a truthy route constraint, so the handler became a sub-route whose
12+ * segment-boundary check ('/' + '/' = '//') never matched any sub-path; every asset 404'd.
13+ * Fixture: `static: { files: 'web/**', urlPath: '/' }`.
714 *
815 * Reproduction:
916 * npm run test:integration -- "integrationTests/components/static-urlpath.test.ts"
@@ -14,6 +21,7 @@ import { resolve } from 'node:path';
1421import { setupHarperWithFixture , teardownHarper , type ContextWithHarper } from '@harperfast/integration-testing' ;
1522
1623const FIXTURE_PATH = resolve ( import . meta. dirname , '../fixtures/static-urlpath' ) ;
24+ const ROOT_FIXTURE_PATH = resolve ( import . meta. dirname , '../fixtures/static-root-urlpath' ) ;
1725
1826suite ( 'static plugin with urlPath (#1583)' , ( ctx : ContextWithHarper ) => {
1927 before ( async ( ) => {
@@ -71,3 +79,58 @@ suite('static plugin with urlPath (#1583)', (ctx: ContextWithHarper) => {
7179 strictEqual ( dir . headers . get ( 'location' ) , '/assets/docs/?foo=bar' ) ;
7280 } ) ;
7381} ) ;
82+
83+ suite ( 'static plugin with root urlPath (#1766)' , ( ctx : ContextWithHarper ) => {
84+ before ( async ( ) => {
85+ await setupHarperWithFixture ( ctx , ROOT_FIXTURE_PATH ) ;
86+ } ) ;
87+
88+ after ( async ( ) => {
89+ await teardownHarper ( ctx ) ;
90+ } ) ;
91+
92+ test ( 'serves the root index at /' , async ( ) => {
93+ const res = await fetch ( new URL ( '/' , ctx . harper . httpURL ) ) ;
94+ const text = await res . text ( ) ;
95+ strictEqual ( res . status , 200 , `expected / to serve the root index: ${ res . status } ${ text } ` ) ;
96+ ok ( text . includes ( 'root index' ) ) ;
97+ } ) ;
98+
99+ test ( 'serves a file at a root-mounted sub-path' , async ( ) => {
100+ const res = await fetch ( new URL ( '/test.css' , ctx . harper . httpURL ) ) ;
101+ const text = await res . text ( ) ;
102+ strictEqual ( res . status , 200 , `expected /test.css to serve: ${ res . status } ${ text } ` ) ;
103+ ok ( text . includes ( 'teal' ) , 'served the fixture css content' ) ;
104+ } ) ;
105+
106+ test ( 'serves the index file by its full path' , async ( ) => {
107+ const res = await fetch ( new URL ( '/index.html' , ctx . harper . httpURL ) ) ;
108+ const text = await res . text ( ) ;
109+ strictEqual ( res . status , 200 , `expected /index.html to serve: ${ res . status } ${ text } ` ) ;
110+ ok ( text . includes ( 'root index' ) ) ;
111+ } ) ;
112+
113+ test ( 'serves a nested directory index' , async ( ) => {
114+ const res = await fetch ( new URL ( '/docs/' , ctx . harper . httpURL ) ) ;
115+ const text = await res . text ( ) ;
116+ strictEqual ( res . status , 200 , `expected /docs/ to serve index.html: ${ res . status } ${ text } ` ) ;
117+ ok ( text . includes ( 'docs index' ) ) ;
118+ } ) ;
119+
120+ test ( 'redirects a directory request to the trailing-slash path' , async ( ) => {
121+ const res = await fetch ( new URL ( '/docs' , ctx . harper . httpURL ) , { redirect : 'manual' } ) ;
122+ strictEqual ( res . status , 301 ) ;
123+ strictEqual ( res . headers . get ( 'location' ) , '/docs/' ) ;
124+ } ) ;
125+
126+ test ( 'preserves the query string on the trailing-slash redirect' , async ( ) => {
127+ const res = await fetch ( new URL ( '/docs?foo=bar' , ctx . harper . httpURL ) , { redirect : 'manual' } ) ;
128+ strictEqual ( res . status , 301 ) ;
129+ strictEqual ( res . headers . get ( 'location' ) , '/docs/?foo=bar' ) ;
130+ } ) ;
131+
132+ test ( 'falls through to a 404 for paths with no static entry' , async ( ) => {
133+ const res = await fetch ( new URL ( '/no-such-file.js' , ctx . harper . httpURL ) ) ;
134+ strictEqual ( res . status , 404 ) ;
135+ } ) ;
136+ } ) ;
0 commit comments