package main import ( "encoding/base64" "os" "path/filepath" "strings" "testing" ) func tempCap(t *testing.T, allowed ...string) *Capability { t.Helper() return &Capability{ Name: "fs.test", PathsAllowed: allowed, } } func TestFsRead_PathNotAllowed(t *testing.T) { tmp := t.TempDir() cap := tempCap(t, tmp+"/**") _, _, err := runFsRead(cap, map[string]any{"path": "/etc/passwd"}) if err == nil || !strings.Contains(err.Error(), "not allowed") { t.Fatalf("expected not allowed, got: %v", err) } } func TestFsRead_PathAllowed(t *testing.T) { tmp := t.TempDir() target := filepath.Join(tmp, "test.txt") if err := os.WriteFile(target, []byte("hello world"), 0644); err != nil { t.Fatal(err) } cap := tempCap(t, tmp+"/**") res, code, err := runFsRead(cap, map[string]any{"path": target}) if err != nil { t.Fatalf("unexpected error: %v", err) } if code != 0 { t.Fatalf("expected exit=0 got %d", code) } m := res.(map[string]any) b, _ := base64.StdEncoding.DecodeString(m["content_b64"].(string)) if string(b) != "hello world" { t.Fatalf("content mismatch: %q", string(b)) } } func TestFsRead_MaxBytesTrunca(t *testing.T) { tmp := t.TempDir() target := filepath.Join(tmp, "big.bin") if err := os.WriteFile(target, make([]byte, 1000), 0644); err != nil { t.Fatal(err) } cap := tempCap(t, tmp+"/**") res, _, err := runFsRead(cap, map[string]any{"path": target, "max_bytes": 100}) if err != nil { t.Fatal(err) } m := res.(map[string]any) if !m["truncated"].(bool) { t.Fatalf("expected truncated=true") } if m["bytes_read"].(int) != 100 { t.Fatalf("expected 100 bytes, got %v", m["bytes_read"]) } } func TestFsWrite_CreaParent(t *testing.T) { tmp := t.TempDir() target := filepath.Join(tmp, "subdir", "nested", "x.txt") cap := tempCap(t, tmp+"/**") body := base64.StdEncoding.EncodeToString([]byte("payload")) _, _, err := runFsWrite(cap, map[string]any{"path": target, "content_b64": body}) if err != nil { t.Fatalf("write: %v", err) } got, err := os.ReadFile(target) if err != nil { t.Fatal(err) } if string(got) != "payload" { t.Fatalf("content mismatch: %q", string(got)) } } func TestFsList_NoRecursive(t *testing.T) { tmp := t.TempDir() for _, n := range []string{"a.txt", "b.txt", "c.txt"} { os.WriteFile(filepath.Join(tmp, n), []byte("x"), 0644) } os.Mkdir(filepath.Join(tmp, "subdir"), 0755) // archivo en subdir NO debe aparecer (no recursivo) os.WriteFile(filepath.Join(tmp, "subdir", "hidden.txt"), []byte("y"), 0644) cap := tempCap(t, tmp+"/**") res, _, err := runFsList(cap, map[string]any{"dir": tmp}) if err != nil { t.Fatal(err) } m := res.(map[string]any) entries := m["entries"].([]map[string]any) if len(entries) != 4 { t.Fatalf("expected 4 entries, got %d: %+v", len(entries), entries) } // Check kind correcto: subdir es dir for _, e := range entries { if e["name"] == "subdir" && e["kind"] != "dir" { t.Fatalf("subdir kind != dir: %v", e) } } } func TestFsStat_OK(t *testing.T) { tmp := t.TempDir() target := filepath.Join(tmp, "z.txt") os.WriteFile(target, []byte("abc"), 0644) cap := tempCap(t, tmp+"/**") res, _, err := runFsStat(cap, map[string]any{"path": target}) if err != nil { t.Fatal(err) } m := res.(map[string]any) if m["kind"] != "file" { t.Fatalf("kind: %v", m["kind"]) } if m["size"].(int64) != 3 { t.Fatalf("size: %v", m["size"]) } } func TestPathTraversal_DotDot(t *testing.T) { tmp := t.TempDir() cap := tempCap(t, tmp+"/**") traversal := filepath.Join(tmp, "..", "..", "..", "etc", "passwd") _, _, err := runFsRead(cap, map[string]any{"path": traversal}) if err == nil || !strings.Contains(err.Error(), "not allowed") { t.Fatalf("expected blocked traversal, got err=%v", err) } } func TestPathTraversal_Symlink(t *testing.T) { tmp := t.TempDir() // crear symlink dentro de tmp apuntando a /etc/passwd linkPath := filepath.Join(tmp, "leak") if err := os.Symlink("/etc/passwd", linkPath); err != nil { t.Skipf("symlink failed: %v", err) } cap := tempCap(t, tmp+"/**") _, _, err := runFsRead(cap, map[string]any{"path": linkPath}) // EvalSymlinks lo resuelve a /etc/passwd que NO esta en allowed -> reject if err == nil { t.Fatalf("expected symlink leak rejected") } } func TestFsList_Glob(t *testing.T) { tmp := t.TempDir() for _, n := range []string{"a.log", "b.log", "c.txt"} { os.WriteFile(filepath.Join(tmp, n), []byte("x"), 0644) } cap := tempCap(t, tmp+"/**") res, _, err := runFsList(cap, map[string]any{"dir": tmp, "glob": "*.log"}) if err != nil { t.Fatal(err) } m := res.(map[string]any) entries := m["entries"].([]map[string]any) if len(entries) != 2 { t.Fatalf("expected 2 .log, got %d", len(entries)) } }