Skip to content

Commit dc702b5

Browse files
committed
Refactor & more tests
1 parent bc00355 commit dc702b5

3 files changed

Lines changed: 131 additions & 27 deletions

File tree

cmd/root.go

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,22 +32,15 @@ import (
3232
func NewRootCmd(cfg *env.Env, tel *telemetry.Client, logger log.Logger) *cobra.Command {
3333
var firstRun bool
3434
root := &cobra.Command{
35-
Use: "lstk",
36-
Short: "LocalStack CLI",
37-
Long: "lstk is the command-line interface for LocalStack.",
38-
PreRunE: func(cmd *cobra.Command, _ []string) error {
39-
path, err := cmd.Flags().GetString("config")
35+
Use: "lstk",
36+
Short: "LocalStack CLI",
37+
Long: "lstk is the command-line interface for LocalStack.",
38+
PreRunE: initConfigCapturingFirstRun(&firstRun),
39+
RunE: func(cmd *cobra.Command, args []string) error {
40+
emulator, err := cmd.Flags().GetString("emulator")
4041
if err != nil {
4142
return err
4243
}
43-
if path != "" {
44-
return config.InitFromPath(path)
45-
}
46-
firstRun, err = config.Init()
47-
return err
48-
},
49-
RunE: func(cmd *cobra.Command, args []string) error {
50-
emulator, _ := cmd.Flags().GetString("emulator")
5144
rt, err := runtime.NewDockerRuntime(cfg.DockerHost)
5245
if err != nil {
5346
return err
@@ -355,3 +348,19 @@ func initConfig(cmd *cobra.Command, _ []string) error {
355348
_, err = config.Init()
356349
return err
357350
}
351+
352+
// initConfigCapturingFirstRun returns a PreRunE that initialises config and
353+
// writes whether this is the first run into the provided pointer.
354+
func initConfigCapturingFirstRun(firstRun *bool) func(*cobra.Command, []string) error {
355+
return func(cmd *cobra.Command, _ []string) error {
356+
path, err := cmd.Flags().GetString("config")
357+
if err != nil {
358+
return err
359+
}
360+
if path != "" {
361+
return config.InitFromPath(path)
362+
}
363+
*firstRun, err = config.Init()
364+
return err
365+
}
366+
}

cmd/start.go

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package cmd
22

33
import (
4-
"github.com/localstack/lstk/internal/config"
54
"github.com/localstack/lstk/internal/env"
65
"github.com/localstack/lstk/internal/log"
76
"github.com/localstack/lstk/internal/runtime"
@@ -12,22 +11,15 @@ import (
1211
func newStartCmd(cfg *env.Env, tel *telemetry.Client, logger log.Logger) *cobra.Command {
1312
var firstRun bool
1413
cmd := &cobra.Command{
15-
Use: "start",
16-
Short: "Start emulator",
17-
Long: "Start emulator and services.",
18-
PreRunE: func(c *cobra.Command, _ []string) error {
19-
path, err := c.Flags().GetString("config")
14+
Use: "start",
15+
Short: "Start emulator",
16+
Long: "Start emulator and services.",
17+
PreRunE: initConfigCapturingFirstRun(&firstRun),
18+
RunE: func(c *cobra.Command, args []string) error {
19+
emulator, err := c.Flags().GetString("emulator")
2020
if err != nil {
2121
return err
2222
}
23-
if path != "" {
24-
return config.InitFromPath(path)
25-
}
26-
firstRun, err = config.Init()
27-
return err
28-
},
29-
RunE: func(c *cobra.Command, args []string) error {
30-
emulator, _ := c.Flags().GetString("emulator")
3123
rt, err := runtime.NewDockerRuntime(cfg.DockerHost)
3224
if err != nil {
3325
return err
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package integration_test
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"io"
7+
"os"
8+
"os/exec"
9+
"path/filepath"
10+
"runtime"
11+
"testing"
12+
"time"
13+
14+
"github.com/creack/pty"
15+
"github.com/localstack/lstk/test/integration/env"
16+
"github.com/stretchr/testify/assert"
17+
"github.com/stretchr/testify/require"
18+
)
19+
20+
// freshConfigEnv returns an Environ pointing HOME and USERPROFILE at a new
21+
// temp directory (with .config/ pre-created) so lstk uses an isolated config
22+
// dir on both Unix (HOME) and Windows (USERPROFILE).
23+
func freshConfigEnv(t *testing.T) (env.Environ, string) {
24+
t.Helper()
25+
tmpHome := t.TempDir()
26+
require.NoError(t, os.MkdirAll(filepath.Join(tmpHome, ".config"), 0755))
27+
return env.Without(env.Home).
28+
With(env.Home, tmpHome).
29+
With("USERPROFILE", tmpHome), tmpHome
30+
}
31+
32+
func TestEmulatorFlagSwitchesConfigToSnowflake(t *testing.T) {
33+
// config.SwitchEmulator writes the file before container.Start is called,
34+
// so we can verify the switch even when the process ultimately fails (no Docker).
35+
e, tmpHome := freshConfigEnv(t)
36+
37+
configDir := filepath.Join(tmpHome, ".config", "lstk")
38+
require.NoError(t, os.MkdirAll(configDir, 0755))
39+
configPath := filepath.Join(configDir, "config.toml")
40+
require.NoError(t, os.WriteFile(configPath, []byte(`[[containers]]
41+
type = "aws"
42+
tag = "latest"
43+
port = "4566"
44+
`), 0644))
45+
46+
ctx := testContext(t)
47+
// The process will fail at container.Start (no Docker / no real auth), but the
48+
// config switch happens earlier so the file should already be updated.
49+
_, _, _ = runLstk(t, ctx, "", e.With(env.AuthToken, "test-token"), "--emulator", "snowflake", "--non-interactive")
50+
51+
got, err := os.ReadFile(configPath)
52+
require.NoError(t, err, "config file should still exist after the run")
53+
assert.Contains(t, string(got), `type = "snowflake"`, "config should be switched to snowflake")
54+
assert.NotContains(t, string(got), "\n[[containers]]\ntype = \"aws\"", "original aws block should be commented out")
55+
}
56+
57+
func TestFirstRunShowsEmulatorSelectionPrompt(t *testing.T) {
58+
if runtime.GOOS == "windows" {
59+
t.Skip("PTY not supported on Windows")
60+
}
61+
62+
// Fresh HOME with no lstk config — triggers first-run behaviour.
63+
e, tmpHome := freshConfigEnv(t)
64+
65+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
66+
defer cancel()
67+
68+
cmd := exec.CommandContext(ctx, binaryPath(), "start")
69+
cmd.Env = e.With(env.AuthToken, "test-token")
70+
71+
ptmx, err := pty.Start(cmd)
72+
require.NoError(t, err, "failed to start lstk in PTY")
73+
defer func() { _ = ptmx.Close() }()
74+
75+
out := &syncBuffer{}
76+
outputCh := make(chan struct{})
77+
go func() {
78+
_, _ = io.Copy(out, ptmx)
79+
close(outputCh)
80+
}()
81+
82+
require.Eventually(t, func() bool {
83+
return bytes.Contains(out.Bytes(), []byte("Which emulator would you like to use?"))
84+
}, 10*time.Second, 100*time.Millisecond, "emulator selection prompt should appear on first run")
85+
86+
// Choose Snowflake.
87+
_, err = ptmx.Write([]byte("s"))
88+
require.NoError(t, err)
89+
90+
require.Eventually(t, func() bool {
91+
return bytes.Contains(out.Bytes(), []byte("Snowflake emulator selected"))
92+
}, 5*time.Second, 100*time.Millisecond, "confirmation should appear after selection")
93+
94+
// Config is written before the confirmation is emitted, so it is safe to read now.
95+
configPath := filepath.Join(tmpHome, ".config", "lstk", "config.toml")
96+
got, err := os.ReadFile(configPath)
97+
require.NoError(t, err, "config file should have been created on first run")
98+
assert.Contains(t, string(got), `type = "snowflake"`)
99+
100+
// Kill the process — we do not need the emulator to actually start.
101+
cancel()
102+
<-outputCh
103+
}

0 commit comments

Comments
 (0)