package restic import ( "strings" "testing" ) // realistic restic ls --json output sample. First line is the // snapshot preamble, subsequent lines are nodes. Trimmed to a few // entries that exercise depth filtering. const sampleLsOutput = `{"struct_type":"snapshot","time":"2026-05-04T09:14:00Z","id":"f3a7b2c1"} {"name":"etc","type":"dir","path":"/etc","permissions":"drwxr-xr-x","struct_type":"node"} {"name":"nginx","type":"dir","path":"/etc/nginx","permissions":"drwxr-xr-x","struct_type":"node"} {"name":"nginx.conf","type":"file","path":"/etc/nginx/nginx.conf","size":2400,"struct_type":"node"} {"name":"sites-available","type":"dir","path":"/etc/nginx/sites-available","struct_type":"node"} {"name":"alfa.conf","type":"file","path":"/etc/nginx/sites-available/alfa.conf","size":3100,"struct_type":"node"} {"name":"default.conf","type":"file","path":"/etc/nginx/sites-available/default.conf","size":2900,"struct_type":"node"} ` func TestParseLsChildrenAtRoot(t *testing.T) { t.Parallel() entries, err := parseLsChildren(strings.NewReader(sampleLsOutput), "/") if err != nil { t.Fatalf("parse: %v", err) } if len(entries) != 1 { t.Fatalf("entries: got %d (%+v), want 1", len(entries), entries) } if entries[0].Name != "etc" || entries[0].Path != "/etc" || entries[0].Type != "dir" { t.Fatalf("entry: %+v", entries[0]) } } func TestParseLsChildrenAtEtc(t *testing.T) { t.Parallel() entries, err := parseLsChildren(strings.NewReader(sampleLsOutput), "/etc") if err != nil { t.Fatalf("parse: %v", err) } if len(entries) != 1 { t.Fatalf("entries: got %d, want 1 (just nginx, not nested children)", len(entries)) } if entries[0].Name != "nginx" { t.Fatalf("entry: %+v", entries[0]) } } func TestParseLsChildrenAtNginx(t *testing.T) { t.Parallel() entries, err := parseLsChildren(strings.NewReader(sampleLsOutput), "/etc/nginx") if err != nil { t.Fatalf("parse: %v", err) } if len(entries) != 2 { t.Fatalf("entries: got %d (%+v), want 2 (nginx.conf + sites-available, not nested)", len(entries), entries) } gotNames := []string{entries[0].Name, entries[1].Name} want := map[string]bool{"nginx.conf": true, "sites-available": true} for _, n := range gotNames { if !want[n] { t.Errorf("unexpected name %q in result", n) } } } func TestParseLsChildrenAtSitesAvailable(t *testing.T) { t.Parallel() entries, err := parseLsChildren(strings.NewReader(sampleLsOutput), "/etc/nginx/sites-available") if err != nil { t.Fatalf("parse: %v", err) } if len(entries) != 2 { t.Fatalf("entries: got %d, want 2", len(entries)) } for _, e := range entries { if e.Type != "file" { t.Errorf("expected file type, got %q on %q", e.Type, e.Name) } } } func TestNormalizeTreePath(t *testing.T) { t.Parallel() cases := []struct{ in, want string }{ {"", "/"}, {"/", "/"}, {"/etc", "/etc"}, {"/etc/", "/etc"}, {"etc/nginx", "/etc/nginx"}, {"/etc//nginx", "/etc/nginx"}, {"/etc/./nginx", "/etc/nginx"}, } for _, c := range cases { got := normalizeTreePath(c.in) if got != c.want { t.Errorf("normalizeTreePath(%q): got %q, want %q", c.in, got, c.want) } } } func TestIsDirectChild(t *testing.T) { t.Parallel() cases := []struct { child, parent string want bool }{ {"/etc", "/", true}, {"/etc/nginx", "/", false}, {"/etc/nginx", "/etc", true}, {"/etc/nginx/conf", "/etc", false}, {"/etc/nginx/conf", "/etc/nginx", true}, {"/etc", "/etc", false}, {"/etcc", "/etc", false}, // prefix match guard } for _, c := range cases { got := isDirectChild(c.child, c.parent) if got != c.want { t.Errorf("isDirectChild(%q, %q): got %v, want %v", c.child, c.parent, got, c.want) } } }