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