Files
coder/cli/login_test.go
T
Spike Curtis bddb808b25 chore: arrange imports in a standard way (#21452)
Fixes all our Go file imports to match the preferred spec that we've _mostly_ been using. For example:

```
import (
	"context"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"golang.org/x/xerrors"
	"gopkg.in/natefinch/lumberjack.v2"

	"cdr.dev/slog/v3"
	"github.com/coder/coder/v2/codersdk/agentsdk"
	"github.com/coder/serpent"
)
```

3 groups: standard library, 3rd partly libs, Coder libs.

This PR makes the change across the codebase. The PR in the stack above modifies our formatting to maintain this state of affairs, and is a separate PR so it's possible to review that one in detail.
2026-01-08 15:24:11 +04:00

540 lines
19 KiB
Go

package cli_test
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"runtime"
"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/pty/ptytest"
"github.com/coder/coder/v2/testutil"
"github.com/coder/pretty"
)
func TestLogin(t *testing.T) {
t.Parallel()
t.Run("InitialUserNoTTY", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
root, _ := clitest.New(t, "login", client.URL.String())
err := root.Run()
require.Error(t, err)
})
t.Run("InitialUserBadLoginURL", func(t *testing.T) {
t.Parallel()
badLoginURL := "https://fcca2077f06e68aaf9"
root, _ := clitest.New(t, "login", badLoginURL)
err := root.Run()
errMsg := fmt.Sprintf("Failed to check server %q for first user, is the URL correct and is coder accessible from your browser?", badLoginURL)
require.ErrorContains(t, err, errMsg)
})
t.Run("InitialUserNonCoderURLFail", func(t *testing.T) {
t.Parallel()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("Not Found"))
}))
defer ts.Close()
badLoginURL := ts.URL
root, _ := clitest.New(t, "login", badLoginURL)
err := root.Run()
errMsg := fmt.Sprintf("Failed to check server %q for first user, is the URL correct and is coder accessible from your browser?", badLoginURL)
require.ErrorContains(t, err, errMsg)
})
t.Run("InitialUserNonCoderURLSuccess", func(t *testing.T) {
t.Parallel()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set(codersdk.BuildVersionHeader, "something")
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("Not Found"))
}))
defer ts.Close()
badLoginURL := ts.URL
root, _ := clitest.New(t, "login", badLoginURL)
err := root.Run()
// this means we passed the check for a valid coder server
require.ErrorContains(t, err, "the initial user cannot be created in non-interactive mode")
})
t.Run("InitialUserTTY", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
// The --force-tty flag is required on Windows, because the `isatty` library does not
// accurately detect Windows ptys when they are not attached to a process:
// https://github.com/mattn/go-isatty/issues/59
doneChan := make(chan struct{})
root, _ := clitest.New(t, "login", "--force-tty", client.URL.String())
pty := ptytest.New(t).Attach(root)
go func() {
defer close(doneChan)
err := root.Run()
assert.NoError(t, err)
}()
matches := []string{
"first user?", "yes",
"username", coderdtest.FirstUserParams.Username,
"name", coderdtest.FirstUserParams.Name,
"email", coderdtest.FirstUserParams.Email,
"password", coderdtest.FirstUserParams.Password,
"password", coderdtest.FirstUserParams.Password, // confirm
"trial", "yes",
"firstName", coderdtest.TrialUserParams.FirstName,
"lastName", coderdtest.TrialUserParams.LastName,
"phoneNumber", coderdtest.TrialUserParams.PhoneNumber,
"jobTitle", coderdtest.TrialUserParams.JobTitle,
"companyName", coderdtest.TrialUserParams.CompanyName,
// `developers` and `country` `cliui.Select` automatically selects the first option during tests.
}
for i := 0; i < len(matches); i += 2 {
match := matches[i]
value := matches[i+1]
pty.ExpectMatch(match)
pty.WriteLine(value)
}
pty.ExpectMatch("Welcome to Coder")
<-doneChan
ctx := testutil.Context(t, testutil.WaitShort)
resp, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
Email: coderdtest.FirstUserParams.Email,
Password: coderdtest.FirstUserParams.Password,
})
require.NoError(t, err)
client.SetSessionToken(resp.SessionToken)
me, err := client.User(ctx, codersdk.Me)
require.NoError(t, err)
assert.Equal(t, coderdtest.FirstUserParams.Username, me.Username)
assert.Equal(t, coderdtest.FirstUserParams.Name, me.Name)
assert.Equal(t, coderdtest.FirstUserParams.Email, me.Email)
})
t.Run("InitialUserTTYWithNoTrial", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
// The --force-tty flag is required on Windows, because the `isatty` library does not
// accurately detect Windows ptys when they are not attached to a process:
// https://github.com/mattn/go-isatty/issues/59
doneChan := make(chan struct{})
root, _ := clitest.New(t, "login", "--force-tty", client.URL.String())
pty := ptytest.New(t).Attach(root)
go func() {
defer close(doneChan)
err := root.Run()
assert.NoError(t, err)
}()
matches := []string{
"first user?", "yes",
"username", coderdtest.FirstUserParams.Username,
"name", coderdtest.FirstUserParams.Name,
"email", coderdtest.FirstUserParams.Email,
"password", coderdtest.FirstUserParams.Password,
"password", coderdtest.FirstUserParams.Password, // confirm
"trial", "no",
}
for i := 0; i < len(matches); i += 2 {
match := matches[i]
value := matches[i+1]
pty.ExpectMatch(match)
pty.WriteLine(value)
}
pty.ExpectMatch("Welcome to Coder")
<-doneChan
ctx := testutil.Context(t, testutil.WaitShort)
resp, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
Email: coderdtest.FirstUserParams.Email,
Password: coderdtest.FirstUserParams.Password,
})
require.NoError(t, err)
client.SetSessionToken(resp.SessionToken)
me, err := client.User(ctx, codersdk.Me)
require.NoError(t, err)
assert.Equal(t, coderdtest.FirstUserParams.Username, me.Username)
assert.Equal(t, coderdtest.FirstUserParams.Name, me.Name)
assert.Equal(t, coderdtest.FirstUserParams.Email, me.Email)
})
t.Run("InitialUserTTYNameOptional", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
// The --force-tty flag is required on Windows, because the `isatty` library does not
// accurately detect Windows ptys when they are not attached to a process:
// https://github.com/mattn/go-isatty/issues/59
doneChan := make(chan struct{})
root, _ := clitest.New(t, "login", "--force-tty", client.URL.String())
pty := ptytest.New(t).Attach(root)
go func() {
defer close(doneChan)
err := root.Run()
assert.NoError(t, err)
}()
matches := []string{
"first user?", "yes",
"username", coderdtest.FirstUserParams.Username,
"name", "",
"email", coderdtest.FirstUserParams.Email,
"password", coderdtest.FirstUserParams.Password,
"password", coderdtest.FirstUserParams.Password, // confirm
"trial", "yes",
"firstName", coderdtest.TrialUserParams.FirstName,
"lastName", coderdtest.TrialUserParams.LastName,
"phoneNumber", coderdtest.TrialUserParams.PhoneNumber,
"jobTitle", coderdtest.TrialUserParams.JobTitle,
"companyName", coderdtest.TrialUserParams.CompanyName,
// `developers` and `country` `cliui.Select` automatically selects the first option during tests.
}
for i := 0; i < len(matches); i += 2 {
match := matches[i]
value := matches[i+1]
pty.ExpectMatch(match)
pty.WriteLine(value)
}
pty.ExpectMatch("Welcome to Coder")
<-doneChan
ctx := testutil.Context(t, testutil.WaitShort)
resp, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
Email: coderdtest.FirstUserParams.Email,
Password: coderdtest.FirstUserParams.Password,
})
require.NoError(t, err)
client.SetSessionToken(resp.SessionToken)
me, err := client.User(ctx, codersdk.Me)
require.NoError(t, err)
assert.Equal(t, coderdtest.FirstUserParams.Username, me.Username)
assert.Equal(t, coderdtest.FirstUserParams.Email, me.Email)
assert.Empty(t, me.Name)
})
t.Run("InitialUserTTYFlag", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
// The --force-tty flag is required on Windows, because the `isatty` library does not
// accurately detect Windows ptys when they are not attached to a process:
// https://github.com/mattn/go-isatty/issues/59
inv, _ := clitest.New(t, "--url", client.URL.String(), "login", "--force-tty")
pty := ptytest.New(t).Attach(inv)
clitest.Start(t, inv)
pty.ExpectMatch(fmt.Sprintf("Attempting to authenticate with flag URL: '%s'", client.URL.String()))
matches := []string{
"first user?", "yes",
"username", coderdtest.FirstUserParams.Username,
"name", coderdtest.FirstUserParams.Name,
"email", coderdtest.FirstUserParams.Email,
"password", coderdtest.FirstUserParams.Password,
"password", coderdtest.FirstUserParams.Password, // confirm
"trial", "yes",
"firstName", coderdtest.TrialUserParams.FirstName,
"lastName", coderdtest.TrialUserParams.LastName,
"phoneNumber", coderdtest.TrialUserParams.PhoneNumber,
"jobTitle", coderdtest.TrialUserParams.JobTitle,
"companyName", coderdtest.TrialUserParams.CompanyName,
// `developers` and `country` `cliui.Select` automatically selects the first option during tests.
}
for i := 0; i < len(matches); i += 2 {
match := matches[i]
value := matches[i+1]
pty.ExpectMatch(match)
pty.WriteLine(value)
}
pty.ExpectMatch("Welcome to Coder")
ctx := testutil.Context(t, testutil.WaitShort)
resp, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
Email: coderdtest.FirstUserParams.Email,
Password: coderdtest.FirstUserParams.Password,
})
require.NoError(t, err)
client.SetSessionToken(resp.SessionToken)
me, err := client.User(ctx, codersdk.Me)
require.NoError(t, err)
assert.Equal(t, coderdtest.FirstUserParams.Username, me.Username)
assert.Equal(t, coderdtest.FirstUserParams.Name, me.Name)
assert.Equal(t, coderdtest.FirstUserParams.Email, me.Email)
})
t.Run("InitialUserFlags", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
inv, _ := clitest.New(
t, "login", client.URL.String(),
"--first-user-username", coderdtest.FirstUserParams.Username,
"--first-user-full-name", coderdtest.FirstUserParams.Name,
"--first-user-email", coderdtest.FirstUserParams.Email,
"--first-user-password", coderdtest.FirstUserParams.Password,
"--first-user-trial",
)
pty := ptytest.New(t).Attach(inv)
w := clitest.StartWithWaiter(t, inv)
pty.ExpectMatch("firstName")
pty.WriteLine(coderdtest.TrialUserParams.FirstName)
pty.ExpectMatch("lastName")
pty.WriteLine(coderdtest.TrialUserParams.LastName)
pty.ExpectMatch("phoneNumber")
pty.WriteLine(coderdtest.TrialUserParams.PhoneNumber)
pty.ExpectMatch("jobTitle")
pty.WriteLine(coderdtest.TrialUserParams.JobTitle)
pty.ExpectMatch("companyName")
pty.WriteLine(coderdtest.TrialUserParams.CompanyName)
// `developers` and `country` `cliui.Select` automatically selects the first option during tests.
pty.ExpectMatch("Welcome to Coder")
w.RequireSuccess()
ctx := testutil.Context(t, testutil.WaitShort)
resp, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
Email: coderdtest.FirstUserParams.Email,
Password: coderdtest.FirstUserParams.Password,
})
require.NoError(t, err)
client.SetSessionToken(resp.SessionToken)
me, err := client.User(ctx, codersdk.Me)
require.NoError(t, err)
assert.Equal(t, coderdtest.FirstUserParams.Username, me.Username)
assert.Equal(t, coderdtest.FirstUserParams.Name, me.Name)
assert.Equal(t, coderdtest.FirstUserParams.Email, me.Email)
})
t.Run("InitialUserFlagsNameOptional", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
inv, _ := clitest.New(
t, "login", client.URL.String(),
"--first-user-username", coderdtest.FirstUserParams.Username,
"--first-user-email", coderdtest.FirstUserParams.Email,
"--first-user-password", coderdtest.FirstUserParams.Password,
"--first-user-trial",
)
pty := ptytest.New(t).Attach(inv)
w := clitest.StartWithWaiter(t, inv)
pty.ExpectMatch("firstName")
pty.WriteLine(coderdtest.TrialUserParams.FirstName)
pty.ExpectMatch("lastName")
pty.WriteLine(coderdtest.TrialUserParams.LastName)
pty.ExpectMatch("phoneNumber")
pty.WriteLine(coderdtest.TrialUserParams.PhoneNumber)
pty.ExpectMatch("jobTitle")
pty.WriteLine(coderdtest.TrialUserParams.JobTitle)
pty.ExpectMatch("companyName")
pty.WriteLine(coderdtest.TrialUserParams.CompanyName)
// `developers` and `country` `cliui.Select` automatically selects the first option during tests.
pty.ExpectMatch("Welcome to Coder")
w.RequireSuccess()
ctx := testutil.Context(t, testutil.WaitShort)
resp, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
Email: coderdtest.FirstUserParams.Email,
Password: coderdtest.FirstUserParams.Password,
})
require.NoError(t, err)
client.SetSessionToken(resp.SessionToken)
me, err := client.User(ctx, codersdk.Me)
require.NoError(t, err)
assert.Equal(t, coderdtest.FirstUserParams.Username, me.Username)
assert.Equal(t, coderdtest.FirstUserParams.Email, me.Email)
assert.Empty(t, me.Name)
})
t.Run("InitialUserTTYConfirmPasswordFailAndReprompt", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
client := coderdtest.New(t, nil)
// The --force-tty flag is required on Windows, because the `isatty` library does not
// accurately detect Windows ptys when they are not attached to a process:
// https://github.com/mattn/go-isatty/issues/59
doneChan := make(chan struct{})
root, _ := clitest.New(t, "login", "--force-tty", client.URL.String())
pty := ptytest.New(t).Attach(root)
go func() {
defer close(doneChan)
err := root.WithContext(ctx).Run()
assert.NoError(t, err)
}()
matches := []string{
"first user?", "yes",
"username", coderdtest.FirstUserParams.Username,
"name", coderdtest.FirstUserParams.Name,
"email", coderdtest.FirstUserParams.Email,
"password", coderdtest.FirstUserParams.Password,
"password", "something completely different",
}
for i := 0; i < len(matches); i += 2 {
match := matches[i]
value := matches[i+1]
pty.ExpectMatch(match)
pty.WriteLine(value)
}
// Validate that we reprompt for matching passwords.
pty.ExpectMatch("Passwords do not match")
pty.ExpectMatch("Enter a " + pretty.Sprint(cliui.DefaultStyles.Field, "password"))
pty.WriteLine(coderdtest.FirstUserParams.Password)
pty.ExpectMatch("Confirm")
pty.WriteLine(coderdtest.FirstUserParams.Password)
pty.ExpectMatch("trial")
pty.WriteLine("yes")
pty.ExpectMatch("firstName")
pty.WriteLine(coderdtest.TrialUserParams.FirstName)
pty.ExpectMatch("lastName")
pty.WriteLine(coderdtest.TrialUserParams.LastName)
pty.ExpectMatch("phoneNumber")
pty.WriteLine(coderdtest.TrialUserParams.PhoneNumber)
pty.ExpectMatch("jobTitle")
pty.WriteLine(coderdtest.TrialUserParams.JobTitle)
pty.ExpectMatch("companyName")
pty.WriteLine(coderdtest.TrialUserParams.CompanyName)
pty.ExpectMatch("Welcome to Coder")
<-doneChan
})
t.Run("ExistingUserValidTokenTTY", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
coderdtest.CreateFirstUser(t, client)
doneChan := make(chan struct{})
root, _ := clitest.New(t, "login", "--force-tty", client.URL.String(), "--no-open")
pty := ptytest.New(t).Attach(root)
go func() {
defer close(doneChan)
err := root.Run()
assert.NoError(t, err)
}()
pty.ExpectMatch(fmt.Sprintf("Attempting to authenticate with argument URL: '%s'", client.URL.String()))
pty.ExpectMatch("Paste your token here:")
pty.WriteLine(client.SessionToken())
if runtime.GOOS != "windows" {
// For some reason, the match does not show up on Windows.
pty.ExpectMatch(client.SessionToken())
}
pty.ExpectMatch("Welcome to Coder")
<-doneChan
})
t.Run("ExistingUserURLSavedInConfig", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
url := client.URL.String()
coderdtest.CreateFirstUser(t, client)
inv, root := clitest.New(t, "login", "--no-open")
clitest.SetupConfig(t, client, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
go func() {
defer close(doneChan)
err := inv.Run()
assert.NoError(t, err)
}()
pty.ExpectMatch(fmt.Sprintf("Attempting to authenticate with config URL: '%s'", url))
pty.ExpectMatch("Paste your token here:")
pty.WriteLine(client.SessionToken())
<-doneChan
})
t.Run("ExistingUserURLSavedInEnv", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
url := client.URL.String()
coderdtest.CreateFirstUser(t, client)
inv, _ := clitest.New(t, "login", "--no-open")
inv.Environ.Set("CODER_URL", url)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
go func() {
defer close(doneChan)
err := inv.Run()
assert.NoError(t, err)
}()
pty.ExpectMatch(fmt.Sprintf("Attempting to authenticate with environment URL: '%s'", url))
pty.ExpectMatch("Paste your token here:")
pty.WriteLine(client.SessionToken())
<-doneChan
})
t.Run("ExistingUserInvalidTokenTTY", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
coderdtest.CreateFirstUser(t, client)
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
doneChan := make(chan struct{})
root, _ := clitest.New(t, "login", client.URL.String(), "--no-open")
pty := ptytest.New(t).Attach(root)
go func() {
defer close(doneChan)
err := root.WithContext(ctx).Run()
// An error is expected in this case, since the login wasn't successful:
assert.Error(t, err)
}()
pty.ExpectMatch("Paste your token here:")
pty.WriteLine("an-invalid-token")
if runtime.GOOS != "windows" {
// For some reason, the match does not show up on Windows.
pty.ExpectMatch("an-invalid-token")
}
pty.ExpectMatch("That's not a valid token!")
cancelFunc()
<-doneChan
})
// TokenFlag should generate a new session token and store it in the session file.
t.Run("TokenFlag", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
coderdtest.CreateFirstUser(t, client)
root, cfg := clitest.New(t, "login", client.URL.String(), "--token", client.SessionToken())
err := root.Run()
require.NoError(t, err)
sessionFile, err := cfg.Session().Read()
require.NoError(t, err)
// This **should not be equal** to the token we passed in.
require.NotEqual(t, client.SessionToken(), sessionFile)
})
t.Run("KeepOrganizationContext", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
first := coderdtest.CreateFirstUser(t, client)
root, cfg := clitest.New(t, "login", client.URL.String(), "--token", client.SessionToken())
err := cfg.Organization().Write(first.OrganizationID.String())
require.NoError(t, err, "write bad org to config")
err = root.Run()
require.NoError(t, err)
sessionFile, err := cfg.Session().Read()
require.NoError(t, err)
require.NotEqual(t, client.SessionToken(), sessionFile)
// Organization config should be deleted since the org does not exist
selected, err := cfg.Organization().Read()
require.NoError(t, err)
require.Equal(t, selected, first.OrganizationID.String())
})
}