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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 1 addition & 13 deletions cmd/aws_devops_agent/confirm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,6 @@ func (a countingAgent) CreateAsset(context.Context, *devopsagent.CreateAssetInpu
return &devopsagent.CreateAssetOutput{Asset: &agenttypes.Asset{AssetId: aws.String("ki-1")}}, nil
}

func (a countingAgent) DeregisterService(context.Context, *devopsagent.DeregisterServiceInput, ...func(*devopsagent.Options)) (*devopsagent.DeregisterServiceOutput, error) {
a.writes.add("DeregisterService")

return &devopsagent.DeregisterServiceOutput{}, nil
}

func (a countingAgent) DisassociateService(context.Context, *devopsagent.DisassociateServiceInput, ...func(*devopsagent.Options)) (*devopsagent.DisassociateServiceOutput, error) {
a.writes.add("DisassociateService")

return &devopsagent.DisassociateServiceOutput{}, nil
}

func (a countingAgent) EnableOperatorApp(context.Context, *devopsagent.EnableOperatorAppInput, ...func(*devopsagent.Options)) (*devopsagent.EnableOperatorAppOutput, error) {
a.writes.add("EnableOperatorApp")

Expand Down Expand Up @@ -173,7 +161,7 @@ func setupTestCmd(t *testing.T, stdin string) (*cobra.Command, *bytes.Buffer) {
}

func newSpaceOptions() awsdevops.SetupOptions {
return awsdevops.SetupOptions{AgentSpaceName: "ld-devops-agent", AuthFlow: "iam", LDAccessToken: "api-token"}
return awsdevops.SetupOptions{AgentSpaceName: "ld-devops-agent", AuthFlow: "iam"}
}

// A json run in a terminal asks too, because the question goes to stderr.
Expand Down
19 changes: 0 additions & 19 deletions cmd/aws_devops_agent/flags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,9 @@ import (

"github.com/aws/aws-sdk-go-v2/aws"
agenttypes "github.com/aws/aws-sdk-go-v2/service/devopsagent/types"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/launchdarkly/ldcli/cmd/cliflags"
"github.com/launchdarkly/ldcli/internal/analytics"
)

Expand Down Expand Up @@ -55,7 +53,6 @@ func spaceIDRun(t *testing.T, flags map[string]string) []string {
})
opts, err := setupOptions(cmd)
require.NoError(t, err)
opts.LDAccessToken = "api-token"

require.NoError(t, setupWith(cmd, clients, opts, true, false))

Expand Down Expand Up @@ -154,19 +151,3 @@ func TestSetupOptionsRecordsAuthFlow(t *testing.T) {
require.NoError(t, err)
assert.True(t, opts.FullSetup)
}

// Setup does not ask for the replacement token, so the flag needs one.
func TestReplaceMCPTokenNeedsAToken(t *testing.T) {
viper.Set(cliflags.AccessTokenFlag, "")
t.Cleanup(func() { viper.Set(cliflags.AccessTokenFlag, "") })
cmd := NewSetupCmd(nil)
require.NoError(t, cmd.Flags().Set(replaceMCPTokenFlag, "true"))

_, err := setupOptions(cmd)
assert.EqualError(t, err, "--replace-mcp-token needs a token. Pass --access-token")

viper.Set(cliflags.AccessTokenFlag, "api-token")
opts, err := setupOptions(cmd)
require.NoError(t, err)
assert.True(t, opts.ReplaceMCPToken)
}
11 changes: 0 additions & 11 deletions cmd/aws_devops_agent/main_test.go
Original file line number Diff line number Diff line change
@@ -1,27 +1,16 @@
package awsdevopsagent

import (
"context"
"fmt"
"os"
"strconv"
"strings"
"testing"

"github.com/spf13/cobra"

"github.com/launchdarkly/ldcli/internal/awsdevops"
)

// Never call the real LaunchDarkly API.
func TestMain(m *testing.M) {
awsdevops.CheckToken = func(ctx context.Context, baseURI, token string) error {
if baseURI == "" {
return nil
}

return awsdevops.ValidateAccessToken(ctx, baseURI, token)
}
newChooser = lineChooser
os.Exit(m.Run())
}
Expand Down
148 changes: 1 addition & 147 deletions cmd/aws_devops_agent/plan_print_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,68 +78,6 @@ func TestSetupCanceledCreatesNoSkill(t *testing.T) {
assert.NotContains(t, w.calls, "CreateAsset")
}

func noTokenOptions() awsdevops.SetupOptions {
opts := newSpaceOptions()
opts.LDAccessToken = ""

return opts
}

// The token prompt comes before the plan question.
func TestTokenPromptAsksWhenInteractive(t *testing.T) {
cmd, _ := setupTestCmd(t, "api-token\ny\n")
clients, w := countingClients()

require.NoError(t, setupWith(cmd, clients, noTokenOptions(), true, true))

assert.Contains(t, w.calls, "RegisterService")
assert.Less(t, strings.Index(stderrOf(cmd), "Paste the token"), strings.Index(stderrOf(cmd), "Setup plan:"))
// The plan is built again with the token, so it is current.
assert.Contains(t, stderrOf(cmd), "Register a new MCP server for this agent space at")
assert.NotContains(t, stderrOf(cmd), "Ask for a token")
}

// A rejected token stops setup before any AWS write.
func TestSetupChecksAPastedTokenFirst(t *testing.T) {
saved := awsdevops.CheckToken
t.Cleanup(func() { awsdevops.CheckToken = saved })
awsdevops.CheckToken = func(context.Context, string, string) error { return awsdevops.ErrInvalidAccessToken }
cmd, _ := setupTestCmd(t, "expired-token\ny\n")
clients, w := countingClients()

err := setupWith(cmd, clients, noTokenOptions(), true, true)

require.ErrorIs(t, err, awsdevops.ErrInvalidAccessToken)
assert.Empty(t, w.calls)
assert.NotContains(t, stderrOf(cmd), "Setup plan:")
}

// Enter skips the MCP server, and the end names the next step.
func TestSetupSkippedTokenNamesTheNextStep(t *testing.T) {
cmd, _ := setupTestCmd(t, "\ny\n")
clients, w := countingClients()

require.NoError(t, setupWith(cmd, clients, noTokenOptions(), true, true))

assert.NotContains(t, w.calls, "RegisterService")
assert.Contains(t, stderrOf(cmd), "Skip the MCP server of this agent space")
assert.NotContains(t, stderrOf(cmd), "Ask for a token")
assert.Contains(t, stdoutOf(cmd), "The LaunchDarkly MCP server is not connected")
}

// Without a token, a connect run has nothing to do.
func TestSetupConnectWithoutATokenStops(t *testing.T) {
cmd, _ := setupTestCmd(t, "2\n\n")
clients, w := twoSimilarSpaces()

err := setupWith(cmd, clients, noTokenOptions(), true, true)

require.Error(t, err)
assert.Contains(t, err.Error(), "nothing to do on agent space LaunchDarkly (space-2)")
assert.Empty(t, w.calls)
assert.NotContains(t, stderrOf(cmd), "Continue?")
}

