package zfsadm import ( "context" "errors" "strings" "testing" ) func TestValidators(t *testing.T) { for _, s := range []string{"media", "media/video", "a.b_c:d-e", "x/y/z"} { if err := ValidateRelName(s); err != nil { t.Errorf("%q rejected: %v", s, err) } } for _, s := range []string{"", "/media", "media/", "a//b", "..", "a/../b", ".", "-x", "a/-b", "a b", "a;b", "a/b\n", strings.Repeat("x", 201)} { if ValidateRelName(s) == nil { t.Errorf("%q accepted", s) } } for _, s := range []string{"", "100G", "1.5T", "512M", "10k", "42"} { if ValidateSize(s) != nil { t.Errorf("size %q rejected", s) } } for _, s := range []string{"10 G", "10GB", "G", "-1G", "1e3"} { if ValidateSize(s) == nil { t.Errorf("size %q accepted", s) } } if ValidateCompression("gzip") == nil || ValidateCompression("lz4") != nil || ValidateCompression("") != nil { t.Error("compression validation") } if ValidateOwner("ansible") == nil || ValidateOwner("1000:1000") != nil || ValidateOwner("") != nil { t.Error("owner validation") } } // fake records calls and answers by command prefix. type fake struct { calls []string reply map[string]string fail map[string]string } func (f *fake) run(_ context.Context, name string, args ...string) (string, error) { c := name + " " + strings.Join(args, " ") f.calls = append(f.calls, c) for k, v := range f.fail { if strings.HasPrefix(c, k) { return v, errors.New(v) } } for k, v := range f.reply { if strings.HasPrefix(c, k) { return v, nil } } return "", nil } func TestCreate(t *testing.T) { f := &fake{reply: map[string]string{"zfs get": "/tank/media"}} a := New(f.run) if err := a.Create("tank/media", CreateOpts{Compression: "lz4", Quota: "100G", Owner: "1000:1000"}); err != nil { t.Fatal(err) } want := []string{ "zfs create -p -o compression=lz4 -o quota=100G tank/media", "zfs get -H -o value mountpoint tank/media", "chown 1000:1000 /tank/media", } if strings.Join(f.calls, "\n") != strings.Join(want, "\n") { t.Errorf("calls:\n%s\nwant:\n%s", strings.Join(f.calls, "\n"), strings.Join(want, "\n")) } // no owner, inherit everything f = &fake{} a = New(f.run) if err := a.Create("tank/x", CreateOpts{}); err != nil { t.Fatal(err) } if len(f.calls) != 1 || f.calls[0] != "zfs create -p tank/x" { t.Errorf("calls = %v", f.calls) } // invalid input never reaches the runner f = &fake{} a = New(f.run) if err := a.Create("tank/../x", CreateOpts{}); err == nil || len(f.calls) != 0 { t.Errorf("bad name: err=%v calls=%v", err, f.calls) } if err := a.Create("tank/x", CreateOpts{Quota: "lots"}); err == nil || len(f.calls) != 0 { t.Errorf("bad quota: err=%v calls=%v", err, f.calls) } // zfs error is surfaced verbatim f = &fake{fail: map[string]string{"zfs create": "cannot create 'tank/x': out of space"}} a = New(f.run) if err := a.Create("tank/x", CreateOpts{}); err == nil || !strings.Contains(err.Error(), "out of space") { t.Errorf("err = %v", err) } // unmounted dataset: created but chown skipped, reported f = &fake{reply: map[string]string{"zfs get": "none"}} a = New(f.run) err := a.Create("tank/x", CreateOpts{Owner: "1000:1000"}) if err == nil || !strings.Contains(err.Error(), "chown skipped") { t.Errorf("err = %v", err) } for _, c := range f.calls { if strings.HasPrefix(c, "chown") { t.Errorf("chown must not run: %v", f.calls) } } } func TestDestroyEmpty(t *testing.T) { ok := map[string]string{"zfs list -Hp -r -t filesystem": "tank/x\t98304", "zfs list -H -t snapshot": ""} f := &fake{reply: ok} a := New(f.run) if err := a.DestroyEmpty("tank/x"); err != nil { t.Fatal(err) } if f.calls[len(f.calls)-1] != "zfs destroy tank/x" { t.Errorf("calls = %v", f.calls) } cases := map[string]map[string]string{ "children": {"zfs list -Hp -r -t filesystem": "tank/x\t98304\ntank/x/y\t98304", "zfs list -H -t snapshot": ""}, "not empty": {"zfs list -Hp -r -t filesystem": "tank/x\t5000000", "zfs list -H -t snapshot": ""}, "snapshots": {"zfs list -Hp -r -t filesystem": "tank/x\t98304", "zfs list -H -t snapshot": "tank/x@s1"}, } for name, reply := range cases { f := &fake{reply: reply} a := New(f.run) err := a.DestroyEmpty("tank/x") if err == nil { t.Errorf("%s: destroyed", name) } for _, c := range f.calls { if strings.HasPrefix(c, "zfs destroy") { t.Errorf("%s: destroy ran", name) } } } f = &fake{reply: ok} a = New(f.run) if err := a.DestroyEmpty("tank"); err == nil || len(f.calls) != 0 { t.Errorf("pool root: err=%v calls=%v", err, f.calls) } }