Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions cli/exp_update_user_email.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package cli

import (
"fmt"

"golang.org/x/xerrors"

"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)

func (r *RootCmd) updateUserEmail() *serpent.Command {
var (
oldEmail string
newEmail string
)

cmd := &serpent.Command{
Use: "update-user-email",
Short: "Update a user's email address (break-glass; experimental)",
Hidden: true,
Options: serpent.OptionSet{
{
Flag: "old-email",
Description: "Current email address of the user to update.",
Required: true,
Value: serpent.StringOf(&oldEmail),
},
{
Flag: "new-email",
Description: "New email address to assign to the user.",
Required: true,
Value: serpent.StringOf(&newEmail),
},
cliui.SkipPromptOption(),
},
Handler: func(inv *serpent.Invocation) error {
if oldEmail == "" {
return xerrors.Errorf("--old-email must not be blank")
}
if newEmail == "" {
return xerrors.Errorf("--new-email must not be blank")
}

client, err := r.InitClient(inv)
if err != nil {
return err
}

_, _ = fmt.Fprintf(inv.Stdout,
"This will update the email address for the account currently using %q to %q.\n"+
"All Coder sessions and API tokens for that user will be revoked.\n"+
"If the user logs in with an external identity provider, it may overwrite the email when the user next signs in.\n",
oldEmail, newEmail,
)

_, err = cliui.Prompt(inv, cliui.PromptOptions{
Text: "Confirm email update?",
IsConfirm: true,
Default: cliui.ConfirmNo,
})
if err != nil {
return err
}

err = client.UpdateUserEmail(inv.Context(), codersdk.UpdateUserEmailRequest{
OldEmail: oldEmail,
NewEmail: newEmail,
})
if err != nil {
return xerrors.Errorf("update user email: %w", err)
}

_, _ = fmt.Fprintf(inv.Stdout, "Updated user email from %s to %s.\n", oldEmail, newEmail)
return nil
},
}

return cmd
}
191 changes: 191 additions & 0 deletions cli/exp_update_user_email_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
package cli_test

import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/testutil"
"github.com/coder/serpent"
)

func TestUpdateUserEmail(t *testing.T) {
Comment thread
johnstcn marked this conversation as resolved.
t.Parallel()

t.Run("CommandReachable", func(t *testing.T) {
t.Parallel()

root := getRoot(t)
var found *serpent.Command
root.Walk(func(cmd *serpent.Command) {
if cmd.Name() == "update-user-email" {
found = cmd
}
})
require.NotNil(t, found, "update-user-email command not found under exp")
require.True(t, found.Hidden, "command should be hidden")
})

t.Run("MissingOldEmail", func(t *testing.T) {
t.Parallel()

inv, _ := clitest.New(t, "exp", "update-user-email", "--new-email", "[email protected]")
err := inv.Run()
require.Error(t, err)
require.Contains(t, err.Error(), "old-email")
})

t.Run("MissingNewEmail", func(t *testing.T) {
t.Parallel()

inv, _ := clitest.New(t, "exp", "update-user-email", "--old-email", "[email protected]")
err := inv.Run()
require.Error(t, err)
require.Contains(t, err.Error(), "new-email")
})

t.Run("DeclinePrompt", func(t *testing.T) {
t.Parallel()

// Use a channel closed by the handler to detect whether the API is called.
apiCalled := make(chan struct{})
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
close(apiCalled)
w.WriteHeader(http.StatusNoContent)
}))
t.Cleanup(srv.Close)

client := codersdk.New(must(url.Parse(srv.URL)))
inv, root := clitest.New(t, "exp", "update-user-email",
"--old-email", "[email protected]",
"--new-email", "[email protected]",
)
clitest.SetupConfig(t, client, root)
inv.Stdin = strings.NewReader("no\n")

ctx := testutil.Context(t, testutil.WaitShort)
done := make(chan struct{})
var runErr error
go func() {
defer close(done)
runErr = inv.Run()
}()

testutil.TryReceive(ctx, t, done)
require.ErrorIs(t, runErr, cliui.ErrCanceled)

// Verify the API was not called after the command returned.
select {
case <-apiCalled:
t.Fatal("API should not be called when prompt is declined")
default:
}
})

// AcceptPrompt runs a full end-to-end test against a real Coder server: it
// creates a second user, invokes the CLI as an admin, confirms the prompt,
// and asserts both the command output and the actual stored email.
t.Run("AcceptPrompt", func(t *testing.T) {
t.Parallel()

client := coderdtest.New(t, nil)
owner := coderdtest.CreateFirstUser(t, client)
_, member := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
ctx := testutil.Context(t, testutil.WaitShort)

oldEmail := member.Email
newEmail := "updated-" + oldEmail

inv, root := clitest.New(t, "exp", "update-user-email",
"--old-email", oldEmail,
"--new-email", newEmail,
)
//nolint:gocritic // This break-glass command is restricted to deployment owners.
clitest.SetupConfig(t, client, root)
inv.Stdin = strings.NewReader("yes\n")

var outBuf bytes.Buffer
inv.Stdout = &outBuf

require.NoError(t, inv.Run())

out := outBuf.String()
require.Contains(t, out, oldEmail)
require.Contains(t, out, newEmail)
require.Contains(t, out, "sessions and API tokens")
require.Contains(t, out, "external identity provider")
require.Contains(t, out, "Updated user email from "+oldEmail+" to "+newEmail+".")

// Verify the email was actually persisted.
updated, err := client.User(ctx, member.ID.String())
require.NoError(t, err)
require.Equal(t, newEmail, updated.Email)
})

t.Run("YesSkipsPrompt", func(t *testing.T) {
t.Parallel()

var gotBody codersdk.UpdateUserEmailRequest
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err := json.NewDecoder(r.Body).Decode(&gotBody)
assert.NoError(t, err)
w.WriteHeader(http.StatusNoContent)
}))
t.Cleanup(srv.Close)

client := codersdk.New(must(url.Parse(srv.URL)))
inv, root := clitest.New(t, "exp", "update-user-email",
"--old-email", "[email protected]",
"--new-email", "[email protected]",
"--yes",
)
clitest.SetupConfig(t, client, root)

var outBuf bytes.Buffer
inv.Stdout = &outBuf

require.NoError(t, inv.Run())
require.Contains(t, outBuf.String(), "Updated user email from [email protected] to [email protected].")
require.Equal(t, "[email protected]", gotBody.OldEmail)
require.Equal(t, "[email protected]", gotBody.NewEmail)
})

t.Run("APIError", func(t *testing.T) {
t.Parallel()

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(`{"message":"internal server error"}`))
}))
t.Cleanup(srv.Close)

client := codersdk.New(must(url.Parse(srv.URL)))
inv, root := clitest.New(t, "exp", "update-user-email",
"--old-email", "[email protected]",
"--new-email", "[email protected]",
"--yes",
)
clitest.SetupConfig(t, client, root)

err := inv.Run()
require.Error(t, err)
require.Contains(t, err.Error(), "update user email")
Comment thread
johnstcn marked this conversation as resolved.

var sdkErr *codersdk.Error
require.ErrorAs(t, err, &sdkErr)
require.Equal(t, http.StatusInternalServerError, sdkErr.StatusCode())
require.Contains(t, sdkErr.Message, "internal server error")
})
}
1 change: 1 addition & 0 deletions cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ func (r *RootCmd) AGPLExperimental() []*serpent.Command {
r.promptExample(),
r.rptyCommand(),
r.syncCommand(),
r.updateUserEmail(),
}
}

Expand Down
53 changes: 53 additions & 0 deletions coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading