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
31 changes: 31 additions & 0 deletions internal/ui/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"

tea "github.com/charmbracelet/bubbletea"
"github.com/localstack/lstk/internal/auth"
"github.com/localstack/lstk/internal/container"
"github.com/localstack/lstk/internal/output"
"github.com/localstack/lstk/internal/runtime"
Expand Down Expand Up @@ -75,6 +76,19 @@ func Run(parentCtx context.Context, runOpts RunOptions) error {
p.Send(runDoneMsg{})
return
}
// Resolve the auth token before any emulator-selection prompt so the user
// logs in first and only configures an emulator once they're authenticated.
// container.Start still calls GetToken as a safety net for non-interactive
// callers; once the token is in opts.AuthToken (or the keyring), it returns
// immediately.
if authErr := resolveAuthToken(ctx, sink, &runOpts); authErr != nil {
if errors.Is(authErr, context.Canceled) {
return
}
err = authErr
p.Send(runErrMsg{err: authErr})
return
}
if runOpts.NeedsEmulatorSelection {
newContainers, selErr := container.SelectEmulator(ctx, sink, runOpts.ConfigPath)
if selErr != nil {
Expand Down Expand Up @@ -115,6 +129,23 @@ func Run(parentCtx context.Context, runOpts RunOptions) error {
return nil
}

// resolveAuthToken ensures the user is authenticated before the start flow
// continues. On success, the resolved token is written to opts.StartOptions.AuthToken
// so container.Start short-circuits its own auth call.
func resolveAuthToken(ctx context.Context, sink output.Sink, opts *RunOptions) error {
tokenStorage, err := auth.NewTokenStorage(opts.StartOptions.ForceFileKeyring, opts.StartOptions.Logger)
if err != nil {
return err
}
a := auth.New(sink, opts.StartOptions.PlatformClient, tokenStorage, opts.StartOptions.AuthToken, opts.StartOptions.WebAppURL, true, "")
token, err := a.GetToken(ctx)
if err != nil {
return err
}
opts.StartOptions.AuthToken = token
return nil
}

func RunMessage(parentCtx context.Context, event output.MessageEvent) error {
return runWithTUI(parentCtx, withoutHeader(), func(ctx context.Context, sink output.Sink) error {
sink.Emit(event)
Expand Down
56 changes: 56 additions & 0 deletions test/integration/emulator_select_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,62 @@ func TestFirstRunShowsEmulatorSelectionPrompt(t *testing.T) {
<-outputCh
}

func TestFirstRunPromptsForLoginBeforeEmulatorSelection(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" {
t.Skip("PTY not supported on Windows")
}

mockServer := createMockAPIServer(t, "test-license-token", true)
defer mockServer.Close()

tmpHome := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(tmpHome, ".config"), 0755))
e := env.Environ(testEnvWithHome(tmpHome, tmpHome)).
Without(env.AuthToken).
With(env.APIEndpoint, mockServer.URL).
With(env.DisableEvents, "1")

// No config exists so this is a first run; no token means login fires before emulator selection.
configPath, _, err := runLstk(t, testContext(t), "", e, "config", "path")
require.NoError(t, err)
require.NoFileExists(t, configPath)

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

cmd := exec.CommandContext(ctx, binaryPath(), "start")
cmd.Env = e

ptmx, err := pty.Start(cmd)
require.NoError(t, err, "failed to start lstk in PTY")
defer func() { _ = ptmx.Close() }()

out := &syncBuffer{}
outputCh := make(chan struct{})
go func() {
_, _ = io.Copy(out, ptmx)
close(outputCh)
}()

require.Eventually(t, func() bool {
return bytes.Contains(out.Bytes(), []byte("Press any key when complete"))
}, 10*time.Second, 100*time.Millisecond, "auth prompt should appear on first run when no token is set")

assert.NotContains(t, out.String(), "Which emulator would you like to use?",
"emulator selection prompt must not appear before auth completes")

_, err = ptmx.Write([]byte("\r"))
require.NoError(t, err)

require.Eventually(t, func() bool {
return bytes.Contains(out.Bytes(), []byte("Which emulator would you like to use?"))
}, 10*time.Second, 100*time.Millisecond, "emulator selection prompt should appear after auth completes")

cancel()
<-outputCh
}

func TestFirstRunNonInteractiveEmitsDefaultEmulatorNote(t *testing.T) {
t.Parallel()
tmpHome := t.TempDir()
Expand Down