From 29c2fc54720aa8cb76a54d1c5ebf063795a422a1 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Thu, 3 Feb 2022 16:10:15 -0500 Subject: [PATCH] CLI tests for 'ntfy user' --- cmd/user.go | 60 +++++++++++++++++++-------- cmd/user_test.go | 103 +++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 8 ++-- go.sum | 9 +++++ 4 files changed, 160 insertions(+), 20 deletions(-) create mode 100644 cmd/user_test.go diff --git a/cmd/user.go b/cmd/user.go index 044ad70e..ae74e0f1 100644 --- a/cmd/user.go +++ b/cmd/user.go @@ -23,8 +23,9 @@ var cmdUser = &cli.Command{ { Name: "add", Aliases: []string{"a"}, - Usage: "add user", + Usage: "Adds a new user", UsageText: "ntfy user add [--role=admin|user] USERNAME", + Before: inheritRootReaderFunc, Action: execUserAdd, Flags: []cli.Flag{ &cli.StringFlag{Name: "role", Aliases: []string{"r"}, Value: string(auth.RoleUser), Usage: "user role"}, @@ -43,8 +44,9 @@ Examples: { Name: "remove", Aliases: []string{"del", "rm"}, - Usage: "remove user", + Usage: "Removes a user", UsageText: "ntfy user remove USERNAME", + Before: inheritRootReaderFunc, Action: execUserDel, Description: `Remove a user from the ntfy user database. @@ -55,8 +57,9 @@ Example: { Name: "change-pass", Aliases: []string{"chp"}, - Usage: "change user password", + Usage: "Changes a user's password", UsageText: "ntfy user change-pass USERNAME", + Before: inheritRootReaderFunc, Action: execUserChangePass, Description: `Change the password for the given user. @@ -70,8 +73,9 @@ Example: { Name: "change-role", Aliases: []string{"chr"}, - Usage: "change user role", + Usage: "Changes the role of a user", UsageText: "ntfy user change-role USERNAME ROLE", + Before: inheritRootReaderFunc, Action: execUserChangeRole, Description: `Change the role for the given user to admin or user. @@ -92,7 +96,8 @@ Example: { Name: "list", Aliases: []string{"l"}, - Usage: "list users", + Usage: "Shows a list of users", + Before: inheritRootReaderFunc, Action: execUserList, }, }, @@ -125,18 +130,21 @@ func execUserAdd(c *cli.Context) error { } else if !auth.AllowedRole(role) { return errors.New("role must be either 'user' or 'admin'") } - password, err := readPassword(c) - if err != nil { - return err - } manager, err := createAuthManager(c) if err != nil { return err } - if err := manager.AddUser(username, password, auth.Role(role)); err != nil { + if user, _ := manager.User(username); user != nil { + return fmt.Errorf("user %s already exists", username) + } + password, err := readPassword(c) + if err != nil { return err } - fmt.Fprintf(c.App.ErrWriter, "User %s added with role %s\n", username, role) + if err := manager.AddUser(username, password, role); err != nil { + return err + } + fmt.Fprintf(c.App.ErrWriter, "user %s added with role %s\n", username, role) return nil } @@ -151,10 +159,13 @@ func execUserDel(c *cli.Context) error { if err != nil { return err } + if _, err := manager.User(username); err == auth.ErrNotFound { + return fmt.Errorf("user %s does not exist", username) + } if err := manager.RemoveUser(username); err != nil { return err } - fmt.Fprintf(c.App.ErrWriter, "User %s removed\n", username) + fmt.Fprintf(c.App.ErrWriter, "user %s removed\n", username) return nil } @@ -165,18 +176,21 @@ func execUserChangePass(c *cli.Context) error { } else if username == userEveryone { return errors.New("username not allowed") } - password, err := readPassword(c) + manager, err := createAuthManager(c) if err != nil { return err } - manager, err := createAuthManager(c) + if _, err := manager.User(username); err == auth.ErrNotFound { + return fmt.Errorf("user %s does not exist", username) + } + password, err := readPassword(c) if err != nil { return err } if err := manager.ChangePassword(username, password); err != nil { return err } - fmt.Fprintf(c.App.ErrWriter, "Changed password for user %s\n", username) + fmt.Fprintf(c.App.ErrWriter, "changed password for user %s\n", username) return nil } @@ -192,10 +206,13 @@ func execUserChangeRole(c *cli.Context) error { if err != nil { return err } + if _, err := manager.User(username); err == auth.ErrNotFound { + return fmt.Errorf("user %s does not exist", username) + } if err := manager.ChangeRole(username, role); err != nil { return err } - fmt.Fprintf(c.App.ErrWriter, "Changed role for user %s to %s\n", username, role) + fmt.Fprintf(c.App.ErrWriter, "changed role for user %s to %s\n", username, role) return nil } @@ -251,3 +268,14 @@ func userCommandFlags() []cli.Flag { altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-default-access", Aliases: []string{"p"}, EnvVars: []string{"NTFY_AUTH_DEFAULT_ACCESS"}, Value: "read-write", Usage: "default permissions if no matching entries in the auth database are found"}), } } + +// inheritRootReaderFunc is a workaround for a urfave/cli bug that makes subcommands not inherit the App.Reader. +// This bug was fixed in master, but not in v2.3.0. +func inheritRootReaderFunc(ctx *cli.Context) error { + for _, c := range ctx.Lineage() { + if c.App != nil && c.App.Reader != nil { + ctx.App.Reader = c.App.Reader + } + } + return nil +} diff --git a/cmd/user_test.go b/cmd/user_test.go new file mode 100644 index 00000000..3d8a5d77 --- /dev/null +++ b/cmd/user_test.go @@ -0,0 +1,103 @@ +package cmd + +import ( + "github.com/stretchr/testify/require" + "github.com/urfave/cli/v2" + "heckel.io/ntfy/server" + "heckel.io/ntfy/test" + "path/filepath" + "testing" +) + +func TestCLI_User_Add(t *testing.T) { + s, conf, port := newTestServerWithAuth(t) + defer test.StopServer(t, s, port) + + app, stdin, _, stderr := newTestApp() + stdin.WriteString("mypass\nmypass") + require.Nil(t, runUserCommand(app, conf, "add", "phil")) + require.Contains(t, stderr.String(), "user phil added with role user") +} + +func TestCLI_User_Add_Exists(t *testing.T) { + s, conf, port := newTestServerWithAuth(t) + defer test.StopServer(t, s, port) + + app, stdin, _, stderr := newTestApp() + stdin.WriteString("mypass\nmypass") + require.Nil(t, runUserCommand(app, conf, "add", "phil")) + require.Contains(t, stderr.String(), "user phil added with role user") + + app, stdin, _, stderr = newTestApp() + stdin.WriteString("mypass\nmypass") + err := runUserCommand(app, conf, "add", "phil") + require.Error(t, err) + require.Contains(t, err.Error(), "user phil already exists") +} + +func TestCLI_User_Add_Admin(t *testing.T) { + s, conf, port := newTestServerWithAuth(t) + defer test.StopServer(t, s, port) + + app, stdin, _, stderr := newTestApp() + stdin.WriteString("mypass\nmypass") + require.Nil(t, runUserCommand(app, conf, "add", "--role=admin", "phil")) + require.Contains(t, stderr.String(), "user phil added with role admin") +} + +func TestCLI_User_Add_Password_Mismatch(t *testing.T) { + s, conf, port := newTestServerWithAuth(t) + defer test.StopServer(t, s, port) + + app, stdin, _, _ := newTestApp() + stdin.WriteString("mypass\nNOTMATCH") + err := runUserCommand(app, conf, "add", "phil") + require.Error(t, err) + require.Contains(t, err.Error(), "passwords do not match: try it again, but this time type slooowwwlly") +} + +func TestCLI_User_ChangePass(t *testing.T) { + s, conf, port := newTestServerWithAuth(t) + defer test.StopServer(t, s, port) + + // Add user + app, stdin, _, stderr := newTestApp() + stdin.WriteString("mypass\nmypass") + require.Nil(t, runUserCommand(app, conf, "add", "phil")) + require.Contains(t, stderr.String(), "user phil added with role user") + + // Change pass + app, stdin, _, stderr = newTestApp() + stdin.WriteString("newpass\nnewpass") + require.Nil(t, runUserCommand(app, conf, "change-pass", "phil")) + require.Contains(t, stderr.String(), "changed password for user phil") +} + +func newTestServerWithAuth(t *testing.T) (s *server.Server, conf *server.Config, port int) { + conf = server.NewConfig() + conf.AuthFile = filepath.Join(t.TempDir(), "user.db") + conf.AuthDefaultRead = false + conf.AuthDefaultWrite = false + s, port = test.StartServerWithConfig(t, conf) + return +} + +func runUserCommand(app *cli.App, conf *server.Config, args ...string) error { + var defaultAccess string + if conf.AuthDefaultRead && conf.AuthDefaultWrite { + defaultAccess = "read-write" + } else if conf.AuthDefaultRead && !conf.AuthDefaultWrite { + defaultAccess = "read-only" + } else if !conf.AuthDefaultRead && conf.AuthDefaultWrite { + defaultAccess = "write-only" + } else if !conf.AuthDefaultRead && !conf.AuthDefaultWrite { + defaultAccess = "deny-all" + } + userArgs := []string{ + "ntfy", + "user", + "--auth-file=" + conf.AuthFile, + "--auth-default-access=" + defaultAccess, + } + return app.Run(append(userArgs, args...)) +} diff --git a/go.mod b/go.mod index e1fa5988..b8054cc7 100644 --- a/go.mod +++ b/go.mod @@ -20,13 +20,13 @@ require ( golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 - google.golang.org/api v0.66.0 + google.golang.org/api v0.67.0 gopkg.in/yaml.v2 v2.4.0 ) require ( cloud.google.com/go v0.100.2 // indirect - cloud.google.com/go/compute v1.1.0 // indirect + cloud.google.com/go/compute v1.2.0 // indirect cloud.google.com/go/iam v0.1.1 // indirect github.com/AlekSi/pointer v1.0.0 // indirect github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect @@ -46,11 +46,11 @@ require ( github.com/russross/blackfriday/v2 v2.1.0 // indirect go.opencensus.io v0.23.0 // indirect golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d // indirect - golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect + golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27 // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350 // indirect + google.golang.org/genproto v0.0.0-20220203182621-f4ae394cde3f // indirect google.golang.org/grpc v1.44.0 // indirect google.golang.org/protobuf v1.27.1 // indirect gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect diff --git a/go.sum b/go.sum index b9d09976..cdc65cd8 100644 --- a/go.sum +++ b/go.sum @@ -39,6 +39,8 @@ cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM7 cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= cloud.google.com/go/compute v1.1.0 h1:pyPhehLfZ6pVzRgJmXGYvCY4K7WSWRhVw0AwhgVvS84= cloud.google.com/go/compute v1.1.0/go.mod h1:2NIffxgWfORSI7EOYMFatGTfjMLnqrOKBEyYb6NoRgA= +cloud.google.com/go/compute v1.2.0 h1:EKki8sSdvDU0OO9mAXGwPXOTOgPz2l08R0/IutDH11I= +cloud.google.com/go/compute v1.2.0/go.mod h1:xlogom/6gr8RJGBe7nT2eGsQYAFUbbv8dbC29qE3Xmw= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.6.1 h1:8rBq3zRjnHx8UtBvaOWqBB1xq9jH6/wltfQLlTMh2Fw= @@ -440,6 +442,8 @@ golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27 h1:XDXtA5hveEEV8JB2l7nhMTp3t3cHp9ZpwcdjqyEWLlo= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= @@ -553,6 +557,8 @@ google.golang.org/api v0.64.0/go.mod h1:931CdxA8Rm4t6zqTFGSsgwbAEZ2+GMYurbndwSim google.golang.org/api v0.65.0/go.mod h1:ArYhxgGadlWmqO1IqVujw6Cs8IdD33bTmzKo2Sh+cbg= google.golang.org/api v0.66.0 h1:CbGy4LEiXCVCiNEDFgGpWOVwsDT7E2Qej1ZvN1P7KPg= google.golang.org/api v0.66.0/go.mod h1:I1dmXYpX7HGwz/ejRxwQp2qj5bFAz93HiCU1C1oYd9M= +google.golang.org/api v0.67.0 h1:lYaaLa+x3VVUhtosaK9xihwQ9H9KRa557REHwwZ2orM= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -633,6 +639,9 @@ google.golang.org/genproto v0.0.0-20220114231437-d2e6a121cae0/go.mod h1:5CzLGKJ6 google.golang.org/genproto v0.0.0-20220118154757-00ab72f36ad5/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350 h1:YxHp5zqIcAShDEvRr5/0rVESVS+njYF68PSdazrNLJo= google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220201184016-50beb8ab5c44/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220203182621-f4ae394cde3f h1:w9Sx4FBkwsN0jMZz8E42tMdmhZ5b2Z/vFx2LKAxxI9o= +google.golang.org/genproto v0.0.0-20220203182621-f4ae394cde3f/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=