test: batch 00 of refactoring CLI tests not to use PTY (#25868)

Part of https://github.com/coder/internal/issues/1400

Batch of refactored CLI tests to avoid creating PTYs.
This commit is contained in:
Spike Curtis
2026-05-29 15:33:45 -04:00
committed by GitHub
parent 0401ed3af5
commit 8a47b7fa14
9 changed files with 192 additions and 180 deletions
+4 -3
View File
@@ -7,8 +7,8 @@ import (
"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/pty/ptytest"
"github.com/coder/coder/v2/testutil"
"github.com/coder/coder/v2/testutil/expecter"
)
func TestMain(m *testing.M) {
@@ -17,11 +17,12 @@ func TestMain(m *testing.M) {
func TestCli(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitMedium)
clitest.CreateTemplateVersionSource(t, nil)
client := coderdtest.New(t, nil)
i, config := clitest.New(t)
clitest.SetupConfig(t, client, config)
pty := ptytest.New(t).Attach(i)
stdout := expecter.NewAttachedToInvocation(t, i)
clitest.Start(t, i)
pty.ExpectMatch("coder")
stdout.ExpectMatchContext(ctx, "coder")
}
+5 -6
View File
@@ -10,8 +10,8 @@ import (
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/pty/ptytest"
"github.com/coder/coder/v2/testutil"
"github.com/coder/coder/v2/testutil/expecter"
"github.com/coder/serpent"
)
@@ -21,7 +21,6 @@ func TestExternalAuth(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()
ptty := ptytest.New(t)
cmd := &serpent.Command{
Handler: func(inv *serpent.Invocation) error {
var fetched atomic.Bool
@@ -42,16 +41,16 @@ func TestExternalAuth(t *testing.T) {
}
inv := cmd.Invoke().WithContext(ctx)
stdout := expecter.NewAttachedToInvocation(t, inv)
ptty.Attach(inv)
done := make(chan struct{})
go func() {
defer close(done)
err := inv.Run()
assert.NoError(t, err)
}()
ptty.ExpectMatchContext(ctx, "You must authenticate with")
ptty.ExpectMatchContext(ctx, "https://example.com/gitauth/github")
ptty.ExpectMatchContext(ctx, "Successfully authenticated with GitHub")
stdout.ExpectMatchContext(ctx, "You must authenticate with")
stdout.ExpectMatchContext(ctx, "https://example.com/gitauth/github")
stdout.ExpectMatchContext(ctx, "Successfully authenticated with GitHub")
<-done
}
+19 -20
View File
@@ -16,8 +16,8 @@ import (
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/pty/ptytest"
"github.com/coder/coder/v2/testutil"
"github.com/coder/coder/v2/testutil/expecter"
"github.com/coder/serpent"
)
@@ -48,12 +48,12 @@ func TestProvisionerJob(t *testing.T) {
test.JobMutex.Unlock()
})
testutil.Eventually(ctx, t, func(ctx context.Context) (done bool) {
test.PTY.ExpectMatch(cliui.ProvisioningStateQueued)
test.Stdout.ExpectMatchContext(ctx, cliui.ProvisioningStateQueued)
test.Next <- struct{}{}
test.PTY.ExpectMatch(cliui.ProvisioningStateQueued)
test.PTY.ExpectMatch(cliui.ProvisioningStateRunning)
test.Stdout.ExpectMatchContext(ctx, cliui.ProvisioningStateQueued)
test.Stdout.ExpectMatchContext(ctx, cliui.ProvisioningStateRunning)
test.Next <- struct{}{}
test.PTY.ExpectMatch(cliui.ProvisioningStateRunning)
test.Stdout.ExpectMatchContext(ctx, cliui.ProvisioningStateRunning)
return true
}, testutil.IntervalFast)
})
@@ -85,12 +85,12 @@ func TestProvisionerJob(t *testing.T) {
test.JobMutex.Unlock()
})
testutil.Eventually(ctx, t, func(ctx context.Context) (done bool) {
test.PTY.ExpectMatch(cliui.ProvisioningStateQueued)
test.Stdout.ExpectMatchContext(ctx, cliui.ProvisioningStateQueued)
test.Next <- struct{}{}
test.PTY.ExpectMatch(cliui.ProvisioningStateQueued)
test.PTY.ExpectMatch("Something")
test.Stdout.ExpectMatchContext(ctx, cliui.ProvisioningStateQueued)
test.Stdout.ExpectMatchContext(ctx, "Something")
test.Next <- struct{}{}
test.PTY.ExpectMatch("Something")
test.Stdout.ExpectMatchContext(ctx, "Something")
return true
}, testutil.IntervalFast)
})
@@ -151,12 +151,12 @@ func TestProvisionerJob(t *testing.T) {
test.JobMutex.Unlock()
})
testutil.Eventually(ctx, t, func(ctx context.Context) (done bool) {
test.PTY.ExpectRegexMatch(tc.expected)
test.Stdout.ExpectRegexMatchContext(ctx, tc.expected)
test.Next <- struct{}{}
test.PTY.ExpectMatch(cliui.ProvisioningStateQueued) // step completed
test.PTY.ExpectMatch(cliui.ProvisioningStateRunning)
test.Stdout.ExpectMatchContext(ctx, cliui.ProvisioningStateQueued) // step completed
test.Stdout.ExpectMatchContext(ctx, cliui.ProvisioningStateRunning)
test.Next <- struct{}{}
test.PTY.ExpectMatch(cliui.ProvisioningStateRunning)
test.Stdout.ExpectMatchContext(ctx, cliui.ProvisioningStateRunning)
return true
}, testutil.IntervalFast)
})
@@ -193,11 +193,11 @@ func TestProvisionerJob(t *testing.T) {
test.JobMutex.Unlock()
})
testutil.Eventually(ctx, t, func(ctx context.Context) (done bool) {
test.PTY.ExpectMatch(cliui.ProvisioningStateQueued)
test.Stdout.ExpectMatchContext(ctx, cliui.ProvisioningStateQueued)
test.Next <- struct{}{}
test.PTY.ExpectMatch("Gracefully canceling")
test.Stdout.ExpectMatchContext(ctx, "Gracefully canceling")
test.Next <- struct{}{}
test.PTY.ExpectMatch(cliui.ProvisioningStateQueued)
test.Stdout.ExpectMatchContext(ctx, cliui.ProvisioningStateQueued)
return true
}, testutil.IntervalFast)
})
@@ -208,7 +208,7 @@ type provisionerJobTest struct {
Job *codersdk.ProvisionerJob
JobMutex *sync.Mutex
Logs chan codersdk.ProvisionerJobLog
PTY *ptytest.PTY
Stdout *expecter.Expecter
}
func newProvisionerJob(t *testing.T) provisionerJobTest {
@@ -240,8 +240,7 @@ func newProvisionerJob(t *testing.T) provisionerJobTest {
}
inv := cmd.Invoke()
ptty := ptytest.New(t)
ptty.Attach(inv)
stdout := expecter.NewAttachedToInvocation(t, inv)
done := make(chan struct{})
go func() {
defer close(done)
@@ -258,7 +257,7 @@ func newProvisionerJob(t *testing.T) provisionerJobTest {
Job: job,
JobMutex: &jobLock,
Logs: logs,
PTY: ptty,
Stdout: stdout,
}
}
+7 -13
View File
@@ -8,7 +8,6 @@ import (
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/pty/ptytest"
"github.com/coder/serpent"
)
@@ -16,10 +15,9 @@ func TestSelect(t *testing.T) {
t.Parallel()
t.Run("Select", func(t *testing.T) {
t.Parallel()
ptty := ptytest.New(t)
msgChan := make(chan string)
go func() {
resp, err := newSelect(ptty, cliui.SelectOptions{
resp, err := newSelect(cliui.SelectOptions{
Options: []string{"First", "Second"},
})
assert.NoError(t, err)
@@ -29,7 +27,7 @@ func TestSelect(t *testing.T) {
})
}
func newSelect(ptty *ptytest.PTY, opts cliui.SelectOptions) (string, error) {
func newSelect(opts cliui.SelectOptions) (string, error) {
value := ""
cmd := &serpent.Command{
Handler: func(inv *serpent.Invocation) error {
@@ -39,7 +37,6 @@ func newSelect(ptty *ptytest.PTY, opts cliui.SelectOptions) (string, error) {
},
}
inv := cmd.Invoke()
ptty.Attach(inv)
return value, inv.Run()
}
@@ -47,10 +44,10 @@ func TestRichSelect(t *testing.T) {
t.Parallel()
t.Run("RichSelect", func(t *testing.T) {
t.Parallel()
ptty := ptytest.New(t)
msgChan := make(chan string)
go func() {
resp, err := newRichSelect(ptty, cliui.RichSelectOptions{
resp, err := newRichSelect(cliui.RichSelectOptions{
Options: []codersdk.TemplateVersionParameterOption{
{Name: "A-Name", Value: "A-Value", Description: "A-Description."},
{Name: "B-Name", Value: "B-Value", Description: "B-Description."},
@@ -63,7 +60,7 @@ func TestRichSelect(t *testing.T) {
})
}
func newRichSelect(ptty *ptytest.PTY, opts cliui.RichSelectOptions) (string, error) {
func newRichSelect(opts cliui.RichSelectOptions) (string, error) {
value := ""
cmd := &serpent.Command{
Handler: func(inv *serpent.Invocation) error {
@@ -75,7 +72,6 @@ func newRichSelect(ptty *ptytest.PTY, opts cliui.RichSelectOptions) (string, err
},
}
inv := cmd.Invoke()
ptty.Attach(inv)
return value, inv.Run()
}
@@ -181,11 +177,10 @@ func TestMultiSelect(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
ptty := ptytest.New(t)
msgChan := make(chan []string)
go func() {
resp, err := newMultiSelect(ptty, tt.items, tt.allowCustom)
resp, err := newMultiSelect(tt.items, tt.allowCustom)
assert.NoError(t, err)
msgChan <- resp
}()
@@ -195,7 +190,7 @@ func TestMultiSelect(t *testing.T) {
}
}
func newMultiSelect(pty *ptytest.PTY, items []string, custom bool) ([]string, error) {
func newMultiSelect(items []string, custom bool) ([]string, error) {
var values []string
cmd := &serpent.Command{
Handler: func(inv *serpent.Invocation) error {
@@ -211,6 +206,5 @@ func newMultiSelect(pty *ptytest.PTY, items []string, custom bool) ([]string, er
},
}
inv := cmd.Invoke()
pty.Attach(inv)
return values, inv.Run()
}
+13 -12
View File
@@ -24,8 +24,8 @@ import (
"github.com/coder/coder/v2/coderd/database/dbfake"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/workspacesdk"
"github.com/coder/coder/v2/pty/ptytest"
"github.com/coder/coder/v2/testutil"
"github.com/coder/coder/v2/testutil/expecter"
)
func sshConfigFileName(t *testing.T) (sshConfig string) {
@@ -64,6 +64,8 @@ func TestConfigSSH(t *testing.T) {
t.Skip("See coder/internal#117")
}
logger := testutil.Logger(t)
ctx := testutil.Context(t, testutil.WaitMedium)
const hostname = "test-coder."
const expectedKey = "ConnectionAttempts"
const removeKey = "ConnectTimeout"
@@ -131,9 +133,8 @@ func TestConfigSSH(t *testing.T) {
"--ssh-config-file", sshConfigFile,
"--skip-proxy-command")
clitest.SetupConfig(t, member, root)
pty := ptytest.New(t)
inv.Stdin = pty.Input()
inv.Stdout = pty.Output()
stdout := expecter.NewAttachedToInvocation(t, inv)
stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
waiter := clitest.StartWithWaiter(t, inv)
@@ -143,8 +144,8 @@ func TestConfigSSH(t *testing.T) {
{match: "Continue?", write: "yes"},
}
for _, m := range matches {
pty.ExpectMatch(m.match)
pty.WriteLine(m.write)
stdout.ExpectMatchContext(ctx, m.match)
stdin.WriteLine(m.write)
}
waiter.RequireSuccess()
@@ -157,10 +158,8 @@ func TestConfigSSH(t *testing.T) {
home := filepath.Dir(filepath.Dir(sshConfigFile))
// #nosec
sshCmd := exec.Command("ssh", "-F", sshConfigFile, hostname+r.Workspace.Name, "echo", "test")
pty = ptytest.New(t)
// Set HOME because coder config is included from ~/.ssh/coder.
sshCmd.Env = append(sshCmd.Env, fmt.Sprintf("HOME=%s", home))
inv.Stderr = pty.Output()
data, err := sshCmd.Output()
require.NoError(t, err)
require.Equal(t, "test", strings.TrimSpace(string(data)))
@@ -693,6 +692,8 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
logger := testutil.Logger(t)
ctx := testutil.Context(t, testutil.WaitMedium)
client, db := coderdtest.NewWithDatabase(t, nil)
user := coderdtest.CreateFirstUser(t, client)
@@ -718,8 +719,8 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) {
//nolint:gocritic // This has always ran with the admin user.
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t)
pty.Attach(inv)
stdout := expecter.NewAttachedToInvocation(t, inv)
stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
done := tGo(t, func() {
err := inv.Run()
if !tt.wantErr {
@@ -730,8 +731,8 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) {
})
for _, m := range tt.matches {
pty.ExpectMatch(m.match)
pty.WriteLine(m.write)
stdout.ExpectMatchContext(ctx, m.match)
stdin.WriteLine(m.write)
}
<-done
+15 -15
View File
@@ -22,8 +22,8 @@ import (
"github.com/coder/coder/v2/coderd/database/pubsub"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/pty/ptytest"
"github.com/coder/coder/v2/testutil"
"github.com/coder/coder/v2/testutil/expecter"
"github.com/coder/quartz"
)
@@ -31,6 +31,7 @@ func TestDelete(t *testing.T) {
t.Parallel()
t.Run("WithParameter", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitMedium)
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, client)
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
@@ -42,7 +43,7 @@ func TestDelete(t *testing.T) {
inv, root := clitest.New(t, "delete", workspace.Name, "-y")
clitest.SetupConfig(t, member, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
stdout := expecter.NewAttachedToInvocation(t, inv)
go func() {
defer close(doneChan)
err := inv.Run()
@@ -51,7 +52,7 @@ func TestDelete(t *testing.T) {
assert.ErrorIs(t, err, io.EOF)
}
}()
pty.ExpectMatch("has been deleted")
stdout.ExpectMatchContext(ctx, "has been deleted")
<-doneChan
})
@@ -71,8 +72,7 @@ func TestDelete(t *testing.T) {
clitest.SetupConfig(t, templateAdmin, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
inv.Stderr = pty.Output()
stdout := expecter.NewAttachedToInvocation(t, inv)
go func() {
defer close(doneChan)
err := inv.WithContext(ctx).Run()
@@ -81,7 +81,7 @@ func TestDelete(t *testing.T) {
assert.ErrorIs(t, err, io.EOF)
}
}()
pty.ExpectMatch("has been deleted")
stdout.ExpectMatchContext(ctx, "has been deleted")
testutil.TryReceive(ctx, t, doneChan)
_, err := client.Workspace(ctx, workspace.ID)
@@ -117,8 +117,7 @@ func TestDelete(t *testing.T) {
//nolint:gocritic // Deleting orphaned workspaces requires an admin.
clitest.SetupConfig(t, client, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
inv.Stderr = pty.Output()
stdout := expecter.NewAttachedToInvocation(t, inv)
go func() {
defer close(doneChan)
err := inv.Run()
@@ -127,7 +126,7 @@ func TestDelete(t *testing.T) {
assert.ErrorIs(t, err, io.EOF)
}
}()
pty.ExpectMatch("has been deleted")
stdout.ExpectMatchContext(ctx, "has been deleted")
<-doneChan
})
@@ -146,11 +145,12 @@ func TestDelete(t *testing.T) {
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
ctx := testutil.Context(t, testutil.WaitMedium)
inv, root := clitest.New(t, "delete", user.Username+"/"+workspace.Name, "-y")
//nolint:gocritic // This requires an admin.
clitest.SetupConfig(t, adminClient, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
stdout := expecter.NewAttachedToInvocation(t, inv)
go func() {
defer close(doneChan)
err := inv.Run()
@@ -160,7 +160,7 @@ func TestDelete(t *testing.T) {
}
}()
pty.ExpectMatch("has been deleted")
stdout.ExpectMatchContext(ctx, "has been deleted")
<-doneChan
workspace, err = client.Workspace(context.Background(), workspace.ID)
@@ -207,7 +207,7 @@ func TestDelete(t *testing.T) {
// Then: the workspace deletion should warn about no provisioners
inv, root := clitest.New(t, "delete", workspace.Name, "-y")
pty := ptytest.New(t).Attach(inv)
stdout := expecter.NewAttachedToInvocation(t, inv)
clitest.SetupConfig(t, templateAdmin, root)
doneChan := make(chan struct{})
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
@@ -216,7 +216,7 @@ func TestDelete(t *testing.T) {
defer close(doneChan)
_ = inv.WithContext(ctx).Run()
}()
pty.ExpectMatch("there are no provisioners that accept the required tags")
stdout.ExpectMatchContext(ctx, "there are no provisioners that accept the required tags")
cancel()
<-doneChan
})
@@ -311,7 +311,7 @@ func TestDelete(t *testing.T) {
inv, root := clitest.New(t, "delete", workspaceOwner+"/"+workspace.Name, "-y")
clitest.SetupConfig(t, runClient, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
stdout := expecter.NewAttachedToInvocation(t, inv)
var runErr error
go func() {
defer close(doneChan)
@@ -324,7 +324,7 @@ func TestDelete(t *testing.T) {
require.Error(t, runErr)
require.Contains(t, runErr.Error(), expectedErr)
} else {
pty.ExpectMatch("has been deleted")
stdout.ExpectMatchContext(ctx, "has been deleted")
<-doneChan
// When running with the race detector on, we sometimes get an EOF.
+12 -10
View File
@@ -15,8 +15,8 @@ import (
"github.com/coder/coder/v2/agent/agenttest"
"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/pty/ptytest"
"github.com/coder/coder/v2/testutil"
"github.com/coder/coder/v2/testutil/expecter"
)
func TestExpRpty(t *testing.T) {
@@ -28,7 +28,7 @@ func TestExpRpty(t *testing.T) {
client, workspace, agentToken := setupWorkspaceForAgent(t)
inv, root := clitest.New(t, "exp", "rpty", workspace.Name)
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t).Attach(inv)
stdin := testutil.NewWriterAttachedToInvocation(t, testutil.Logger(t), inv)
ctx := testutil.Context(t, testutil.WaitLong)
@@ -40,7 +40,7 @@ func TestExpRpty(t *testing.T) {
assert.NoError(t, err)
})
pty.WriteLine("exit")
stdin.WriteLine("exit")
<-cmdDone
})
@@ -51,7 +51,7 @@ func TestExpRpty(t *testing.T) {
randStr := uuid.NewString()
inv, root := clitest.New(t, "exp", "rpty", workspace.Name, "echo", randStr)
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t).Attach(inv)
stdout := expecter.NewAttachedToInvocation(t, inv)
ctx := testutil.Context(t, testutil.WaitLong)
@@ -63,7 +63,7 @@ func TestExpRpty(t *testing.T) {
assert.NoError(t, err)
})
pty.ExpectMatch(randStr)
stdout.ExpectMatchContext(ctx, randStr)
<-cmdDone
})
@@ -86,6 +86,7 @@ func TestExpRpty(t *testing.T) {
t.Skip("Skipping test on non-Linux platform")
}
logger := testutil.Logger(t)
wantLabel := "coder.devcontainers.TestExpRpty.Container"
client, workspace, agentToken := setupWorkspaceForAgent(t)
@@ -124,7 +125,8 @@ func TestExpRpty(t *testing.T) {
inv, root := clitest.New(t, "exp", "rpty", workspace.Name, "-c", ct.Container.ID)
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t).Attach(inv)
stdout := expecter.NewAttachedToInvocation(t, inv)
stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
ctx := testutil.Context(t, testutil.WaitLong)
cmdDone := tGo(t, func() {
@@ -132,10 +134,10 @@ func TestExpRpty(t *testing.T) {
assert.NoError(t, err)
})
pty.ExpectMatchContext(ctx, " #")
pty.WriteLine("hostname")
pty.ExpectMatchContext(ctx, ct.Container.Config.Hostname)
pty.WriteLine("exit")
stdout.ExpectMatchContext(ctx, " #")
stdin.WriteLine("hostname")
stdout.ExpectMatchContext(ctx, ct.Container.Config.Hostname)
stdin.WriteLine("exit")
<-cmdDone
})
}
+4 -4
View File
@@ -15,8 +15,8 @@ import (
"github.com/coder/coder/v2/coderd/database/dbfake"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/pty/ptytest"
"github.com/coder/coder/v2/testutil"
"github.com/coder/coder/v2/testutil/expecter"
)
func TestList(t *testing.T) {
@@ -34,7 +34,7 @@ func TestList(t *testing.T) {
inv, root := clitest.New(t, "ls")
clitest.SetupConfig(t, member, root)
pty := ptytest.New(t).Attach(inv)
stdout := expecter.NewAttachedToInvocation(t, inv)
ctx, cancelFunc := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancelFunc()
@@ -44,8 +44,8 @@ func TestList(t *testing.T) {
assert.NoError(t, errC)
close(done)
}()
pty.ExpectMatch(r.Workspace.Name)
pty.ExpectMatch("Started")
stdout.ExpectMatchContext(ctx, r.Workspace.Name)
stdout.ExpectMatchContext(ctx, "Started")
cancelFunc()
<-done
})
+113 -97
View File
@@ -5,7 +5,6 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
@@ -15,8 +14,8 @@ import (
"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/coder/v2/testutil/expecter"
"github.com/coder/pretty"
)
@@ -74,13 +73,16 @@ func TestLogin(t *testing.T) {
t.Run("InitialUserTTY", func(t *testing.T) {
t.Parallel()
logger := testutil.Logger(t)
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)
stdout := expecter.NewAttachedToInvocation(t, root)
stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), root)
ctx := testutil.Context(t, testutil.WaitMedium)
go func() {
defer close(doneChan)
err := root.Run()
@@ -105,12 +107,11 @@ func TestLogin(t *testing.T) {
for i := 0; i < len(matches); i += 2 {
match := matches[i]
value := matches[i+1]
pty.ExpectMatch(match)
pty.WriteLine(value)
stdout.ExpectMatchContext(ctx, match)
stdin.WriteLine(value)
}
pty.ExpectMatch("Welcome to Coder")
stdout.ExpectMatchContext(ctx, "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,
@@ -126,13 +127,16 @@ func TestLogin(t *testing.T) {
t.Run("InitialUserTTYWithNoTrial", func(t *testing.T) {
t.Parallel()
logger := testutil.Logger(t)
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)
stdout := expecter.NewAttachedToInvocation(t, root)
stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), root)
ctx := testutil.Context(t, testutil.WaitMedium)
go func() {
defer close(doneChan)
err := root.Run()
@@ -151,12 +155,11 @@ func TestLogin(t *testing.T) {
for i := 0; i < len(matches); i += 2 {
match := matches[i]
value := matches[i+1]
pty.ExpectMatch(match)
pty.WriteLine(value)
stdout.ExpectMatchContext(ctx, match)
stdin.WriteLine(value)
}
pty.ExpectMatch("Welcome to Coder")
stdout.ExpectMatchContext(ctx, "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,
@@ -172,13 +175,16 @@ func TestLogin(t *testing.T) {
t.Run("InitialUserTTYNameOptional", func(t *testing.T) {
t.Parallel()
logger := testutil.Logger(t)
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)
stdout := expecter.NewAttachedToInvocation(t, root)
stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), root)
ctx := testutil.Context(t, testutil.WaitMedium)
go func() {
defer close(doneChan)
err := root.Run()
@@ -203,12 +209,11 @@ func TestLogin(t *testing.T) {
for i := 0; i < len(matches); i += 2 {
match := matches[i]
value := matches[i+1]
pty.ExpectMatch(match)
pty.WriteLine(value)
stdout.ExpectMatchContext(ctx, match)
stdin.WriteLine(value)
}
pty.ExpectMatch("Welcome to Coder")
stdout.ExpectMatchContext(ctx, "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,
@@ -224,16 +229,19 @@ func TestLogin(t *testing.T) {
t.Run("InitialUserTTYFlag", func(t *testing.T) {
t.Parallel()
logger := testutil.Logger(t)
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)
stdout := expecter.NewAttachedToInvocation(t, inv)
stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
ctx := testutil.Context(t, testutil.WaitMedium)
clitest.Start(t, inv)
pty.ExpectMatch(fmt.Sprintf("Attempting to authenticate with flag URL: '%s'", client.URL.String()))
stdout.ExpectMatchContext(ctx, fmt.Sprintf("Attempting to authenticate with flag URL: '%s'", client.URL.String()))
matches := []string{
"first user?", "yes",
"username", coderdtest.FirstUserParams.Username,
@@ -252,11 +260,10 @@ func TestLogin(t *testing.T) {
for i := 0; i < len(matches); i += 2 {
match := matches[i]
value := matches[i+1]
pty.ExpectMatch(match)
pty.WriteLine(value)
stdout.ExpectMatchContext(ctx, match)
stdin.WriteLine(value)
}
pty.ExpectMatch("Welcome to Coder")
ctx := testutil.Context(t, testutil.WaitShort)
stdout.ExpectMatchContext(ctx, "Welcome to Coder")
resp, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
Email: coderdtest.FirstUserParams.Email,
Password: coderdtest.FirstUserParams.Password,
@@ -272,6 +279,7 @@ func TestLogin(t *testing.T) {
t.Run("InitialUserFlags", func(t *testing.T) {
t.Parallel()
logger := testutil.Logger(t)
client := coderdtest.New(t, nil)
inv, _ := clitest.New(
t, "login", client.URL.String(),
@@ -281,22 +289,23 @@ func TestLogin(t *testing.T) {
"--first-user-password", coderdtest.FirstUserParams.Password,
"--first-user-trial",
)
pty := ptytest.New(t).Attach(inv)
stdout := expecter.NewAttachedToInvocation(t, inv)
stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
ctx := testutil.Context(t, testutil.WaitMedium)
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)
stdout.ExpectMatchContext(ctx, "firstName")
stdin.WriteLine(coderdtest.TrialUserParams.FirstName)
stdout.ExpectMatchContext(ctx, "lastName")
stdin.WriteLine(coderdtest.TrialUserParams.LastName)
stdout.ExpectMatchContext(ctx, "phoneNumber")
stdin.WriteLine(coderdtest.TrialUserParams.PhoneNumber)
stdout.ExpectMatchContext(ctx, "jobTitle")
stdin.WriteLine(coderdtest.TrialUserParams.JobTitle)
stdout.ExpectMatchContext(ctx, "companyName")
stdin.WriteLine(coderdtest.TrialUserParams.CompanyName)
// `developers` and `country` `cliui.Select` automatically selects the first option during tests.
pty.ExpectMatch("Welcome to Coder")
stdout.ExpectMatchContext(ctx, "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,
@@ -312,6 +321,7 @@ func TestLogin(t *testing.T) {
t.Run("InitialUserFlagsNameOptional", func(t *testing.T) {
t.Parallel()
logger := testutil.Logger(t)
client := coderdtest.New(t, nil)
inv, _ := clitest.New(
t, "login", client.URL.String(),
@@ -320,22 +330,23 @@ func TestLogin(t *testing.T) {
"--first-user-password", coderdtest.FirstUserParams.Password,
"--first-user-trial",
)
pty := ptytest.New(t).Attach(inv)
stdout := expecter.NewAttachedToInvocation(t, inv)
stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
ctx := testutil.Context(t, testutil.WaitMedium)
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)
stdout.ExpectMatchContext(ctx, "firstName")
stdin.WriteLine(coderdtest.TrialUserParams.FirstName)
stdout.ExpectMatchContext(ctx, "lastName")
stdin.WriteLine(coderdtest.TrialUserParams.LastName)
stdout.ExpectMatchContext(ctx, "phoneNumber")
stdin.WriteLine(coderdtest.TrialUserParams.PhoneNumber)
stdout.ExpectMatchContext(ctx, "jobTitle")
stdin.WriteLine(coderdtest.TrialUserParams.JobTitle)
stdout.ExpectMatchContext(ctx, "companyName")
stdin.WriteLine(coderdtest.TrialUserParams.CompanyName)
// `developers` and `country` `cliui.Select` automatically selects the first option during tests.
pty.ExpectMatch("Welcome to Coder")
stdout.ExpectMatchContext(ctx, "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,
@@ -351,6 +362,7 @@ func TestLogin(t *testing.T) {
t.Run("InitialUserTTYConfirmPasswordFailAndReprompt", func(t *testing.T) {
t.Parallel()
logger := testutil.Logger(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
client := coderdtest.New(t, nil)
@@ -359,7 +371,8 @@ func TestLogin(t *testing.T) {
// 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)
stdout := expecter.NewAttachedToInvocation(t, root)
stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), root)
go func() {
defer close(doneChan)
err := root.WithContext(ctx).Run()
@@ -377,59 +390,60 @@ func TestLogin(t *testing.T) {
for i := 0; i < len(matches); i += 2 {
match := matches[i]
value := matches[i+1]
pty.ExpectMatch(match)
pty.WriteLine(value)
stdout.ExpectMatchContext(ctx, match)
stdin.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")
stdout.ExpectMatchContext(ctx, "Passwords do not match")
stdout.ExpectMatchContext(ctx, "Enter a "+pretty.Sprint(cliui.DefaultStyles.Field, "password"))
stdin.WriteLine(coderdtest.FirstUserParams.Password)
stdout.ExpectMatchContext(ctx, "Confirm")
stdin.WriteLine(coderdtest.FirstUserParams.Password)
stdout.ExpectMatchContext(ctx, "trial")
stdin.WriteLine("yes")
stdout.ExpectMatchContext(ctx, "firstName")
stdin.WriteLine(coderdtest.TrialUserParams.FirstName)
stdout.ExpectMatchContext(ctx, "lastName")
stdin.WriteLine(coderdtest.TrialUserParams.LastName)
stdout.ExpectMatchContext(ctx, "phoneNumber")
stdin.WriteLine(coderdtest.TrialUserParams.PhoneNumber)
stdout.ExpectMatchContext(ctx, "jobTitle")
stdin.WriteLine(coderdtest.TrialUserParams.JobTitle)
stdout.ExpectMatchContext(ctx, "companyName")
stdin.WriteLine(coderdtest.TrialUserParams.CompanyName)
stdout.ExpectMatchContext(ctx, "Welcome to Coder")
<-doneChan
})
t.Run("ExistingUserValidTokenTTY", func(t *testing.T) {
t.Parallel()
logger := testutil.Logger(t)
client := coderdtest.New(t, nil)
coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitMedium)
doneChan := make(chan struct{})
root, _ := clitest.New(t, "login", "--force-tty", client.URL.String(), "--no-open")
pty := ptytest.New(t).Attach(root)
stdout := expecter.NewAttachedToInvocation(t, root)
stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), 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")
stdout.ExpectMatchContext(ctx, fmt.Sprintf("Attempting to authenticate with argument URL: '%s'", client.URL.String()))
stdout.ExpectMatchContext(ctx, "Paste your token here:")
stdin.WriteLine(client.SessionToken())
stdout.ExpectMatchContext(ctx, "Welcome to Coder")
<-doneChan
})
t.Run("ExistingUserURLSavedInConfig", func(t *testing.T) {
t.Parallel()
logger := testutil.Logger(t)
ctx := testutil.Context(t, testutil.WaitMedium)
client := coderdtest.New(t, nil)
url := client.URL.String()
coderdtest.CreateFirstUser(t, client)
@@ -438,21 +452,24 @@ func TestLogin(t *testing.T) {
clitest.SetupConfig(t, client, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
stdout := expecter.NewAttachedToInvocation(t, inv)
stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), 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())
stdout.ExpectMatchContext(ctx, fmt.Sprintf("Attempting to authenticate with config URL: '%s'", url))
stdout.ExpectMatchContext(ctx, "Paste your token here:")
stdin.WriteLine(client.SessionToken())
<-doneChan
})
t.Run("ExistingUserURLSavedInEnv", func(t *testing.T) {
t.Parallel()
logger := testutil.Logger(t)
ctx := testutil.Context(t, testutil.WaitMedium)
client := coderdtest.New(t, nil)
url := client.URL.String()
coderdtest.CreateFirstUser(t, client)
@@ -461,21 +478,23 @@ func TestLogin(t *testing.T) {
inv.Environ.Set("CODER_URL", url)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
stdout := expecter.NewAttachedToInvocation(t, inv)
stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), 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())
stdout.ExpectMatchContext(ctx, fmt.Sprintf("Attempting to authenticate with environment URL: '%s'", url))
stdout.ExpectMatchContext(ctx, "Paste your token here:")
stdin.WriteLine(client.SessionToken())
<-doneChan
})
t.Run("ExistingUserInvalidTokenTTY", func(t *testing.T) {
t.Parallel()
logger := testutil.Logger(t)
client := coderdtest.New(t, nil)
coderdtest.CreateFirstUser(t, client)
@@ -483,7 +502,8 @@ func TestLogin(t *testing.T) {
defer cancelFunc()
doneChan := make(chan struct{})
root, _ := clitest.New(t, "login", client.URL.String(), "--no-open")
pty := ptytest.New(t).Attach(root)
stdout := expecter.NewAttachedToInvocation(t, root)
stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), root)
go func() {
defer close(doneChan)
err := root.WithContext(ctx).Run()
@@ -491,13 +511,9 @@ func TestLogin(t *testing.T) {
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!")
stdout.ExpectMatchContext(ctx, "Paste your token here:")
stdin.WriteLine("an-invalid-token")
stdout.ExpectMatchContext(ctx, "That's not a valid token!")
cancelFunc()
<-doneChan
})
@@ -582,12 +598,12 @@ func TestLoginToken(t *testing.T) {
inv, root := clitest.New(t, "login", "token", "--url", client.URL.String())
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t).Attach(inv)
stdout := expecter.NewAttachedToInvocation(t, inv)
ctx := testutil.Context(t, testutil.WaitShort)
err := inv.WithContext(ctx).Run()
require.NoError(t, err)
pty.ExpectMatch(client.SessionToken())
stdout.ExpectMatchContext(ctx, client.SessionToken())
})
t.Run("NoTokenStored", func(t *testing.T) {