diff --git a/pkg/backend/user.go b/pkg/backend/user.go index bc651d2fe..1ef1dd405 100644 --- a/pkg/backend/user.go +++ b/pkg/backend/user.go @@ -26,6 +26,7 @@ func (d *Backend) User(ctx context.Context, username string) (proto.User, error) var m models.User var pks []ssh.PublicKey var hl models.Handle + var ems []proto.UserEmail if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error { var err error m, err = d.store.FindUserByUsername(ctx, tx, username) @@ -38,6 +39,15 @@ func (d *Backend) User(ctx context.Context, username string) (proto.User, error) return err } + emails, err := d.store.ListUserEmails(ctx, tx, m.ID) + if err != nil { + return err + } + + for _, e := range emails { + ems = append(ems, &userEmail{e}) + } + hl, err = d.store.GetHandleByUserID(ctx, tx, m.ID) return err }); err != nil { @@ -53,6 +63,7 @@ func (d *Backend) User(ctx context.Context, username string) (proto.User, error) user: m, publicKeys: pks, handle: hl, + emails: ems, }, nil } @@ -61,6 +72,7 @@ func (d *Backend) UserByID(ctx context.Context, id int64) (proto.User, error) { var m models.User var pks []ssh.PublicKey var hl models.Handle + var ems []proto.UserEmail if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error { var err error m, err = d.store.GetUserByID(ctx, tx, id) @@ -73,6 +85,15 @@ func (d *Backend) UserByID(ctx context.Context, id int64) (proto.User, error) { return err } + emails, err := d.store.ListUserEmails(ctx, tx, m.ID) + if err != nil { + return err + } + + for _, e := range emails { + ems = append(ems, &userEmail{e}) + } + hl, err = d.store.GetHandleByUserID(ctx, tx, m.ID) return err }); err != nil { @@ -88,6 +109,7 @@ func (d *Backend) UserByID(ctx context.Context, id int64) (proto.User, error) { user: m, publicKeys: pks, handle: hl, + emails: ems, }, nil } @@ -98,6 +120,7 @@ func (d *Backend) UserByPublicKey(ctx context.Context, pk ssh.PublicKey) (proto. var m models.User var pks []ssh.PublicKey var hl models.Handle + var ems []proto.UserEmail if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error { var err error m, err = d.store.FindUserByPublicKey(ctx, tx, pk) @@ -110,6 +133,15 @@ func (d *Backend) UserByPublicKey(ctx context.Context, pk ssh.PublicKey) (proto. return err } + emails, err := d.store.ListUserEmails(ctx, tx, m.ID) + if err != nil { + return err + } + + for _, e := range emails { + ems = append(ems, &userEmail{e}) + } + hl, err = d.store.GetHandleByUserID(ctx, tx, m.ID) return err }); err != nil { @@ -125,6 +157,7 @@ func (d *Backend) UserByPublicKey(ctx context.Context, pk ssh.PublicKey) (proto. user: m, publicKeys: pks, handle: hl, + emails: ems, }, nil } @@ -134,6 +167,7 @@ func (d *Backend) UserByAccessToken(ctx context.Context, token string) (proto.Us var m models.User var pks []ssh.PublicKey var hl models.Handle + var ems []proto.UserEmail token = HashToken(token) if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error { @@ -156,6 +190,15 @@ func (d *Backend) UserByAccessToken(ctx context.Context, token string) (proto.Us return err } + emails, err := d.store.ListUserEmails(ctx, tx, m.ID) + if err != nil { + return err + } + + for _, e := range emails { + ems = append(ems, &userEmail{e}) + } + hl, err = d.store.GetHandleByUserID(ctx, tx, m.ID) return err }); err != nil { @@ -171,6 +214,7 @@ func (d *Backend) UserByAccessToken(ctx context.Context, token string) (proto.Us user: m, publicKeys: pks, handle: hl, + emails: ems, }, nil } @@ -228,7 +272,7 @@ func (d *Backend) AddPublicKey(ctx context.Context, username string, pk ssh.Publ // It implements backend.Backend. func (d *Backend) CreateUser(ctx context.Context, username string, opts proto.UserOptions) (proto.User, error) { if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error { - return d.store.CreateUser(ctx, tx, username, opts.Admin, opts.PublicKeys) + return d.store.CreateUser(ctx, tx, username, opts.Admin, opts.PublicKeys, opts.Emails) }); err != nil { return nil, db.WrapError(err) } @@ -335,10 +379,60 @@ func (d *Backend) SetPassword(ctx context.Context, username string, rawPassword ) } +// AddUserEmail adds an email to a user. +func (d *Backend) AddUserEmail(ctx context.Context, user proto.User, email string) error { + return db.WrapError( + d.db.TransactionContext(ctx, func(tx *db.Tx) error { + return d.store.AddUserEmail(ctx, tx, user.ID(), email, false) + }), + ) +} + +// ListUserEmails lists the emails of a user. +func (d *Backend) ListUserEmails(ctx context.Context, user proto.User) ([]proto.UserEmail, error) { + var ems []proto.UserEmail + if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error { + emails, err := d.store.ListUserEmails(ctx, tx, user.ID()) + if err != nil { + return err + } + + for _, e := range emails { + ems = append(ems, &userEmail{e}) + } + + return nil + }); err != nil { + return nil, db.WrapError(err) + } + + return ems, nil +} + +// RemoveUserEmail deletes an email for a user. +// The deleted email must not be the primary email. +func (d *Backend) RemoveUserEmail(ctx context.Context, user proto.User, email string) error { + return db.WrapError( + d.db.TransactionContext(ctx, func(tx *db.Tx) error { + return d.store.RemoveUserEmail(ctx, tx, user.ID(), email) + }), + ) +} + +// SetUserPrimaryEmail sets the primary email of a user. +func (d *Backend) SetUserPrimaryEmail(ctx context.Context, user proto.User, email string) error { + return db.WrapError( + d.db.TransactionContext(ctx, func(tx *db.Tx) error { + return d.store.SetUserPrimaryEmail(ctx, tx, user.ID(), email) + }), + ) +} + type user struct { user models.User publicKeys []ssh.PublicKey handle models.Handle + emails []proto.UserEmail } var _ proto.User = (*user)(nil) @@ -371,3 +465,29 @@ func (u *user) Password() string { return "" } + +// Emails implements proto.User. +func (u *user) Emails() []proto.UserEmail { + return u.emails +} + +type userEmail struct { + email models.UserEmail +} + +var _ proto.UserEmail = (*userEmail)(nil) + +// Email implements proto.UserEmail. +func (e *userEmail) Email() string { + return e.email.Email +} + +// ID implements proto.UserEmail. +func (e *userEmail) ID() int64 { + return e.email.ID +} + +// IsPrimary implements proto.UserEmail. +func (e *userEmail) IsPrimary() bool { + return e.email.IsPrimary +} diff --git a/pkg/db/migrate/0004_create_orgs_teams_postgres.up.sql b/pkg/db/migrate/0004_create_orgs_teams_postgres.up.sql index 74a10ffef..ea52bd366 100644 --- a/pkg/db/migrate/0004_create_orgs_teams_postgres.up.sql +++ b/pkg/db/migrate/0004_create_orgs_teams_postgres.up.sql @@ -69,7 +69,7 @@ CREATE TABLE IF NOT EXISTS user_emails ( id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL, email TEXT NOT NULL UNIQUE, - is_primary BOOLEAN NOT NULL, + is_primary BOOLEAN NOT NULL DEFAULT false, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL, CONSTRAINT user_id_fk @@ -78,6 +78,9 @@ CREATE TABLE IF NOT EXISTS user_emails ( ON UPDATE CASCADE ); +-- Create unique index for primary email +CREATE UNIQUE INDEX user_emails_user_id_is_primary_idx ON user_emails (user_id) WHERE is_primary; + -- Add name to users table ALTER TABLE users ADD COLUMN name TEXT; @@ -112,7 +115,7 @@ ALTER TABLE repos ADD CONSTRAINT org_id_fk ALTER TABLE repos ALTER COLUMN user_id DROP NOT NULL; -- Check that both user_id and org_id can't be null -ALTER TABLE repos ADD CONSTRAINT user_id_org_id_not_null CHECK (user_id IS NULL <> org_id IS NULL); +ALTER TABLE repos ADD CONSTRAINT user_id_org_id_not_null CHECK ((user_id IS NULL) <> (org_id IS NULL)); -- Add team_id to collabs table ALTER TABLE collabs ADD COLUMN team_id INTEGER; @@ -125,7 +128,7 @@ ALTER TABLE collabs ADD CONSTRAINT team_id_fk ALTER TABLE collabs ALTER COLUMN user_id DROP NOT NULL; -- Check that both user_id and team_id can't be null -ALTER TABLE collabs ADD CONSTRAINT user_id_team_id_not_null CHECK (user_id IS NULL <> team_id IS NULL); +ALTER TABLE collabs ADD CONSTRAINT user_id_team_id_not_null CHECK ((user_id IS NULL) <> (team_id IS NULL)); -- Alter unique constraint on collabs table ALTER TABLE collabs DROP CONSTRAINT collabs_user_id_repo_id_key; diff --git a/pkg/db/migrate/0004_create_orgs_teams_sqlite.up.sql b/pkg/db/migrate/0004_create_orgs_teams_sqlite.up.sql index d9a41eb0d..17e4a88ff 100644 --- a/pkg/db/migrate/0004_create_orgs_teams_sqlite.up.sql +++ b/pkg/db/migrate/0004_create_orgs_teams_sqlite.up.sql @@ -71,7 +71,7 @@ CREATE TABLE IF NOT EXISTS user_emails ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, email TEXT NOT NULL UNIQUE, - is_primary BOOLEAN NOT NULL, + is_primary BOOLEAN NOT NULL DEFAULT false, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL, CONSTRAINT user_id_fk @@ -80,6 +80,9 @@ CREATE TABLE IF NOT EXISTS user_emails ( ON UPDATE CASCADE ); +-- Create unique index for primary email +CREATE UNIQUE INDEX user_emails_user_id_is_primary_idx ON user_emails (user_id) WHERE is_primary; + ALTER TABLE users RENAME TO _users_old; CREATE TABLE IF NOT EXISTS users ( diff --git a/pkg/proto/user.go b/pkg/proto/user.go index 7b334122d..c6c65b1bf 100644 --- a/pkg/proto/user.go +++ b/pkg/proto/user.go @@ -14,6 +14,8 @@ type User interface { PublicKeys() []ssh.PublicKey // Password returns the user's password hash. Password() string + // Emails returns the user's emails. + Emails() []UserEmail } // UserOptions are options for creating a user. @@ -22,4 +24,19 @@ type UserOptions struct { Admin bool // PublicKeys are the user's public keys. PublicKeys []ssh.PublicKey + // Emails are the user's emails. + // The first email in the slice will be set as the user's primary email. + Emails []string +} + +// UserEmail represents a user's email address. +type UserEmail interface { + // ID returns the email's ID. + ID() int64 + + // Email returns the email address. + Email() string + + // IsPrimary returns whether the email is the user's primary email. + IsPrimary() bool } diff --git a/pkg/ssh/cmd/org.go b/pkg/ssh/cmd/org.go index 251d92c3f..1d1a5fe15 100644 --- a/pkg/ssh/cmd/org.go +++ b/pkg/ssh/cmd/org.go @@ -33,7 +33,7 @@ func OrgCommand() *cobra.Command { Use: "list", Short: "List organizations", Args: cobra.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { ctx := cmd.Context() be := backend.FromContext(ctx) user := proto.UserFromContext(ctx) diff --git a/pkg/ssh/cmd/user.go b/pkg/ssh/cmd/user.go index 21981f9cb..9944aa243 100644 --- a/pkg/ssh/cmd/user.go +++ b/pkg/ssh/cmd/user.go @@ -22,9 +22,9 @@ func UserCommand() *cobra.Command { var admin bool var key string userCreateCommand := &cobra.Command{ - Use: "create USERNAME", + Use: "create USERNAME [EMAIL]", Short: "Create a new user", - Args: cobra.ExactArgs(1), + Args: cobra.MinimumNArgs(1), PersistentPreRunE: checkIfAdmin, RunE: func(cmd *cobra.Command, args []string) error { var pubkeys []ssh.PublicKey @@ -45,6 +45,10 @@ func UserCommand() *cobra.Command { PublicKeys: pubkeys, } + if len(args) > 1 { + opts.Emails = append(opts.Emails, args[1]) + } + _, err := be.CreateUser(ctx, username, opts) return err }, @@ -166,6 +170,14 @@ func UserCommand() *cobra.Command { cmd.Printf(" %s\n", sshutils.MarshalAuthorizedKey(pk)) } + emails := user.Emails() + if len(emails) > 0 { + cmd.Printf("Emails:\n") + for _, e := range emails { + cmd.Printf(" %s (primary: %v)\n", e.Email(), e.IsPrimary()) + } + } + return nil }, } @@ -185,6 +197,63 @@ func UserCommand() *cobra.Command { }, } + userAddEmailCommand := &cobra.Command{ + Use: "add-email USERNAME EMAIL", + Short: "Add an email to a user", + Args: cobra.ExactArgs(2), + PersistentPreRunE: checkIfAdmin, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + be := backend.FromContext(ctx) + username := args[0] + email := args[1] + u, err := be.User(ctx, username) + if err != nil { + return err + } + + return be.AddUserEmail(ctx, u, email) + }, + } + + userRemoveEmailCommand := &cobra.Command{ + Use: "remove-email USERNAME EMAIL", + Short: "Remove an email from a user", + Args: cobra.ExactArgs(2), + PersistentPreRunE: checkIfAdmin, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + be := backend.FromContext(ctx) + username := args[0] + email := args[1] + u, err := be.User(ctx, username) + if err != nil { + return err + } + + return be.RemoveUserEmail(ctx, u, email) + }, + } + + userSetPrimaryEmailCommand := &cobra.Command{ + Use: "set-primary-email USERNAME EMAIL", + Short: "Set a user's primary email", + Args: cobra.ExactArgs(2), + PersistentPreRunE: checkIfAdmin, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + be := backend.FromContext(ctx) + username := args[0] + email := args[1] + u, err := be.User(ctx, username) + if err != nil { + return err + } + + return be.SetUserPrimaryEmail(ctx, u, email) + }, + } + cmd.AddCommand( userCreateCommand, userAddPubkeyCommand, @@ -194,6 +263,9 @@ func UserCommand() *cobra.Command { userRemovePubkeyCommand, userSetAdminCommand, userSetUsernameCommand, + userAddEmailCommand, + userRemoveEmailCommand, + userSetPrimaryEmailCommand, ) return cmd diff --git a/pkg/store/database/org.go b/pkg/store/database/org.go index de6fd592a..60571dc9d 100644 --- a/pkg/store/database/org.go +++ b/pkg/store/database/org.go @@ -7,6 +7,7 @@ import ( "github.com/charmbracelet/soft-serve/pkg/db" "github.com/charmbracelet/soft-serve/pkg/db/models" "github.com/charmbracelet/soft-serve/pkg/store" + "github.com/charmbracelet/soft-serve/pkg/utils" ) var _ store.OrgStore = (*orgStore)(nil) @@ -15,6 +16,10 @@ type orgStore struct{ *handleStore } // UpdateOrgContactEmail implements store.OrgStore. func (*orgStore) UpdateOrgContactEmail(ctx context.Context, h db.Handler, org int64, email string) error { + if err := utils.ValidateEmail(email); err != nil { + return err + } + query := h.Rebind(` UPDATE organizations SET @@ -58,6 +63,10 @@ func (s *orgStore) DeleteOrgByID(ctx context.Context, h db.Handler, user, id int // Create implements store.OrgStore. func (s *orgStore) CreateOrg(ctx context.Context, h db.Handler, user int64, name, email string) (models.Organization, error) { + if err := utils.ValidateEmail(email); err != nil { + return models.Organization{}, err + } + handle, err := s.CreateHandle(ctx, h, name) if err != nil { return models.Organization{}, err diff --git a/pkg/store/database/user.go b/pkg/store/database/user.go index b815d546a..f3ed59f39 100644 --- a/pkg/store/database/user.go +++ b/pkg/store/database/user.go @@ -2,6 +2,7 @@ package database import ( "context" + "fmt" "strings" "github.com/charmbracelet/soft-serve/pkg/db" @@ -39,7 +40,7 @@ func (*userStore) AddPublicKeyByUsername(ctx context.Context, tx db.Handler, use } // CreateUser implements store.UserStore. -func (s *userStore) CreateUser(ctx context.Context, tx db.Handler, username string, isAdmin bool, pks []ssh.PublicKey) error { +func (s *userStore) CreateUser(ctx context.Context, tx db.Handler, username string, isAdmin bool, pks []ssh.PublicKey, emails []string) error { handleID, err := s.CreateHandle(ctx, tx, username) if err != nil { return err @@ -71,6 +72,12 @@ func (s *userStore) CreateUser(ctx context.Context, tx db.Handler, username stri } } + for i, e := range emails { + if err := s.AddUserEmail(ctx, tx, userID, e, i == 0); err != nil { + return err + } + } + return nil } @@ -255,6 +262,9 @@ func (*userStore) SetUserPasswordByUsername(ctx context.Context, tx db.Handler, // AddUserEmail implements store.UserStore. func (*userStore) AddUserEmail(ctx context.Context, tx db.Handler, userID int64, email string, isPrimary bool) error { + if err := utils.ValidateEmail(email); err != nil { + return err + } query := tx.Rebind(`INSERT INTO user_emails (user_id, email, is_primary, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP);`) _, err := tx.ExecContext(ctx, query, userID, email, isPrimary) @@ -269,16 +279,40 @@ func (*userStore) ListUserEmails(ctx context.Context, tx db.Handler, userID int6 return ms, err } -// UpdateUserEmail implements store.UserStore. -func (*userStore) UpdateUserEmail(ctx context.Context, tx db.Handler, userID int64, oldEmail string, newEmail string, isPrimary bool) error { - query := tx.Rebind(`UPDATE user_emails SET email = ?, is_primary = ?, updated_at = CURRENT_TIMESTAMP WHERE user_id = ? AND email = ?;`) - _, err := tx.ExecContext(ctx, query, newEmail, isPrimary, userID, oldEmail) - return err +// RemoveUserEmail implements store.UserStore. +func (*userStore) RemoveUserEmail(ctx context.Context, tx db.Handler, userID int64, email string) error { + var e models.UserEmail + query := tx.Rebind(`DELETE FROM user_emails WHERE user_id = ? AND email = ? RETURNING *;`) + if err := tx.GetContext(ctx, &e, query, userID, email); err != nil { + return err + } + + if e.IsPrimary { + return fmt.Errorf("cannot remove primary email") + } else if e.ID == 0 { + return db.ErrRecordNotFound + } + + return nil } -// DeleteUserEmail implements store.UserStore. -func (*userStore) DeleteUserEmail(ctx context.Context, tx db.Handler, userID int64, email string) error { - query := tx.Rebind(`DELETE FROM user_emails WHERE user_id = ? AND email = ?;`) - _, err := tx.ExecContext(ctx, query, userID, email) - return err +// SetUserPrimaryEmail implements store.UserStore. +func (*userStore) SetUserPrimaryEmail(ctx context.Context, tx db.Handler, userID int64, email string) error { + query := tx.Rebind(`UPDATE user_emails SET is_primary = FALSE WHERE user_id = ?;`) + _, err := tx.ExecContext(ctx, query, userID) + if err != nil { + return err + } + + var emailID int64 + query = tx.Rebind(`UPDATE user_emails SET is_primary = TRUE WHERE user_id = ? AND email = ? RETURNING id;`) + if err := tx.GetContext(ctx, &emailID, query, userID, email); err != nil { + return err + } + + if emailID == 0 { + return db.ErrRecordNotFound + } + + return nil } diff --git a/pkg/store/user.go b/pkg/store/user.go index ac8df8ef6..6e6f1c2a5 100644 --- a/pkg/store/user.go +++ b/pkg/store/user.go @@ -15,7 +15,7 @@ type UserStore interface { FindUserByPublicKey(ctx context.Context, h db.Handler, pk ssh.PublicKey) (models.User, error) FindUserByAccessToken(ctx context.Context, h db.Handler, token string) (models.User, error) GetAllUsers(ctx context.Context, h db.Handler) ([]models.User, error) - CreateUser(ctx context.Context, h db.Handler, username string, isAdmin bool, pks []ssh.PublicKey) error + CreateUser(ctx context.Context, h db.Handler, username string, isAdmin bool, pks []ssh.PublicKey, emails []string) error DeleteUserByUsername(ctx context.Context, h db.Handler, username string) error SetUsernameByUsername(ctx context.Context, h db.Handler, username string, newUsername string) error SetAdminByUsername(ctx context.Context, h db.Handler, username string, isAdmin bool) error @@ -28,6 +28,6 @@ type UserStore interface { AddUserEmail(ctx context.Context, h db.Handler, userID int64, email string, isPrimary bool) error ListUserEmails(ctx context.Context, h db.Handler, userID int64) ([]models.UserEmail, error) - UpdateUserEmail(ctx context.Context, h db.Handler, userID int64, oldEmail string, newEmail string, isPrimary bool) error - DeleteUserEmail(ctx context.Context, h db.Handler, userID int64, email string) error + RemoveUserEmail(ctx context.Context, h db.Handler, userID int64, email string) error + SetUserPrimaryEmail(ctx context.Context, h db.Handler, userID int64, email string) error } diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 5fb46f8c4..0b6660e88 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -1,12 +1,20 @@ package utils import ( + "errors" "fmt" + "net/mail" "path" "strings" "unicode" ) +var ( + + // ErrInvalidEmail indicates that an email address is invalid. + ErrInvalidEmail = errors.New("invalid email address") +) + // SanitizeRepo returns a sanitized version of the given repository name. func SanitizeRepo(repo string) string { repo = strings.TrimPrefix(repo, "/") @@ -50,3 +58,17 @@ func ValidateRepo(repo string) error { return nil } + +// ValidateEmail returns an error if the given email address is invalid. +func ValidateEmail(email string) error { + if strings.ContainsAny(email, " <>") { + return ErrInvalidEmail + } + + _, err := mail.ParseAddress(email) + if err != nil { + return fmt.Errorf("%w: %s", ErrInvalidEmail, err) + } + + return nil +} diff --git a/testscript/script_test.go b/testscript/script_test.go index 32ebe77c1..cec209d7f 100644 --- a/testscript/script_test.go +++ b/testscript/script_test.go @@ -76,6 +76,7 @@ func TestScript(t *testing.T) { key, admin1 := mkkey("admin1") _, admin2 := mkkey("admin2") _, user1 := mkkey("user1") + _, user2 := mkkey("user2") testscript.Run(t, testscript.Params{ Dir: "./testdata/", @@ -117,6 +118,7 @@ func TestScript(t *testing.T) { e.Setenv("ADMIN1_AUTHORIZED_KEY", admin1.AuthorizedKey()) e.Setenv("ADMIN2_AUTHORIZED_KEY", admin2.AuthorizedKey()) e.Setenv("USER1_AUTHORIZED_KEY", user1.AuthorizedKey()) + e.Setenv("USER2_AUTHORIZED_KEY", user2.AuthorizedKey()) e.Setenv("SSH_KNOWN_HOSTS_FILE", filepath.Join(t.TempDir(), "known_hosts")) e.Setenv("SSH_KNOWN_CONFIG_FILE", filepath.Join(t.TempDir(), "config")) diff --git a/testscript/testdata/user_management.txtar b/testscript/testdata/user_management.txtar index f65397090..15f1863c9 100644 --- a/testscript/testdata/user_management.txtar +++ b/testscript/testdata/user_management.txtar @@ -1,7 +1,7 @@ # vi: set ft=conf # convert crlf to lf on windows -[windows] dos2unix info.txt admin_key_list1.txt admin_key_list2.txt list1.txt list2.txt foo_info1.txt foo_info2.txt foo_info3.txt foo_info4.txt foo_info5.txt +[windows] dos2unix info.txt admin_key_list1.txt admin_key_list2.txt list1.txt list2.txt foo_info1.txt foo_info2.txt foo_info3.txt foo_info4.txt foo_info5.txt bar_info.txt # start soft serve exec soft serve & @@ -68,6 +68,38 @@ soft user delete foo2 soft user list cmpenv stdout list1.txt +# create a new user with an invalid email +! soft user create bar --key "$USER2_AUTHORIZED_KEY" "foobar" +stderr 'invalid email address.*' + +# create a new user with a valid email +soft user create bar --key "$USER2_AUTHORIZED_KEY" "foo@bar.baz" +! stdout . +# add email to existing user +soft user add-email bar "foobar@fubar.baz" +! stdout . +# add existing email +! soft user add-email bar "foobar@fubar.baz" +stderr 'duplicate key.*' + +# get new user info +soft user info bar +cmpenv stdout bar_info.txt + +# remove primary email from user +! soft user remove-email bar "foo@bar.baz" +stderr 'cannot remove primary email.*' + +# set primary email that doesn't exist +! soft user set-primary-email bar "foobar@foofoo.foo" +stderr 'no rows in result set.*' +# set primary email +soft user set-primary-email bar "foobar@fubar.baz" +! stdout . +# remove other email +soft user remove-email bar "foo@bar.baz" +! stdout . + # stop the server [windows] stopserver [windows] ! stderr . @@ -112,3 +144,11 @@ $ADMIN1_AUTHORIZED_KEY $ADMIN2_AUTHORIZED_KEY -- admin_key_list2.txt -- $ADMIN1_AUTHORIZED_KEY +-- bar_info.txt -- +Username: bar +Admin: false +Public keys: + $USER2_AUTHORIZED_KEY +Emails: + foo@bar.baz (primary: true) + foobar@fubar.baz (primary: false)