diff --git a/build/coverage.sh b/build/coverage.sh index 24ebdb8..60056fd 100755 --- a/build/coverage.sh +++ b/build/coverage.sh @@ -10,5 +10,5 @@ go tool cover -html=coverage.out -o coverage.html rm -rf coverage.out # The HTML file can either be opened locally or served through HTTP using a CLI tool like miniserve -# https://github.com/svenstaro/miniserve +# https://github.com/svenstaro/miniserve. The latter is great if you are work # e.g `miniserve coverage.html` \ No newline at end of file diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 34975f8..93ef3a3 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -71,14 +71,14 @@ devices: for _, subtest := range subtests { t.Run(subtest.Name, func(t *testing.T) { configPath, err := createConfigFile(subtest.Data) - utils.CheckErrorGlob("createConfigFile()", t, nil, err) + utils.CheckError("createConfigFile()", t, nil, err) defer os.Remove(configPath) c, err := New([]string{"ebs-bootstrap", "-config", configPath}) utils.CheckErrorGlob("config.New()", t, subtest.ExpectedErr, err) // Config contains the unexported attribute "overrides" // We need to allow go-cmp to inspect the contents of unexported attributes - utils.CheckOutputCmp("config.New()", t, subtest.ExpectedOutput, c, cmp.AllowUnexported(Config{})) + utils.CheckOutput("config.New()", t, subtest.ExpectedOutput, c, cmp.AllowUnexported(Config{})) }) } } @@ -86,7 +86,7 @@ devices: func TestFlagParsing(t *testing.T) { // Create a variable to the current working directory d, err := os.Getwd() - utils.CheckErrorGlob("os.Getwd()", t, nil, err) + utils.CheckError("os.Getwd()", t, nil, err) subtests := []struct { Name string @@ -148,7 +148,7 @@ devices: Name: "Default Options for Non-Existent Device", Data: []byte(`--- devices: - /dev/null: ~`), + /dev/nonexist: ~`), ExpectedOutput: &Options{ Mode: model.Healthcheck, Remount: false, @@ -162,11 +162,11 @@ devices: for _, subtest := range subtests { t.Run(subtest.Name, func(t *testing.T) { configPath, err := createConfigFile(subtest.Data) - utils.CheckErrorGlob("createConfigFile()", t, nil, err) + utils.CheckError("createConfigFile()", t, nil, err) defer os.Remove(configPath) c, err := New([]string{"ebs-bootstrap", "-config", configPath}) - utils.CheckErrorGlob("config.New()", t, subtest.ExpectedErr, err) + utils.CheckError("config.New()", t, subtest.ExpectedErr, err) d := &Options{ Mode: c.GetMode(device), @@ -175,7 +175,7 @@ devices: ResizeFs: c.GetResizeFs(device), ResizeThreshold: c.GetResizeThreshold(device), } - utils.CheckOutputCmp("config.New()", t, subtest.ExpectedOutput, d) + utils.CheckOutput("config.New()", t, subtest.ExpectedOutput, d) }) } } @@ -185,7 +185,7 @@ func TestFlagOptions(t *testing.T) { c, err := createConfigFile([]byte(fmt.Sprintf(`--- devices: %s: ~`, device))) - utils.CheckErrorGlob("createConfigFile()", t, nil, err) + utils.CheckError("createConfigFile()", t, nil, err) defer os.Remove(c) subtests := []struct { Name string @@ -233,7 +233,7 @@ devices: for _, subtest := range subtests { t.Run(subtest.Name, func(t *testing.T) { c, err := New(subtest.Args) - utils.CheckErrorGlob("config.New()", t, subtest.ExpectedErr, err) + utils.CheckError("config.New()", t, subtest.ExpectedErr, err) o := &Options{ Mode: c.GetMode(device), @@ -242,7 +242,7 @@ devices: ResizeFs: c.GetResizeFs(device), ResizeThreshold: c.GetResizeThreshold(device), } - utils.CheckOutputCmp("config.New()", t, subtest.ExpectedOutput, o) + utils.CheckOutput("config.New()", t, subtest.ExpectedOutput, o) }) } } @@ -309,11 +309,11 @@ devices: for _, subtest := range subtests { t.Run(subtest.Name, func(t *testing.T) { configPath, err := createConfigFile(subtest.Data) - utils.CheckErrorGlob("createConfigFile()", t, nil, err) + utils.CheckError("createConfigFile()", t, nil, err) defer os.Remove(configPath) c, err := New([]string{"ebs-bootstrap", "-config", configPath}) - utils.CheckErrorGlob("config.New()", t, subtest.ExpectedErr, err) + utils.CheckError("config.New()", t, subtest.ExpectedErr, err) d := &Options{ Mode: c.GetMode(device), @@ -322,7 +322,7 @@ devices: ResizeFs: c.GetResizeFs(device), ResizeThreshold: c.GetResizeThreshold(device), } - utils.CheckOutputCmp("config.New()", t, subtest.ExpectedOutput, d) + utils.CheckOutput("config.New()", t, subtest.ExpectedOutput, d) }) } } diff --git a/internal/config/validator.go b/internal/config/validator.go index 2ff31c2..25f59a8 100644 --- a/internal/config/validator.go +++ b/internal/config/validator.go @@ -62,7 +62,7 @@ func (fsv *ModeValidator) Validate(c *Config) error { mode := string(c.Defaults.Mode) _, err := model.ParseMode(mode) if err != nil { - return fmt.Errorf("🔴 '%s' (defaults) is not a global mode", mode) + return fmt.Errorf("🔴 '%s' (defaults) is not a supported mode", mode) } mode = string(c.overrides.Mode) @@ -75,7 +75,7 @@ func (fsv *ModeValidator) Validate(c *Config) error { mode := string(device.Mode) _, err := model.ParseMode(mode) if err != nil { - return fmt.Errorf("🔴 %s: %s is not a supported mode", name, mode) + return fmt.Errorf("🔴 %s: '%s' is not a supported mode", name, mode) } } return nil @@ -89,14 +89,13 @@ func NewMountPointValidator() *MountPointValidator { func (apv *MountPointValidator) Validate(c *Config) error { for name, device := range c.Devices { - if len(device.MountPoint) == 0 { - continue - } - if !path.IsAbs(device.MountPoint) { - return fmt.Errorf("🔴 %s: %s is not an absolute path", name, device.MountPoint) - } - if device.MountPoint == "/" { - return fmt.Errorf("🔴 %s: Can not be mounted to the root directory", name) + if len(device.MountPoint) > 0 { + if !path.IsAbs(device.MountPoint) { + return fmt.Errorf("🔴 %s: %s is not an absolute path", name, device.MountPoint) + } + if device.MountPoint == "/" { + return fmt.Errorf("🔴 %s: Can not be mounted to the root directory", name) + } } } return nil @@ -171,16 +170,20 @@ func NewResizeThresholdValidator() *ResizeThresholdValidator { } func (rtv *ResizeThresholdValidator) Validate(c *Config) error { - if c.Defaults.ResizeThreshold < 0 || c.Defaults.ResizeThreshold > 100 { + if !rtv.isValid(c.Defaults.ResizeThreshold) { return fmt.Errorf("🔴 '%g' (default) must be a floating point between 0 and 100 (inclusive)", c.Defaults.ResizeThreshold) } - if c.overrides.ResizeThreshold < 0 || c.overrides.ResizeThreshold > 100 { + if !rtv.isValid(c.overrides.ResizeThreshold) { return fmt.Errorf("🔴 '%g' (-resize-threshold) must be a floating point between 0 and 100 (inclusive)", c.overrides.ResizeThreshold) } for name, device := range c.Devices { - if device.ResizeThreshold < 0 || device.ResizeThreshold > 100 { + if !rtv.isValid(device.ResizeThreshold) { return fmt.Errorf("🔴 %s: '%g' must be a floating point between 0 and 100 (inclusive)", name, device.ResizeThreshold) } } return nil } + +func (rtv *ResizeThresholdValidator) isValid(rt float64) bool { + return rt >= 0 && rt <= 100 +} diff --git a/internal/config/validator_test.go b/internal/config/validator_test.go new file mode 100644 index 0000000..4e1b20e --- /dev/null +++ b/internal/config/validator_test.go @@ -0,0 +1,461 @@ +package config + +import ( + "fmt" + "testing" + + "github.com/reecetech/ebs-bootstrap/internal/model" + "github.com/reecetech/ebs-bootstrap/internal/service" + "github.com/reecetech/ebs-bootstrap/internal/utils" +) + +func TestDeviceValidator(t *testing.T) { + ds := service.NewMockDeviceService() + + subtests := []struct { + Name string + Config *Config + GetBlockDevice func(name string) (*model.BlockDevice, error) + ExpectedErr error + }{ + { + Name: "Valid Device", + Config: &Config{ + Devices: map[string]Device{ + "/dev/xvdf": {}, + }, + }, + GetBlockDevice: func(name string) (*model.BlockDevice, error) { + return &model.BlockDevice{Name: name}, nil + }, + ExpectedErr: nil, + }, + { + Name: "Non-existent Device", + Config: &Config{ + Devices: map[string]Device{ + "/dev/nonexist": {}, + }, + }, + GetBlockDevice: func(name string) (*model.BlockDevice, error) { + return nil, fmt.Errorf("🔴 lsblk: /dev/nonexist: not a block device") + }, + ExpectedErr: fmt.Errorf("🔴 /dev/nonexist is not a block device"), + }, + } + for _, subtest := range subtests { + ds.StubGetBlockDevice = subtest.GetBlockDevice + + dv := NewDeviceValidator(ds) + err := dv.Validate(subtest.Config) + utils.CheckError("dv.Validate()", t, subtest.ExpectedErr, err) + } +} + +func TestMountPointValidator(t *testing.T) { + subtests := []struct { + Name string + Config *Config + ExpectedErr error + }{ + { + Name: "Valid Mount Point", + Config: &Config{ + Devices: map[string]Device{ + "/dev/xvdf": { + MountPoint: "/mnt/app", + }, + }, + }, + ExpectedErr: nil, + }, + { + Name: "Invalid Mount Point (Relative Path)", + Config: &Config{ + Devices: map[string]Device{ + "/dev/xvdf": { + MountPoint: "relative-path/app", + }, + }, + }, + ExpectedErr: fmt.Errorf("🔴 /dev/xvdf: relative-path/app is not an absolute path"), + }, + { + Name: "Invalid Mount Point (Root Directory)", + Config: &Config{ + Devices: map[string]Device{ + "/dev/xvdf": { + MountPoint: "/", + }, + }, + }, + ExpectedErr: fmt.Errorf("🔴 /dev/xvdf: Can not be mounted to the root directory"), + }, + } + for _, subtest := range subtests { + mpv := NewMountPointValidator() + err := mpv.Validate(subtest.Config) + utils.CheckError("mpv.Validate()", t, subtest.ExpectedErr, err) + } +} + +func TestMountOptionsValidator(t *testing.T) { + subtests := []struct { + Name string + Config *Config + ExpectedErr error + }{ + { + Name: "Valid Mount Options", + Config: &Config{ + Defaults: Options{ + MountOptions: "defaults", + }, + Devices: map[string]Device{ + "/dev/xvdf": { + Options: Options{ + MountOptions: "nouuid", + }, + }, + }, + overrides: Options{ + MountOptions: "has_journal", + }, + }, + ExpectedErr: nil, + }, + { + Name: "Invalid Mount Options (Bind, Overrides)", + Config: &Config{ + Devices: map[string]Device{ + "/dev/xvdf": {}, + }, + overrides: Options{ + MountOptions: "bind", + }, + }, + ExpectedErr: fmt.Errorf("🔴 'bind' (-mount-options) is not a supported mode as bind mounts are not supported for block devices"), + }, + { + Name: "Invalid Mount Options (Remount, Defaults)", + Config: &Config{ + Defaults: Options{ + MountOptions: "remount", + }, + Devices: map[string]Device{ + "/dev/xvdf": {}, + }, + }, + ExpectedErr: fmt.Errorf("🔴 'remount' (defaults) is not a supported mode as it prevents unmounted devices from being mounted"), + }, + { + Name: "Invalid Mount Options (Bind, Device)", + Config: &Config{ + Devices: map[string]Device{ + "/dev/xvdf": { + Options: Options{ + MountOptions: "bind", + }, + }, + }, + }, + ExpectedErr: fmt.Errorf("🔴 /dev/xvdf: 'bind' is not a supported mode as bind mounts are not supported for block devices"), + }, + } + for _, subtest := range subtests { + mov := NewMountOptionsValidator() + err := mov.Validate(subtest.Config) + utils.CheckError("mov.Validate()", t, subtest.ExpectedErr, err) + } +} + +func TestFileSystemValidator(t *testing.T) { + subtests := []struct { + Name string + Config *Config + ExpectedErr error + }{ + { + Name: "Valid File System", + Config: &Config{ + Devices: map[string]Device{ + "/dev/xvdf": { + Fs: model.Ext4, + }, + }, + }, + ExpectedErr: nil, + }, + { + Name: "Unsupported File System", + Config: &Config{ + Devices: map[string]Device{ + "/dev/xvdf": { + Fs: model.FileSystem("zfs"), + }, + }, + }, + ExpectedErr: fmt.Errorf("🔴 /dev/xvdf: 'zfs' is not a supported file system"), + }, + { + Name: "No File System Provided", + Config: &Config{ + Devices: map[string]Device{ + "/dev/xvdf": {}, + }, + }, + ExpectedErr: fmt.Errorf("🔴 /dev/xvdf: Must provide a supported file system"), + }, + } + for _, subtest := range subtests { + fsv := NewFileSystemValidator() + err := fsv.Validate(subtest.Config) + utils.CheckError("fsv.Validate()", t, subtest.ExpectedErr, err) + } +} + +const ( + Invalid = model.Mode("invalid") +) + +func TestModeValidator(t *testing.T) { + + subtests := []struct { + Name string + Config *Config + ExpectedErr error + }{ + { + Name: "Valid Modes", + Config: &Config{ + Defaults: Options{ + Mode: model.Prompt, + }, + Devices: map[string]Device{ + "/dev/xvdf": { + Options: Options{ + Mode: model.Healthcheck, + }, + }, + }, + overrides: Options{ + Mode: model.Force, + }, + }, + ExpectedErr: nil, + }, + { + Name: "Invalid Mode (Overrides)", + Config: &Config{ + Defaults: Options{ + Mode: model.Force, + }, + Devices: map[string]Device{ + "/dev/xvdf": { + Options: Options{ + Mode: model.Prompt, + }, + }, + }, + overrides: Options{ + Mode: Invalid, + }, + }, + ExpectedErr: fmt.Errorf("🔴 '%s' (-mode) is not a supported mode", Invalid), + }, + { + Name: "Invalid Mode (Defaults)", + Config: &Config{ + Defaults: Options{ + Mode: Invalid, + }, + Devices: map[string]Device{ + "/dev/xvdf": { + Options: Options{ + Mode: model.Prompt, + }, + }, + }, + }, + ExpectedErr: fmt.Errorf("🔴 '%s' (defaults) is not a supported mode", Invalid), + }, + { + Name: "Invalid Mode (Device)", + Config: &Config{ + Defaults: Options{ + Mode: model.Force, + }, + Devices: map[string]Device{ + "/dev/xvdf": { + Options: Options{ + Mode: Invalid, + }, + }, + }, + }, + ExpectedErr: fmt.Errorf("🔴 /dev/xvdf: '%s' is not a supported mode", Invalid), + }, + } + for _, subtest := range subtests { + mv := NewModeValidator() + err := mv.Validate(subtest.Config) + utils.CheckError("mv.Validate()", t, subtest.ExpectedErr, err) + } +} + +func TestOwnerValidator(t *testing.T) { + os := service.NewMockOwnerService() + + subtests := []struct { + Name string + Config *Config + GetUser func(usr string) (*model.User, error) + GetGroup func(grp string) (*model.Group, error) + ExpectedErr error + }{ + { + Name: "Valid User and Valid Group", + Config: &Config{ + Devices: map[string]Device{ + "/dev/xvdf": { + User: "example", + Group: "example", + }, + }, + }, + GetUser: func(usr string) (*model.User, error) { + return &model.User{ + Name: usr, + Uid: 1000, + }, nil + }, + GetGroup: func(grp string) (*model.Group, error) { + return &model.Group{ + Name: grp, + Gid: 2000, + }, nil + }, + ExpectedErr: nil, + }, + { + Name: "Invalid User and Valid Group", + Config: &Config{ + Devices: map[string]Device{ + "/dev/xvdf": { + User: "example", + Group: "example", + }, + }, + }, + GetUser: func(usr string) (*model.User, error) { + return nil, fmt.Errorf("🔴 User (name=%s) does not exist", usr) + }, + GetGroup: func(grp string) (*model.Group, error) { + return &model.Group{ + Name: grp, + Gid: 2000, + }, nil + }, + ExpectedErr: fmt.Errorf("🔴 User (name=example) does not exist"), + }, + { + Name: "Valid User and Invalid Group", + Config: &Config{ + Devices: map[string]Device{ + "/dev/xvdf": { + User: "example", + Group: "example", + }, + }, + }, + GetUser: func(usr string) (*model.User, error) { + return &model.User{ + Name: usr, + Uid: 1000, + }, nil + }, + GetGroup: func(grp string) (*model.Group, error) { + return nil, fmt.Errorf("🔴 Group (name=%s) does not exist", grp) + }, + ExpectedErr: fmt.Errorf("🔴 Group (name=example) does not exist"), + }, + } + for _, subtest := range subtests { + os.StubGetUser = subtest.GetUser + os.StubGetGroup = subtest.GetGroup + + ov := NewOwnerValidator(os) + err := ov.Validate(subtest.Config) + utils.CheckError("ov.Validate()", t, subtest.ExpectedErr, err) + } +} + +func TestResizeThresholdValidator(t *testing.T) { + subtests := []struct { + Name string + Config *Config + ExpectedErr error + }{ + { + Name: "Valid Resize Thresholds", + Config: &Config{ + Defaults: Options{ + ResizeThreshold: 100, + }, + Devices: map[string]Device{ + "/dev/xvdf": { + Options: Options{ + ResizeThreshold: 75.0, + }, + }, + }, + overrides: Options{ + ResizeThreshold: 0, + }, + }, + ExpectedErr: nil, + }, + { + Name: "Invalid Resize Threshold (Defaults)", + Config: &Config{ + Defaults: Options{ + ResizeThreshold: -5.0, + }, + Devices: map[string]Device{ + "/dev/xvdf": {}, + }, + }, + ExpectedErr: fmt.Errorf("🔴 '-5' (default) must be a floating point between 0 and 100 (inclusive)"), + }, + { + Name: "Invalid Resize Threshold (Overrides)", + Config: &Config{ + Devices: map[string]Device{ + "/dev/xvdf": {}, + }, + overrides: Options{ + ResizeThreshold: 110.0, + }, + }, + ExpectedErr: fmt.Errorf("🔴 '110' (-resize-threshold) must be a floating point between 0 and 100 (inclusive)"), + }, + { + Name: "Invalid Resize Threshold (Device)", + Config: &Config{ + Devices: map[string]Device{ + "/dev/xvdf": { + Options: Options{ + ResizeThreshold: -1.0, + }, + }, + }, + }, + ExpectedErr: fmt.Errorf("🔴 /dev/xvdf: '-1' must be a floating point between 0 and 100 (inclusive)"), + }, + } + for _, subtest := range subtests { + rtv := NewResizeThresholdValidator() + err := rtv.Validate(subtest.Config) + utils.CheckError("rtv.Validate()", t, subtest.ExpectedErr, err) + } +} diff --git a/internal/service/device.go b/internal/service/device.go index dd0265e..0ab33c2 100644 --- a/internal/service/device.go +++ b/internal/service/device.go @@ -9,8 +9,6 @@ import ( "github.com/reecetech/ebs-bootstrap/internal/utils" ) -// Device Service Interface [START] - type DeviceService interface { GetSize(name string) (uint64, error) // bytes GetBlockDevices() ([]string, error) @@ -19,8 +17,6 @@ type DeviceService interface { Umount(source string, target string) error } -// Device Service Interface [END] - type LinuxDeviceService struct { RunnerFactory utils.RunnerFactory } diff --git a/internal/service/mock.go b/internal/service/mock.go new file mode 100644 index 0000000..88d48f8 --- /dev/null +++ b/internal/service/mock.go @@ -0,0 +1,86 @@ +package service + +import ( + "github.com/reecetech/ebs-bootstrap/internal/model" + "github.com/reecetech/ebs-bootstrap/internal/utils" +) + +type MockDeviceService struct { + StubGetSize func(name string) (uint64, error) + StubGetBlockDevices func() ([]string, error) + StubGetBlockDevice func(name string) (*model.BlockDevice, error) + StubMount func(source string, target string, fs model.FileSystem, options model.MountOptions) error + StubUmount func(source string, target string) error +} + +func NewMockDeviceService() *MockDeviceService { + return &MockDeviceService{ + StubGetSize: func(name string) (uint64, error) { + return 0, utils.NotImeplementedError("GetSize()") + }, + StubGetBlockDevices: func() ([]string, error) { + return nil, utils.NotImeplementedError("GetBlockDevices()") + }, + StubGetBlockDevice: func(name string) (*model.BlockDevice, error) { + return nil, utils.NotImeplementedError("GetBlockDevice()") + }, + StubMount: func(source, target string, fs model.FileSystem, options model.MountOptions) error { + return utils.NotImeplementedError("Mount()") + }, + StubUmount: func(source, target string) error { + return utils.NotImeplementedError("Umount()") + }, + } +} + +func (mds *MockDeviceService) GetSize(name string) (uint64, error) { + return mds.StubGetSize(name) +} + +func (mds *MockDeviceService) GetBlockDevices() ([]string, error) { + return mds.StubGetBlockDevices() +} + +func (mds *MockDeviceService) GetBlockDevice(name string) (*model.BlockDevice, error) { + return mds.StubGetBlockDevice(name) +} + +func (mds *MockDeviceService) Mount(source string, target string, fs model.FileSystem, options model.MountOptions) error { + return mds.StubMount(source, target, fs, options) +} + +func (mds *MockDeviceService) Umount(source string, target string) error { + return mds.StubUmount(source, target) +} + +type MockOwnerService struct { + StubGetCurrentUser func() (*model.User, error) + StubGetUser func(usr string) (*model.User, error) + StubGetGroup func(grp string) (*model.Group, error) +} + +func NewMockOwnerService() *MockOwnerService { + return &MockOwnerService{ + StubGetCurrentUser: func() (*model.User, error) { + return nil, utils.NotImeplementedError("GetCurrentUser()") + }, + StubGetUser: func(usr string) (*model.User, error) { + return nil, utils.NotImeplementedError("GetUser()") + }, + StubGetGroup: func(grp string) (*model.Group, error) { + return nil, utils.NotImeplementedError("GetGroup()") + }, + } +} + +func (mos *MockOwnerService) GetCurrentUser() (*model.User, error) { + return mos.StubGetCurrentUser() +} + +func (mos *MockOwnerService) GetUser(usr string) (*model.User, error) { + return mos.StubGetUser(usr) +} + +func (mos *MockOwnerService) GetGroup(grp string) (*model.Group, error) { + return mos.StubGetGroup(grp) +} diff --git a/internal/service/owner.go b/internal/service/owner.go index 706f170..387d6e1 100644 --- a/internal/service/owner.go +++ b/internal/service/owner.go @@ -10,8 +10,8 @@ import ( type OwnerService interface { GetCurrentUser() (*model.User, error) - GetUser(owner string) (*model.User, error) - GetGroup(owner string) (*model.Group, error) + GetUser(usr string) (*model.User, error) + GetGroup(grp string) (*model.Group, error) } type UnixOwnerService struct{} @@ -35,24 +35,24 @@ func (uos *UnixOwnerService) GetCurrentUser() (*model.User, error) { }, nil } -func (uos *UnixOwnerService) GetUser(us string) (*model.User, error) { +func (uos *UnixOwnerService) GetUser(usr string) (*model.User, error) { var u *user.User - if _, err := strconv.Atoi(us); err != nil { + if _, err := strconv.Atoi(usr); err != nil { // If not a valid integer, try to look up by username - u, err = user.Lookup(us) + u, err = user.Lookup(usr) if err != nil { - return nil, fmt.Errorf("🔴 User (name) %s does not exist", us) + return nil, fmt.Errorf("🔴 User (name=%s) does not exist", usr) } } else { - u, err = user.LookupId(us) + u, err = user.LookupId(usr) if err != nil { - return nil, fmt.Errorf("🔴 User (id) %s does not exist", us) + return nil, fmt.Errorf("🔴 User (id=%s) does not exist", usr) } } uid, err := strconv.Atoi(u.Uid) if err != nil { - return nil, fmt.Errorf("🔴 Failed to cast user (id) to integer") + return nil, fmt.Errorf("🔴 Failed to cast user (id=%s) to integer", u.Uid) } return &model.User{Name: u.Username, Uid: uid}, nil @@ -64,18 +64,18 @@ func (uos *UnixOwnerService) GetGroup(grp string) (*model.Group, error) { // If not a valid integer, try to look up by group name g, err = user.LookupGroup(grp) if err != nil { - return nil, fmt.Errorf("🔴 Group (name) %s does not exist", grp) + return nil, fmt.Errorf("🔴 Group (name=%s) does not exist", grp) } } else { g, err = user.LookupGroupId(grp) if err != nil { - return nil, fmt.Errorf("🔴 Group (id) %s does not exist", grp) + return nil, fmt.Errorf("🔴 Group (id=%s) does not exist", grp) } } gid, err := strconv.Atoi(g.Gid) if err != nil { - return nil, fmt.Errorf("🔴 Failed to cast group (id) to integer") + return nil, fmt.Errorf("🔴 Failed to cast group (id=%s) to integer", g.Gid) } return &model.Group{Name: g.Name, Gid: gid}, nil diff --git a/internal/utils/testing.go b/internal/utils/testing.go index aad179e..4cbb330 100644 --- a/internal/utils/testing.go +++ b/internal/utils/testing.go @@ -1,18 +1,35 @@ package utils import ( + "fmt" "testing" "github.com/google/go-cmp/cmp" "github.com/ryanuber/go-glob" ) -func CheckOutputCmp(id string, t *testing.T, expected interface{}, actual interface{}, opts ...cmp.Option) { +func NotImeplementedError(id string) error { + return fmt.Errorf("🔴 %s is not implemented", id) +} + +func CheckOutput(id string, t *testing.T, expected interface{}, actual interface{}, opts ...cmp.Option) { if !cmp.Equal(expected, actual, opts...) { t.Fatalf("%s [output] mismatch: Expected=%+v Actual=%+v", id, expected, actual) } } +func CheckError(id string, t *testing.T, pattern error, actual error) { + if actual != nil { + if pattern == nil { + t.Fatalf("%s [error] undetected: Actual=%v", id, actual) + return + } + if pattern.Error() != actual.Error() { + t.Fatalf("%s [error] mismatch: Expected=%v Actual=%v", id, pattern, actual) + } + } +} + func CheckErrorGlob(id string, t *testing.T, pattern error, actual error) { if actual != nil { if pattern == nil {