@@ -5,8 +5,12 @@ import (
55 "errors"
66 "fmt"
77 "log"
8+ "mime/multipart"
89 "net/http"
10+ "net/textproto"
911 "os"
12+ "path/filepath"
13+ "strings"
1014 "testing"
1115
1216 "github.com/h2non/gock"
@@ -61,7 +65,7 @@ func TestDownloadCommand(t *testing.T) {
6165 Get ("/v1/projects/" + project + "/functions/" + slug + "/body" ).
6266 Reply (http .StatusOK )
6367 // Run test
64- err = Run (context .Background (), slug , project , true , fsys )
68+ err = Run (context .Background (), slug , project , true , false , false , fsys )
6569 // Check error
6670 assert .NoError (t , err )
6771 assert .Empty (t , apitest .ListUnmatchedRequests ())
@@ -73,7 +77,7 @@ func TestDownloadCommand(t *testing.T) {
7377 // Setup valid project ref
7478 project := apitest .RandomProjectRef ()
7579 // Run test
76- err := Run (context .Background (), "@" , project , true , fsys )
80+ err := Run (context .Background (), "@" , project , true , false , false , fsys )
7781 // Check error
7882 assert .ErrorContains (t , err , "Invalid Function name." )
7983 })
@@ -84,7 +88,7 @@ func TestDownloadCommand(t *testing.T) {
8488 // Setup valid project ref
8589 project := apitest .RandomProjectRef ()
8690 // Run test
87- err := Run (context .Background (), slug , project , true , fsys )
91+ err := Run (context .Background (), slug , project , true , false , false , fsys )
8892 // Check error
8993 assert .ErrorContains (t , err , "operation not permitted" )
9094 })
@@ -98,7 +102,7 @@ func TestDownloadCommand(t *testing.T) {
98102 _ , err := fsys .Create (utils .DenoPathOverride )
99103 require .NoError (t , err )
100104 // Run test
101- err = Run (context .Background (), slug , project , true , afero .NewReadOnlyFs (fsys ))
105+ err = Run (context .Background (), slug , project , true , false , false , afero .NewReadOnlyFs (fsys ))
102106 // Check error
103107 assert .ErrorContains (t , err , "operation not permitted" )
104108 })
@@ -121,7 +125,7 @@ func TestDownloadCommand(t *testing.T) {
121125 Reply (http .StatusNotFound ).
122126 JSON (map [string ]string {"message" : "Function not found" })
123127 // Run test
124- err = Run (context .Background (), slug , project , true , fsys )
128+ err = Run (context .Background (), slug , project , true , false , false , fsys )
125129 // Check error
126130 assert .ErrorContains (t , err , "Function test-func does not exist on the Supabase project." )
127131 })
@@ -235,3 +239,132 @@ func TestGetMetadata(t *testing.T) {
235239 assert .Nil (t , meta )
236240 })
237241}
242+
243+ func TestNormalizeRelativePath (t * testing.T ) {
244+ t .Parallel ()
245+
246+ t .Run ("returns cleaned relative path" , func (t * testing.T ) {
247+ got := normalizeRelativePath ("test-func" , "src/index.ts" )
248+ assert .Equal (t , filepath .Join ("src" , "index.ts" ), got )
249+ })
250+
251+ t .Run ("strips slug prefix" , func (t * testing.T ) {
252+ got := normalizeRelativePath ("test-func" , "test-func/index.ts" )
253+ assert .Equal (t , "index.ts" , got )
254+ })
255+
256+ t .Run ("strips source prefix" , func (t * testing.T ) {
257+ got := normalizeRelativePath ("test-func" , "source/index.ts" )
258+ assert .Equal (t , "index.ts" , got )
259+ })
260+
261+ t .Run ("skips slug directory itself" , func (t * testing.T ) {
262+ got := normalizeRelativePath ("test-func" , "test-func" )
263+ assert .Equal (t , "" , got )
264+ })
265+ }
266+
267+ func TestResolvedPartPath (t * testing.T ) {
268+ t .Parallel ()
269+
270+ newPart := func (headers map [string ]string ) * multipart.Part {
271+ mh := make (textproto.MIMEHeader , len (headers ))
272+ for k , v := range headers {
273+ mh .Set (k , v )
274+ }
275+ return & multipart.Part {Header : mh }
276+ }
277+
278+ t .Run ("returns path from Supabase header" , func (t * testing.T ) {
279+ part := newPart (map [string ]string {
280+ "Supabase-Path" : "dir/file.ts" ,
281+ })
282+ got , err := resolvedPartPath ("test-func" , part )
283+ require .NoError (t , err )
284+ assert .Equal (t , filepath .Join ("dir" , "file.ts" ), got )
285+ })
286+
287+ t .Run ("returns filename from content disposition" , func (t * testing.T ) {
288+ part := newPart (map [string ]string {
289+ "Content-Disposition" : `form-data; name="file"; filename="test-func/index.ts"` ,
290+ })
291+ got , err := resolvedPartPath ("test-func" , part )
292+ require .NoError (t , err )
293+ assert .Equal (t , "index.ts" , got )
294+ })
295+
296+ t .Run ("returns filename from editor-originated content disposition" , func (t * testing.T ) {
297+ part := newPart (map [string ]string {
298+ "Content-Disposition" : `form-data; name="file"; filename="source/index.ts"` ,
299+ })
300+ got , err := resolvedPartPath ("test-func" , part )
301+ require .NoError (t , err )
302+ assert .Equal (t , "index.ts" , got )
303+ })
304+
305+ t .Run ("writes file of arbitrary depth to slug directory" , func (t * testing.T ) {
306+ part := newPart (map [string ]string {
307+ "Content-Disposition" : `form-data; name="file"; filename="test-func/dir/subdir/file.ts"` ,
308+ })
309+ got , err := resolvedPartPath ("test-func" , part )
310+ require .NoError (t , err )
311+ assert .Equal (t , filepath .Join ("dir" , "subdir" , "file.ts" ), got )
312+ })
313+
314+ t .Run ("returns empty when no filename provided" , func (t * testing.T ) {
315+ part := newPart (map [string ]string {
316+ "Content-Disposition" : `form-data; name="file"` ,
317+ })
318+ got , err := resolvedPartPath ("test-func" , part )
319+ require .NoError (t , err )
320+ assert .Equal (t , "" , got )
321+ })
322+
323+ t .Run ("returns error on invalid content disposition" , func (t * testing.T ) {
324+ part := newPart (map [string ]string {
325+ "Content-Disposition" : `form-data; filename="unterminated` ,
326+ })
327+ got , err := resolvedPartPath ("test-func" , part )
328+ require .ErrorContains (t , err , "failed to parse content disposition" )
329+ assert .Equal (t , "" , got )
330+ })
331+ }
332+
333+ func TestJoinWithinDir (t * testing.T ) {
334+ t .Parallel ()
335+
336+ base := filepath .Join (os .TempDir (), "base-dir" )
337+
338+ t .Run ("joins path within base directory" , func (t * testing.T ) {
339+ got , err := joinWithinDir (base , filepath .Join ("sub" , "file.ts" ))
340+ require .NoError (t , err )
341+ assert .True (t , strings .HasPrefix (filepath .Clean (got ), filepath .Clean (base )+ "/" ) || filepath .Clean (got ) == filepath .Clean (base ))
342+ })
343+
344+ t .Run ("treats leading slash as relative to base" , func (t * testing.T ) {
345+ got , err := joinWithinDir (base , "/foo/bar.ts" )
346+ require .NoError (t , err )
347+ assert .True (t , strings .HasPrefix (filepath .Clean (got ), filepath .Clean (base )+ "/" ))
348+ assert .Equal (t , filepath .Join (filepath .Clean (base ), "foo" , "bar.ts" ), filepath .Clean (got ))
349+ })
350+
351+ t .Run ("rejects absolute path" , func (t * testing.T ) {
352+ abs := "/" + filepath .Join ("etc" , "passwd" )
353+ got , err := joinWithinDir (base , abs )
354+ require .NoError (t , err )
355+ assert .True (t , strings .HasPrefix (filepath .Clean (got ), filepath .Clean (base )+ "/" ))
356+ })
357+
358+ t .Run ("rejects parent directory traversal" , func (t * testing.T ) {
359+ got , err := joinWithinDir (base , filepath .Join (".." , "escape" ))
360+ require .Error (t , err )
361+ assert .Equal (t , "" , got )
362+ })
363+
364+ t .Run ("accepts traversal within base directory" , func (t * testing.T ) {
365+ base = os .TempDir ()
366+ got , err := joinWithinDir (base , filepath .Join ("some" , ".." , "file.ts" ))
367+ require .NoError (t , err )
368+ assert .Equal (t , filepath .Join (base , "file.ts" ), got )
369+ })
370+ }
0 commit comments