@@ -1781,4 +1781,307 @@ describe('Routes', () => {
17811781 assert . ok ( Array . isArray ( data . dirs ) , 'dirs should be an array' ) ;
17821782 } ) ;
17831783 } ) ;
1784+
1785+ // === Query param type validation ===
1786+ describe ( 'Query param type validation' , ( ) => {
1787+ let inst ;
1788+ let tmpDir ;
1789+ after ( ( ) => {
1790+ inst ?. shutdown ( ) ;
1791+ if ( tmpDir ) fs . rmSync ( tmpDir , { recursive : true , force : true } ) ;
1792+ } ) ;
1793+
1794+ async function setup ( ) {
1795+ if ( inst ) return ;
1796+ tmpDir = fs . mkdtempSync ( path . join ( require ( 'os' ) . tmpdir ( ) , 'tb-qparam-' ) ) ;
1797+ fs . writeFileSync ( path . join ( tmpDir , 'test.txt' ) , 'hello' ) ;
1798+ inst = await startServer ( { cwd : tmpDir } ) ;
1799+ }
1800+
1801+ it ( 'should return 400 when /files dir param is an array' , async ( ) => {
1802+ await setup ( ) ;
1803+ const res = await httpRequest ( {
1804+ hostname : '127.0.0.1' ,
1805+ port : inst . port ,
1806+ path : `/api/sessions/${ inst . defaultId } /files?dir=a&dir=b` ,
1807+ method : 'GET' ,
1808+ } ) ;
1809+ assert . strictEqual ( res . statusCode , 400 ) ;
1810+ const data = JSON . parse ( res . data ) ;
1811+ assert . strictEqual ( data . error , 'Invalid dir parameter' ) ;
1812+ } ) ;
1813+
1814+ it ( 'should return 400 when /download file param is an array' , async ( ) => {
1815+ await setup ( ) ;
1816+ const res = await httpRequest ( {
1817+ hostname : '127.0.0.1' ,
1818+ port : inst . port ,
1819+ path : `/api/sessions/${ inst . defaultId } /download?file=a&file=b` ,
1820+ method : 'GET' ,
1821+ } ) ;
1822+ assert . strictEqual ( res . statusCode , 400 ) ;
1823+ const data = JSON . parse ( res . data ) ;
1824+ assert . strictEqual ( data . error , 'Missing file parameter' ) ;
1825+ } ) ;
1826+
1827+ it ( 'should return 400 when /file-raw file param is an array' , async ( ) => {
1828+ await setup ( ) ;
1829+ const res = await httpRequest ( {
1830+ hostname : '127.0.0.1' ,
1831+ port : inst . port ,
1832+ path : `/api/sessions/${ inst . defaultId } /file-raw?file=a&file=b` ,
1833+ method : 'GET' ,
1834+ } ) ;
1835+ assert . strictEqual ( res . statusCode , 400 ) ;
1836+ const data = JSON . parse ( res . data ) ;
1837+ assert . strictEqual ( data . error , 'Missing file parameter' ) ;
1838+ } ) ;
1839+
1840+ it ( 'should return 400 when /file-content file param is an array' , async ( ) => {
1841+ await setup ( ) ;
1842+ const res = await httpRequest ( {
1843+ hostname : '127.0.0.1' ,
1844+ port : inst . port ,
1845+ path : `/api/sessions/${ inst . defaultId } /file-content?file=a&file=b` ,
1846+ method : 'GET' ,
1847+ } ) ;
1848+ assert . strictEqual ( res . statusCode , 400 ) ;
1849+ const data = JSON . parse ( res . data ) ;
1850+ assert . strictEqual ( data . error , 'Missing file parameter' ) ;
1851+ } ) ;
1852+ } ) ;
1853+
1854+ // === Symlink rejection ===
1855+ describe ( 'Symlink rejection' , ( ) => {
1856+ let inst ;
1857+ let tmpDir ;
1858+ after ( ( ) => {
1859+ inst ?. shutdown ( ) ;
1860+ if ( tmpDir ) fs . rmSync ( tmpDir , { recursive : true , force : true } ) ;
1861+ } ) ;
1862+
1863+ async function setup ( ) {
1864+ if ( inst ) return ;
1865+ tmpDir = fs . mkdtempSync ( path . join ( require ( 'os' ) . tmpdir ( ) , 'tb-symlink-' ) ) ;
1866+ fs . writeFileSync ( path . join ( tmpDir , 'real.txt' ) , 'real content' ) ;
1867+ fs . symlinkSync ( path . join ( tmpDir , 'real.txt' ) , path . join ( tmpDir , 'link.txt' ) ) ;
1868+ inst = await startServer ( { cwd : tmpDir } ) ;
1869+ }
1870+
1871+ it ( 'should reject symlink in /download with 403' , async ( ) => {
1872+ await setup ( ) ;
1873+ const res = await httpRequest ( {
1874+ hostname : '127.0.0.1' ,
1875+ port : inst . port ,
1876+ path : `/api/sessions/${ inst . defaultId } /download?file=link.txt` ,
1877+ method : 'GET' ,
1878+ } ) ;
1879+ assert . strictEqual ( res . statusCode , 403 ) ;
1880+ const data = JSON . parse ( res . data ) ;
1881+ assert . strictEqual ( data . error , 'Symbolic links are not allowed' ) ;
1882+ } ) ;
1883+
1884+ it ( 'should reject symlink in /file-raw with 403' , async ( ) => {
1885+ await setup ( ) ;
1886+ const res = await httpRequest ( {
1887+ hostname : '127.0.0.1' ,
1888+ port : inst . port ,
1889+ path : `/api/sessions/${ inst . defaultId } /file-raw?file=link.txt` ,
1890+ method : 'GET' ,
1891+ } ) ;
1892+ assert . strictEqual ( res . statusCode , 403 ) ;
1893+ const data = JSON . parse ( res . data ) ;
1894+ assert . strictEqual ( data . error , 'Symbolic links are not allowed' ) ;
1895+ } ) ;
1896+
1897+ it ( 'should reject symlink in /file-content with 403' , async ( ) => {
1898+ await setup ( ) ;
1899+ const res = await httpRequest ( {
1900+ hostname : '127.0.0.1' ,
1901+ port : inst . port ,
1902+ path : `/api/sessions/${ inst . defaultId } /file-content?file=link.txt` ,
1903+ method : 'GET' ,
1904+ } ) ;
1905+ assert . strictEqual ( res . statusCode , 403 ) ;
1906+ const data = JSON . parse ( res . data ) ;
1907+ assert . strictEqual ( data . error , 'Symbolic links are not allowed' ) ;
1908+ } ) ;
1909+
1910+ it ( 'should filter symlinks from /files listing' , async ( ) => {
1911+ await setup ( ) ;
1912+ const res = await httpRequest ( {
1913+ hostname : '127.0.0.1' ,
1914+ port : inst . port ,
1915+ path : `/api/sessions/${ inst . defaultId } /files` ,
1916+ method : 'GET' ,
1917+ } ) ;
1918+ assert . strictEqual ( res . statusCode , 200 ) ;
1919+ const data = JSON . parse ( res . data ) ;
1920+ const names = data . entries . map ( ( e ) => e . name ) ;
1921+ assert . ok ( names . includes ( 'real.txt' ) , 'should include real file' ) ;
1922+ assert . ok ( ! names . includes ( 'link.txt' ) , 'should not include symlink' ) ;
1923+ } ) ;
1924+ } ) ;
1925+
1926+ // === Entry limit + truncated flag ===
1927+ describe ( '/files truncated flag' , ( ) => {
1928+ let inst ;
1929+ let tmpDir ;
1930+ after ( ( ) => {
1931+ inst ?. shutdown ( ) ;
1932+ if ( tmpDir ) fs . rmSync ( tmpDir , { recursive : true , force : true } ) ;
1933+ } ) ;
1934+
1935+ async function setup ( ) {
1936+ if ( inst ) return ;
1937+ tmpDir = fs . mkdtempSync ( path . join ( require ( 'os' ) . tmpdir ( ) , 'tb-trunc-' ) ) ;
1938+ fs . writeFileSync ( path . join ( tmpDir , 'a.txt' ) , 'a' ) ;
1939+ fs . writeFileSync ( path . join ( tmpDir , 'b.txt' ) , 'b' ) ;
1940+ inst = await startServer ( { cwd : tmpDir } ) ;
1941+ }
1942+
1943+ it ( 'should include truncated: false for small directories' , async ( ) => {
1944+ await setup ( ) ;
1945+ const res = await httpRequest ( {
1946+ hostname : '127.0.0.1' ,
1947+ port : inst . port ,
1948+ path : `/api/sessions/${ inst . defaultId } /files` ,
1949+ method : 'GET' ,
1950+ } ) ;
1951+ assert . strictEqual ( res . statusCode , 200 ) ;
1952+ const data = JSON . parse ( res . data ) ;
1953+ assert . ok ( 'truncated' in data , 'Response should have a truncated field' ) ;
1954+ assert . strictEqual ( data . truncated , false ) ;
1955+ } ) ;
1956+
1957+ it ( 'should include base and rootDir in /files response' , async ( ) => {
1958+ await setup ( ) ;
1959+ const res = await httpRequest ( {
1960+ hostname : '127.0.0.1' ,
1961+ port : inst . port ,
1962+ path : `/api/sessions/${ inst . defaultId } /files` ,
1963+ method : 'GET' ,
1964+ } ) ;
1965+ assert . strictEqual ( res . statusCode , 200 ) ;
1966+ const data = JSON . parse ( res . data ) ;
1967+ assert . ok ( 'base' in data , 'Response should have a base field' ) ;
1968+ assert . ok ( 'rootDir' in data , 'Response should have a rootDir field' ) ;
1969+ assert . ok ( 'entries' in data , 'Response should have entries field' ) ;
1970+ assert . ok ( 'truncated' in data , 'Response should have truncated field' ) ;
1971+ } ) ;
1972+ } ) ;
1973+
1974+ // === /api/sessions/:id/file-raw endpoint ===
1975+ describe ( 'GET /api/sessions/:id/file-raw' , ( ) => {
1976+ let inst ;
1977+ let tmpDir ;
1978+ after ( ( ) => {
1979+ inst ?. shutdown ( ) ;
1980+ if ( tmpDir ) fs . rmSync ( tmpDir , { recursive : true , force : true } ) ;
1981+ } ) ;
1982+
1983+ async function setup ( ) {
1984+ if ( inst ) return ;
1985+ tmpDir = fs . mkdtempSync ( path . join ( require ( 'os' ) . tmpdir ( ) , 'tb-fileraw-' ) ) ;
1986+ fs . writeFileSync ( path . join ( tmpDir , 'hello.txt' ) , 'hello raw' ) ;
1987+ fs . mkdirSync ( path . join ( tmpDir , 'adir' ) ) ;
1988+ inst = await startServer ( { cwd : tmpDir } ) ;
1989+ }
1990+
1991+ it ( 'should return file content inline (no Content-Disposition: attachment)' , async ( ) => {
1992+ await setup ( ) ;
1993+ const res = await httpRequest ( {
1994+ hostname : '127.0.0.1' ,
1995+ port : inst . port ,
1996+ path : `/api/sessions/${ inst . defaultId } /file-raw?file=hello.txt` ,
1997+ method : 'GET' ,
1998+ } ) ;
1999+ assert . strictEqual ( res . statusCode , 200 ) ;
2000+ assert . strictEqual ( res . data , 'hello raw' ) ;
2001+ const cd = res . headers [ 'content-disposition' ] ;
2002+ assert . ok ( ! cd || ! cd . includes ( 'attachment' ) , 'should not have attachment disposition' ) ;
2003+ } ) ;
2004+
2005+ it ( 'should return 404 for non-existent file' , async ( ) => {
2006+ await setup ( ) ;
2007+ const res = await httpRequest ( {
2008+ hostname : '127.0.0.1' ,
2009+ port : inst . port ,
2010+ path : `/api/sessions/${ inst . defaultId } /file-raw?file=no-such-file.txt` ,
2011+ method : 'GET' ,
2012+ } ) ;
2013+ assert . strictEqual ( res . statusCode , 404 ) ;
2014+ const data = JSON . parse ( res . data ) ;
2015+ assert . strictEqual ( data . error , 'File not found' ) ;
2016+ } ) ;
2017+
2018+ it ( 'should return 400 for missing file parameter' , async ( ) => {
2019+ await setup ( ) ;
2020+ const res = await httpRequest ( {
2021+ hostname : '127.0.0.1' ,
2022+ port : inst . port ,
2023+ path : `/api/sessions/${ inst . defaultId } /file-raw` ,
2024+ method : 'GET' ,
2025+ } ) ;
2026+ assert . strictEqual ( res . statusCode , 400 ) ;
2027+ const data = JSON . parse ( res . data ) ;
2028+ assert . strictEqual ( data . error , 'Missing file parameter' ) ;
2029+ } ) ;
2030+
2031+ it ( 'should return 400 for directory (not a file)' , async ( ) => {
2032+ await setup ( ) ;
2033+ const res = await httpRequest ( {
2034+ hostname : '127.0.0.1' ,
2035+ port : inst . port ,
2036+ path : `/api/sessions/${ inst . defaultId } /file-raw?file=adir` ,
2037+ method : 'GET' ,
2038+ } ) ;
2039+ assert . strictEqual ( res . statusCode , 400 ) ;
2040+ const data = JSON . parse ( res . data ) ;
2041+ assert . strictEqual ( data . error , 'Not a regular file' ) ;
2042+ } ) ;
2043+
2044+ it ( 'should return 404 for invalid session id' , async ( ) => {
2045+ await setup ( ) ;
2046+ const res = await httpRequest ( {
2047+ hostname : '127.0.0.1' ,
2048+ port : inst . port ,
2049+ path : '/api/sessions/nonexistent/file-raw?file=hello.txt' ,
2050+ method : 'GET' ,
2051+ } ) ;
2052+ assert . strictEqual ( res . statusCode , 404 ) ;
2053+ const data = JSON . parse ( res . data ) ;
2054+ assert . strictEqual ( data . error , 'Session not found' ) ;
2055+ } ) ;
2056+ } ) ;
2057+
2058+ // === Generic error message for /files ===
2059+ describe ( '/files generic error message' , ( ) => {
2060+ let inst ;
2061+ let tmpDir ;
2062+ after ( ( ) => {
2063+ inst ?. shutdown ( ) ;
2064+ if ( tmpDir ) fs . rmSync ( tmpDir , { recursive : true , force : true } ) ;
2065+ } ) ;
2066+
2067+ async function setup ( ) {
2068+ if ( inst ) return ;
2069+ tmpDir = fs . mkdtempSync ( path . join ( require ( 'os' ) . tmpdir ( ) , 'tb-generr-' ) ) ;
2070+ inst = await startServer ( { cwd : tmpDir } ) ;
2071+ }
2072+
2073+ it ( 'should return generic error for non-existent directory' , async ( ) => {
2074+ await setup ( ) ;
2075+ const badDir = path . join ( tmpDir , 'does-not-exist' ) ;
2076+ const res = await httpRequest ( {
2077+ hostname : '127.0.0.1' ,
2078+ port : inst . port ,
2079+ path : `/api/sessions/${ inst . defaultId } /files?dir=${ encodeURIComponent ( badDir ) } ` ,
2080+ method : 'GET' ,
2081+ } ) ;
2082+ assert . strictEqual ( res . statusCode , 500 ) ;
2083+ const data = JSON . parse ( res . data ) ;
2084+ assert . strictEqual ( data . error , 'Failed to read directory' ) ;
2085+ } ) ;
2086+ } ) ;
17842087} ) ;
0 commit comments