test(e2e): add support to rdp connect test on windows

test(e2e): add support to rdp connect test on windows
This commit is contained in:
Tony authored and GitHub committed 2025-11-18 15:35:59 -05:00
commit 6533c8bdb3
22 files changed
+116 -68

No files matched your search

-44
View File
@@ -7,7 +7,6 @@ import (
"bufio"
"context"
"encoding/json"
"fmt"
"os/exec"
"testing"
@@ -44,46 +43,3 @@ func ConnectCli(t testing.TB, ctx context.Context, targetId string) ConnectCliOu
return connectCliOutput
}
// ConnectCliStdoutPipe uses the boundary CLI to establish connection to the target.
// It captures stdout via a pipe, parses proxy details from the command output, and returns them.
// The connection must be closed separately via the `boundary sessions cancel` command
// This implementation works on cross platforms (Windows, Unix, Linux, macOS) - unlike the pty-based version above (Unix, Linux, macOS).
// StdoutPipe only captures stdout, as opposed to pty which is an interactive pseudo-terminal
func ConnectCliStdoutPipe(t testing.TB, ctx context.Context, targetId string) ConnectCliOutput {
cmd := exec.CommandContext(ctx,
"boundary", "connect",
"-target-id", targetId,
"-format", "json",
)
// Capture stdout
stdoutPipe, err := cmd.StdoutPipe()
require.NoError(t, err)
// Start the command
err = cmd.Start()
require.NoError(t, err)
// Register cleanup to kill the process
t.Cleanup(func() {
if cmd.Process != nil {
_ = cmd.Process.Kill()
}
})
// Read the first line of output (JSON with connection info)
scanner := bufio.NewScanner(stdoutPipe)
var outputLine string
if scanner.Scan() {
outputLine = scanner.Text()
}
require.NoError(t, scanner.Err())
// Parse the JSON output
var connectOutput ConnectCliOutput
err = json.Unmarshal([]byte(outputLine), &connectOutput)
require.NoError(t, err, fmt.Sprintf("received output: %s", outputLine))
return connectOutput
}
+12 -3
View File
@@ -20,19 +20,28 @@ import (
// WaitForSessionCli waits for sessions to appear in the session list and returns the most recent session
// information. If the WithExpectedSessionsCount option is provided, expects to find specified amount of sessions
// or only 1 otherwise.
func WaitForSessionCli(t testing.TB, ctx context.Context, projectId string, opt ...SessionOption) *sessions.Session {
opts := getSessionOpts(opt...)
func WaitForSessionCli(t testing.TB, ctx context.Context, projectId string, sessionOpts []SessionOption, e2eOpts ...e2e.Option) *sessions.Session {
opts := getSessionOpts(sessionOpts...)
var options []e2e.Option
options = append(options,
e2e.WithArgs("sessions", "list", "-scope-id", projectId, "-include-terminated", "-format", "json"),
)
options = append(options, e2eOpts...)
t.Log("Waiting for session to appear...")
var session *sessions.Session
err := backoff.RetryNotify(
func() error {
// List sessions
output := e2e.RunCommand(ctx, "boundary",
e2e.WithArgs("sessions", "list", "-scope-id", projectId, "-include-terminated", "-format", "json"),
options...,
)
if output.Err != nil {
return backoff.Permanent(errors.New(string(output.Stderr)))
}
var sessionListResult sessions.SessionListResult
err := json.Unmarshal(output.Stdout, &sessionListResult)
if err != nil {
+83
View File
@@ -4,9 +4,12 @@
package e2e
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"io"
"os"
"os/exec"
"testing"
@@ -101,6 +104,86 @@ func RunCommand(ctx context.Context, command string, opt ...Option) *CommandResu
}
}
// RunCommandWithPipe runs a command and captures the first line of output from its stdout pipe.
// This implementation works on cross platforms (Windows, Unix, Linux, macOS) - unlike the pty-based version above (Unix, Linux, macOS).
// StdoutPipe only captures stdout, as opposed to pty which is an interactive pseudo-terminal
func RunCommandWithPipe(t testing.TB, ctx context.Context, command string, opt ...Option) (string, error) {
ctxCancel, cancel := context.WithCancel(ctx)
// channels to capture output and errors from goroutine
outputChan := make(chan string, 1)
errorChan := make(chan error, 1)
// Process options
opts := getOpts(opt...)
// Build command with args
var cmd *exec.Cmd
if opts.withArgs == nil {
cmd = exec.CommandContext(ctxCancel, command)
} else {
cmd = exec.CommandContext(ctxCancel, command, opts.withArgs...)
}
// Apply environment variables
if opts.withEnv != nil {
cmd.Env = os.Environ()
for k, v := range opts.withEnv {
cmd.Env = append(cmd.Env, k+"="+v)
}
}
// Run goroutine in background
go func() {
// Capture stdout via pipe
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
errorChan <- fmt.Errorf("failed to create standard out pipe: %w", err)
return
}
// Start the command (don't wait for it to finish)
err = cmd.Start()
if err != nil {
errorChan <- fmt.Errorf("failed to start command: %w", err)
return
}
// Read first line of output and send to channel
scanner := bufio.NewScanner(stdoutPipe)
if scanner.Scan() {
outputChan <- scanner.Text()
} else {
if err := scanner.Err(); err != nil {
errorChan <- fmt.Errorf("failed read output from pipe: %w", err)
} else {
errorChan <- errors.New("no output from command")
}
}
// Command continues running after this as proxy connection is intended to stay open
// We only need the first line of output
// Continuously drains the stdout pipe in the background to prevent the command from blocking
go func() {
_, _ = io.Copy(io.Discard, stdoutPipe)
}()
}()
// Cleanup kills the process
t.Cleanup(func() {
cancel()
})
// Return result of goroutine
select {
case output := <-outputChan:
return output, nil
case err := <-errorChan:
return "", err
case <-ctxCancel.Done():
return "", ctxCancel.Err()
}
}
// WithArgs is an option to RunCommand that allows the user to specify arguments
// for the provided command. This option can be used multiple times in one command.
//
@@ -156,7 +156,7 @@ func TestCliCreateAwsDynamicHostCatalogWithHostSet(t *testing.T) {
}()
t.Cleanup(cancel)
s := boundary.WaitForSessionCli(t, ctx, projectId)
s := boundary.WaitForSessionCli(t, ctx, projectId, nil)
boundary.WaitForSessionStatusCli(t, ctx, s.Id, session.StatusActive.String())
output = e2e.RunCommand(ctx, "boundary",
@@ -70,7 +70,7 @@ func TestCliBytesUpDownEmpty(t *testing.T) {
}()
t.Cleanup(cancel)
session := boundary.WaitForSessionCli(t, ctx, projectId)
session := boundary.WaitForSessionCli(t, ctx, projectId, nil)
assert.Equal(t, targetId, session.TargetId)
// Wait until bytes up and down is greater than 0
@@ -73,7 +73,7 @@ func TestCliBytesUpDownTransferData(t *testing.T) {
)
}()
t.Cleanup(cancel)
s := boundary.WaitForSessionCli(t, ctx, projectId)
s := boundary.WaitForSessionCli(t, ctx, projectId, nil)
boundary.WaitForSessionStatusCli(t, ctx, s.Id, session.StatusActive.String())
assert.Equal(t, targetId, s.TargetId)
@@ -68,7 +68,7 @@ func TestCliSessionCancelAdmin(t *testing.T) {
)
}()
t.Cleanup(cancel)
s := boundary.WaitForSessionCli(t, ctx, projectId)
s := boundary.WaitForSessionCli(t, ctx, projectId, nil)
boundary.WaitForSessionStatusCli(t, ctx, s.Id, session.StatusActive.String())
assert.Equal(t, targetId, s.TargetId)
assert.Equal(t, hostId, s.HostId)
@@ -145,7 +145,7 @@ func TestCliSessionCancelGroup(t *testing.T) {
)
}()
t.Cleanup(cancel)
s := boundary.WaitForSessionCli(t, ctx, projectId)
s := boundary.WaitForSessionCli(t, ctx, projectId, nil)
boundary.WaitForSessionStatusCli(t, ctx, s.Id, session.StatusActive.String())
assert.Equal(t, targetId, s.TargetId)
assert.Equal(t, hostId, s.HostId)
@@ -138,7 +138,7 @@ func TestCliSessionCancelUser(t *testing.T) {
)
}()
t.Cleanup(cancel)
s := boundary.WaitForSessionCli(t, ctx, projectId)
s := boundary.WaitForSessionCli(t, ctx, projectId, nil)
boundary.WaitForSessionStatusCli(t, ctx, s.Id, session.StatusActive.String())
assert.Equal(t, targetId, s.TargetId)
assert.Equal(t, hostId, s.HostId)
@@ -101,7 +101,7 @@ func TestCliSessionEndWhenHostSetIsDeleted(t *testing.T) {
)
}()
t.Cleanup(cancel)
s := boundary.WaitForSessionCli(t, ctx, projectId)
s := boundary.WaitForSessionCli(t, ctx, projectId, nil)
boundary.WaitForSessionStatusCli(t, ctx, s.Id, session.StatusActive.String())
assert.Equal(t, targetId, s.TargetId)
assert.Equal(t, hostId, s.HostId)
@@ -101,7 +101,7 @@ func TestCliSessionEndWhenHostIsDeleted(t *testing.T) {
)
}()
t.Cleanup(cancel)
s := boundary.WaitForSessionCli(t, ctx, projectId)
s := boundary.WaitForSessionCli(t, ctx, projectId, nil)
boundary.WaitForSessionStatusCli(t, ctx, s.Id, session.StatusActive.String())
assert.Equal(t, targetId, s.TargetId)
assert.Equal(t, hostId, s.HostId)
@@ -92,7 +92,7 @@ func TestCliSessionEndWhenProjectIsDeleted(t *testing.T) {
)
}()
t.Cleanup(cancel)
s := boundary.WaitForSessionCli(t, ctx, projectId)
s := boundary.WaitForSessionCli(t, ctx, projectId, nil)
boundary.WaitForSessionStatusCli(t, ctx, s.Id, session.StatusActive.String())
assert.Equal(t, targetId, s.TargetId)
@@ -101,7 +101,7 @@ func TestCliSessionEndWhenTargetIsDeleted(t *testing.T) {
)
}()
t.Cleanup(cancel)
s := boundary.WaitForSessionCli(t, ctx, projectId)
s := boundary.WaitForSessionCli(t, ctx, projectId, nil)
boundary.WaitForSessionStatusCli(t, ctx, s.Id, session.StatusActive.String())
assert.Equal(t, targetId, s.TargetId)
assert.Equal(t, hostId, s.HostId)
@@ -106,7 +106,7 @@ func TestCliSessionEndWhenUserIsDeleted(t *testing.T) {
)
}()
t.Cleanup(cancel)
s := boundary.WaitForSessionCli(t, ctx, projectId)
s := boundary.WaitForSessionCli(t, ctx, projectId, nil)
boundary.WaitForSessionStatusCli(t, ctx, s.Id, session.StatusActive.String())
assert.Equal(t, targetId, s.TargetId)
assert.Equal(t, hostId, s.HostId)
@@ -67,7 +67,7 @@ func TestCliTcpTargetConnectTargetWithConnectionLimits(t *testing.T) {
}()
t.Cleanup(cancel)
boundary.WaitForSessionCli(t, ctx, projectId)
boundary.WaitForSessionCli(t, ctx, projectId, nil)
// Start connections. Expect an error once the limit is reached
for i := 0; i <= sessionConnectionLimit; i++ {
@@ -75,7 +75,7 @@ func TestCliTcpTargetConnectExecLongLastingScript(t *testing.T) {
)
}()
t.Cleanup(cancel)
boundary.WaitForSessionCli(t, ctx, projectId)
boundary.WaitForSessionCli(t, ctx, projectId, nil)
t.Log("Copying script to host...")
output := e2e.RunCommand(ctx, "scp",
@@ -55,7 +55,7 @@ func TestCliTcpTargetConnectGoSsh(t *testing.T) {
}()
t.Cleanup(cancel)
boundary.WaitForSessionCli(t, ctx, projectId)
boundary.WaitForSessionCli(t, ctx, projectId, nil)
// Connect to the target using the go experimental ssh library
privateKeyRaw, err := os.ReadFile(c.TargetSshKeyPath)
@@ -79,7 +79,7 @@ Server: netcat-can-you-believe-it
_, _ = io.Copy(io.Discard, f) // Not checking error here since it will return an error on session close
}()
s := boundary.WaitForSessionCli(t, ctx, projectId)
s := boundary.WaitForSessionCli(t, ctx, projectId, nil)
boundary.WaitForSessionStatusCli(t, ctx, s.Id, session.StatusActive.String())
// Create http target and connect to it
@@ -57,7 +57,7 @@ func TestCliTcpTargetConnectListenPort(t *testing.T) {
}()
t.Cleanup(cancel)
s := boundary.WaitForSessionCli(t, ctx, projectId)
s := boundary.WaitForSessionCli(t, ctx, projectId, nil)
boundary.WaitForSessionStatusCli(t, ctx, s.Id, session.StatusPending.String())
t.Logf("Successfully connected to target with listen port %s", ListenPort)
@@ -118,7 +118,7 @@ func TestCliTcpTargetConnectDefaultClientPort(t *testing.T) {
}()
t.Cleanup(cancel)
s := boundary.WaitForSessionCli(t, ctx, projectId)
s := boundary.WaitForSessionCli(t, ctx, projectId, nil)
boundary.WaitForSessionStatusCli(t, ctx, s.Id, session.StatusPending.String())
t.Logf("Successfully connected to target with client port %d", DefaultClientPort)
@@ -65,7 +65,7 @@ func TestCliTcpTargetConnectTargetWithLocalhost(t *testing.T) {
}()
t.Cleanup(cancel)
boundary.WaitForSessionCli(t, ctx, projectId)
boundary.WaitForSessionCli(t, ctx, projectId, nil)
// Connect to target and print host's IP address
output := e2e.RunCommand(ctx, "ssh",
@@ -64,7 +64,7 @@ func TestCliTcpTargetConnectTargetAndScp(t *testing.T) {
}()
t.Cleanup(cancel)
boundary.WaitForSessionCli(t, ctx, projectId)
boundary.WaitForSessionCli(t, ctx, projectId, nil)
// Create file to scp
testDir := t.TempDir()
@@ -71,7 +71,7 @@ func TestCliTcpTargetConnectTargetWithSessionMaxSecondsTearDown(t *testing.T) {
)
}()
t.Cleanup(cancel)
s := boundary.WaitForSessionCli(t, ctx, projectId)
s := boundary.WaitForSessionCli(t, ctx, projectId, nil)
boundary.WaitForSessionStatusCli(t, ctx, s.Id, session.StatusActive.String())
// Check that session was closed once time limit is reached
@@ -140,7 +140,7 @@ func TestCliTcpTargetConnectTargetWithSessionMaxSecondsRejectNew(t *testing.T) {
)
}()
t.Cleanup(cancel)
boundary.WaitForSessionCli(t, ctx, projectId)
boundary.WaitForSessionCli(t, ctx, projectId, nil)
// Start connections. Expect an error once the time limit is reached
t.Log("Creating connections...")