// Progress goes to stderr, so a json run shows it, and stdout stays JSON.
func TestSetupJSONShowsProgressOnStderr(t *testing.T) {
cmd, _ := setupTestCmd(t, "")
Expand All @@ -160,35 +98,6 @@ func TestPlanNamesTheAWSDefaultProfile(t *testing.T) {
assert.Equal(t, "corp", profileLabel(cmd))
}

// No token, no prompt: stop first.
func TestSetupNeedsATokenItCannotAsk(t *testing.T) {
cases := map[string]struct {
plaintext, interactive, yes bool
}{
"no terminal": {plaintext: true},
"json": {},
"yes in terminal": {plaintext: true, interactive: true, yes: true},
}
for name, c := range cases {
t.Run(name, func(t *testing.T) {
cmd, out := setupTestCmd(t, "")
cmd.SetIn(noReadStdin{t})
if c.yes {
require.NoError(t, cmd.Flags().Set(yesFlag, "true"))
}
clients, w := countingClients()

err := setupWith(cmd, clients, noTokenOptions(), c.plaintext, c.interactive)

require.Error(t, err)
assert.Contains(t, err.Error(), "--access-token")
assert.Contains(t, err.Error(), awsdevops.AccessTokenURL(""))
assert.Empty(t, w.calls)
assert.NotContains(t, out.String(), "Setup plan")
})
}
}

// The server of the agent space needs no token.
func TestSetupWithoutTokenReusesServer(t *testing.T) {
cmd, _ := setupTestCmd(t, "")
Expand All @@ -205,7 +114,7 @@ func TestSetupWithoutTokenReusesServer(t *testing.T) {
}}
clients.Agent = agent

opts := noTokenOptions()
opts := newSpaceOptions()
opts.AgentSpaceName, opts.AgentSpaceID = "", "space-1"

require.NoError(t, setupWith(cmd, clients, opts, true, false))
Expand Down Expand Up @@ -381,7 +290,6 @@ func TestSetupChosenSpaceWithFullSetup(t *testing.T) {
clients, w := twoSimilarSpaces()
opts, err := setupOptions(cmd)
require.NoError(t, err)
opts.LDAccessToken = "api-token"

require.NoError(t, setupWith(cmd, clients, opts, true, true))

Expand All @@ -400,7 +308,6 @@ func TestSetupChosenSpaceWithOnlyAuthFlowStops(t *testing.T) {
clients, w := twoSimilarSpaces()
opts, err := setupOptions(cmd)
require.NoError(t, err)
opts.LDAccessToken = "api-token"

err = setupWith(cmd, clients, opts, true, true)

Expand Down Expand Up @@ -462,33 +369,6 @@ func TestSetupFailureNamesTheNewSpace(t *testing.T) {
assert.Contains(t, err.Error(), "To use it, run setup again with --agent-space-id space-new")
}

// A server that the space gets between the two plans is reused, and setup
// says that it does not use the pasted token.
func TestSetupSaysWhenTheSecondPlanReusesAServer(t *testing.T) {
cmd, _ := setupTestCmd(t, "api-token\n")
clients, _ := countingClients(agenttypes.AgentSpace{AgentSpaceId: aws.String("space-1"), Name: aws.String("platform")})
agent := clients.Agent.(countingAgent)
agent.associations = []agenttypes.Association{{AssociationId: aws.String("assoc-7"), ServiceId: aws.String("mcp-7")}}
agent.services = []agenttypes.RegisteredService{{
ServiceId: aws.String("mcp-7"),
ServiceType: agenttypes.ServiceMcpServer,
AdditionalServiceDetails: &agenttypes.AdditionalServiceDetailsMemberMcpserver{
Value: agenttypes.RegisteredMCPServerDetails{Name: aws.String("Prod LD"), Endpoint: aws.String(awsdevops.MCPServerEndpoint)},
},
}}
clients.Agent = agent
opts := noTokenOptions()
opts.AgentSpaceName, opts.AgentSpaceID = "", "space-1"
first := awsdevops.SetupPlan{MCP: awsdevops.MCPStep{Action: awsdevops.MCPAsk}}

plan, err := planWithToken(cmd, clients, &opts, first, true)

require.NoError(t, err)
assert.Equal(t, awsdevops.MCPReuse, plan.MCP.Action)
assert.Contains(t, stderrOf(cmd), "MCP server Prod LD (mcp-7) was registered after the first plan")
assert.Contains(t, stderrOf(cmd), "--access-token and --replace-mcp-token")
}

type failingSkillAgent struct{ countingAgent }

func (failingSkillAgent) CreateAsset(context.Context, *devopsagent.CreateAssetInput, ...func(*devopsagent.Options)) (*devopsagent.CreateAssetOutput, error) {
Expand Down Expand Up @@ -525,29 +405,3 @@ func TestSetupJSONWarnsOnStderrWhenSkillFails(t *testing.T) {
assert.Contains(t, stdoutOf(cmd), `"skillAction": "failed"`)
assert.Contains(t, errOut.String(), "Warning: unable to create skill "+awsdevops.ExperimentSkillName)
}

// Without a name from AWS, the warning gives only the ID, which identifies
// the server, and does not guess a name.
func TestSetupNamesANamelessServerByItsID(t *testing.T) {
cmd, _ := setupTestCmd(t, "api-token\n")
clients, _ := countingClients(agenttypes.AgentSpace{AgentSpaceId: aws.String("space-1"), Name: aws.String("platform")})
agent := clients.Agent.(countingAgent)
agent.associations = []agenttypes.Association{{AssociationId: aws.String("assoc-7"), ServiceId: aws.String("mcp-7")}}
agent.services = []agenttypes.RegisteredService{{
ServiceId: aws.String("mcp-7"),
ServiceType: agenttypes.ServiceMcpServer,
AdditionalServiceDetails: &agenttypes.AdditionalServiceDetailsMemberMcpserver{
Value: agenttypes.RegisteredMCPServerDetails{Endpoint: aws.String(awsdevops.MCPServerEndpoint)},
},
}}
clients.Agent = agent
opts := noTokenOptions()
opts.AgentSpaceName, opts.AgentSpaceID = "", "space-1"
first := awsdevops.SetupPlan{MCP: awsdevops.MCPStep{Action: awsdevops.MCPAsk}}

plan, err := planWithToken(cmd, clients, &opts, first, true)

require.NoError(t, err)
require.Equal(t, awsdevops.MCPReuse, plan.MCP.Action)
assert.Contains(t, stderrOf(cmd), "MCP server mcp-7 was registered after the first plan")
}
16 changes: 0 additions & 16 deletions cmd/aws_devops_agent/prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,22 +68,6 @@ func readYes(in io.Reader) bool {
return false
}

// readSecret reads one line without echoing it, so a pasted credential stays
// off the screen.
func readSecret(in io.Reader) string {
file, ok := in.(*os.File)
if !ok || !term.IsTerminal(int(file.Fd())) {
return readLine(in)
}

typed, err := term.ReadPassword(int(file.Fd()))
if err != nil {
return ""
}

return strings.TrimSpace(string(typed))
}

// canPrompt reports a terminal on stdin and stderr. The prompts go to stderr,
// so a run with stderr sent to a file does not wait for an answer.
func canPrompt() bool {
Expand Down
9 changes: 0 additions & 9 deletions cmd/aws_devops_agent/prompt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,12 @@ package awsdevopsagent

import (
"os"
"strings"
"testing"

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

func TestReadSecretTrimsThePastedToken(t *testing.T) {
assert.Equal(t, "api-token", readSecret(strings.NewReader("api-token\n")))
}

func TestReadSecretReadsAnEmptyLine(t *testing.T) {
assert.Empty(t, readSecret(strings.NewReader("\n")))
}

// A file is not wrapped, so each prompt must leave the next lines unread.
func TestReadLineLeavesLaterLinesInAFile(t *testing.T) {
r, w, err := os.Pipe()
Expand Down
8 changes: 0 additions & 8 deletions cmd/aws_devops_agent/select_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,6 @@ func (s stubAgent) CreateAgentSpace(context.Context, *devopsagent.CreateAgentSpa
return nil, nil
}

func (s stubAgent) DeregisterService(context.Context, *devopsagent.DeregisterServiceInput, ...func(*devopsagent.Options)) (*devopsagent.DeregisterServiceOutput, error) {
return nil, nil
}

func (s stubAgent) DisassociateService(context.Context, *devopsagent.DisassociateServiceInput, ...func(*devopsagent.Options)) (*devopsagent.DisassociateServiceOutput, error) {
return nil, nil
}

func (s stubAgent) GetOperatorApp(context.Context, *devopsagent.GetOperatorAppInput, ...func(*devopsagent.Options)) (*devopsagent.GetOperatorAppOutput, error) {
return nil, &agenttypes.ResourceNotFoundException{}
}
Expand Down
Loading
Loading