diff --git a/cmd/login.go b/cmd/login.go index 5af61b7..c1c259c 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -3,6 +3,7 @@ package cmd import ( "fmt" + "github.com/localstack/lstk/internal/api" "github.com/localstack/lstk/internal/auth" "github.com/spf13/cobra" ) @@ -12,7 +13,8 @@ var loginCmd = &cobra.Command{ Short: "Authenticate with LocalStack", Long: "Authenticate with LocalStack and store credentials in system keyring", RunE: func(cmd *cobra.Command, args []string) error { - a, err := auth.New() + platformClient := api.NewPlatformClient() + a, err := auth.New(platformClient) if err != nil { return fmt.Errorf("failed to initialize auth: %w", err) } diff --git a/cmd/logout.go b/cmd/logout.go index a5d6d23..83a9733 100644 --- a/cmd/logout.go +++ b/cmd/logout.go @@ -3,6 +3,7 @@ package cmd import ( "fmt" + "github.com/localstack/lstk/internal/api" "github.com/localstack/lstk/internal/auth" "github.com/spf13/cobra" ) @@ -11,7 +12,8 @@ var logoutCmd = &cobra.Command{ Use: "logout", Short: "Remove stored authentication token", RunE: func(cmd *cobra.Command, args []string) error { - a, err := auth.New() + platformClient := api.NewPlatformClient() + a, err := auth.New(platformClient) if err != nil { return fmt.Errorf("failed to initialize auth: %w", err) } diff --git a/cmd/root.go b/cmd/root.go index 9b56503..6d4fa51 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -5,6 +5,7 @@ import ( "fmt" "os" + "github.com/localstack/lstk/internal/api" "github.com/localstack/lstk/internal/config" "github.com/localstack/lstk/internal/container" "github.com/localstack/lstk/internal/runtime" @@ -25,11 +26,13 @@ var rootCmd = &cobra.Command{ os.Exit(1) } + platformClient := api.NewPlatformClient() + onProgress := func(msg string) { fmt.Println(msg) } - if err := container.Start(cmd.Context(), rt, onProgress); err != nil { + if err := container.Start(cmd.Context(), rt, platformClient, onProgress); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } diff --git a/cmd/start.go b/cmd/start.go index 3514d90..1718135 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -4,6 +4,7 @@ import ( "fmt" "os" + "github.com/localstack/lstk/internal/api" "github.com/localstack/lstk/internal/container" "github.com/localstack/lstk/internal/runtime" "github.com/spf13/cobra" @@ -20,11 +21,13 @@ var startCmd = &cobra.Command{ os.Exit(1) } + platformClient := api.NewPlatformClient() + onProgress := func(msg string) { fmt.Println(msg) } - if err := container.Start(cmd.Context(), rt, onProgress); err != nil { + if err := container.Start(cmd.Context(), rt, platformClient, onProgress); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } diff --git a/env.example b/env.example index 51acb92..53c7bba 100644 --- a/env.example +++ b/env.example @@ -2,3 +2,6 @@ export LOCALSTACK_AUTH_TOKEN=ls-... # Force file-based keyring backend (instead of system keychain) # export KEYRING=file +# +export LOCALSTACK_API_ENDPOINT=https://api.staging.aws.localstack.cloud +export LOCALSTACK_WEB_APP_URL=https://app.staging.aws.localstack.cloud diff --git a/internal/api/client.go b/internal/api/client.go index d934ab7..e352068 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -16,6 +16,7 @@ type PlatformAPI interface { CheckAuthRequestConfirmed(ctx context.Context, id, exchangeToken string) (bool, error) ExchangeAuthRequest(ctx context.Context, id, exchangeToken string) (string, error) GetLicenseToken(ctx context.Context, bearerToken string) (string, error) + GetLicense(ctx context.Context, req *LicenseRequest) error } type AuthRequest struct { @@ -37,6 +38,27 @@ type licenseTokenResponse struct { Token string `json:"token"` } +type LicenseRequest struct { + Product ProductInfo `json:"product"` + Credentials CredentialsInfo `json:"credentials"` + Machine MachineInfo `json:"machine"` +} + +type ProductInfo struct { + Name string `json:"name"` + Version string `json:"version"` +} + +type CredentialsInfo struct { + Token string `json:"token"` +} + +type MachineInfo struct { + Hostname string `json:"hostname,omitempty"` + Platform string `json:"platform,omitempty"` + PlatformRelease string `json:"platform_release,omitempty"` +} + type PlatformClient struct { baseURL string httpClient *http.Client @@ -173,3 +195,37 @@ func (c *PlatformClient) GetLicenseToken(ctx context.Context, bearerToken string return tokenResp.Token, nil } + +func (c *PlatformClient) GetLicense(ctx context.Context, licReq *LicenseRequest) error { + body, err := json.Marshal(licReq) + if err != nil { + return fmt.Errorf("failed to marshal request: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/v1/license/request", bytes.NewReader(body)) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + + resp, err := c.httpClient.Do(req) + if err != nil { + return fmt.Errorf("failed to request license: %w", err) + } + defer func() { + if err := resp.Body.Close(); err != nil { + log.Printf("failed to close response body: %v", err) + } + }() + + switch resp.StatusCode { + case http.StatusOK: + return nil + case http.StatusBadRequest: + return fmt.Errorf("license validation failed: invalid token format, missing license assignment, or missing subscription") + case http.StatusForbidden: + return fmt.Errorf("license validation failed: invalid, inactive, or expired authentication token or subscription") + default: + return fmt.Errorf("license request failed with status %d", resp.StatusCode) + } +} diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 4345112..7246f97 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -7,6 +7,7 @@ import ( "os" "github.com/99designs/keyring" + "github.com/localstack/lstk/internal/api" ) type Auth struct { @@ -14,14 +15,14 @@ type Auth struct { browserLogin LoginProvider } -func New() (*Auth, error) { +func New(platformClient api.PlatformAPI) (*Auth, error) { kr, err := newSystemKeyring() if err != nil { return nil, err } return &Auth{ keyring: kr, - browserLogin: newBrowserLogin(), + browserLogin: newBrowserLogin(platformClient), }, nil } diff --git a/internal/auth/login.go b/internal/auth/login.go index 471a28f..51547e3 100644 --- a/internal/auth/login.go +++ b/internal/auth/login.go @@ -26,9 +26,9 @@ type browserLogin struct { platformClient api.PlatformAPI } -func newBrowserLogin() *browserLogin { +func newBrowserLogin(platformClient api.PlatformAPI) *browserLogin { return &browserLogin{ - platformClient: api.NewPlatformClient(), + platformClient: platformClient, } } diff --git a/internal/config/config.go b/internal/config/config.go index daba4e6..04ef4cc 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -14,10 +14,12 @@ const ( EmulatorAWS EmulatorType = "aws" EmulatorSnowflake EmulatorType = "snowflake" EmulatorAzure EmulatorType = "azure" + + dockerRegistry = "localstack" ) var emulatorImages = map[EmulatorType]string{ - EmulatorAWS: "localstack/localstack-pro", + EmulatorAWS: "localstack-pro", } var emulatorHealthPaths = map[EmulatorType]string{ @@ -36,15 +38,15 @@ type ContainerConfig struct { } func (c *ContainerConfig) Image() (string, error) { - baseImage, ok := emulatorImages[c.Type] - if !ok { - return "", fmt.Errorf("%s emulator not supported yet by lstk", c.Type) + productName, err := c.ProductName() + if err != nil { + return "", err } tag := c.Tag if tag == "" { tag = "latest" } - return fmt.Sprintf("%s:%s", baseImage, tag), nil + return fmt.Sprintf("%s/%s:%s", dockerRegistry, productName, tag), nil } // Name returns the container name: "localstack-{type}" or "localstack-{type}-{tag}" if tag != latest @@ -64,6 +66,14 @@ func (c *ContainerConfig) HealthPath() (string, error) { return path, nil } +func (c *ContainerConfig) ProductName() (string, error) { + productName, ok := emulatorImages[c.Type] + if !ok { + return "", fmt.Errorf("%s emulator not supported yet by lstk", c.Type) + } + return productName, nil +} + func ConfigDir() (string, error) { configHome, err := os.UserConfigDir() if err != nil { diff --git a/internal/container/start.go b/internal/container/start.go index 73df1d2..f5e95d0 100644 --- a/internal/container/start.go +++ b/internal/container/start.go @@ -5,16 +5,19 @@ import ( "fmt" "log" "net/http" + "os" + stdruntime "runtime" "time" "github.com/containerd/errdefs" + "github.com/localstack/lstk/internal/api" "github.com/localstack/lstk/internal/auth" "github.com/localstack/lstk/internal/config" "github.com/localstack/lstk/internal/runtime" ) -func Start(ctx context.Context, rt runtime.Runtime, onProgress func(string)) error { - a, err := auth.New() +func Start(ctx context.Context, rt runtime.Runtime, platformClient api.PlatformAPI, onProgress func(string)) error { + a, err := auth.New(platformClient) if err != nil { return fmt.Errorf("failed to initialize auth: %w", err) } @@ -50,6 +53,7 @@ func Start(ctx context.Context, rt runtime.Runtime, onProgress func(string)) err } } + // Pull all images first for _, config := range containers { // Remove any existing stopped container with the same name if err := rt.Remove(ctx, config.Name); err != nil && !errdefs.IsNotFound(err) { @@ -70,7 +74,18 @@ func Start(ctx context.Context, rt runtime.Runtime, onProgress func(string)) err if err := rt.PullImage(ctx, config.Image, progress); err != nil { return fmt.Errorf("failed to pull image %s: %w", config.Image, err) } + } + + // TODO validate license for tag "latest" without resolving the actual image version, + // and avoid pulling all images first + for i, c := range cfg.Containers { + if err := validateLicense(ctx, rt, platformClient, containers[i], &c, token, onProgress); err != nil { + return err + } + } + // Start containers + for _, config := range containers { onProgress(fmt.Sprintf("Starting %s...", config.Name)) containerID, err := rt.Start(ctx, config) if err != nil { @@ -89,6 +104,45 @@ func Start(ctx context.Context, rt runtime.Runtime, onProgress func(string)) err return nil } +func validateLicense(ctx context.Context, rt runtime.Runtime, platformClient api.PlatformAPI, containerConfig runtime.ContainerConfig, cfgContainer *config.ContainerConfig, token string, onProgress func(string)) error { + version := cfgContainer.Tag + if version == "" || version == "latest" { + actualVersion, err := rt.GetImageVersion(ctx, containerConfig.Image) + if err != nil { + return fmt.Errorf("could not resolve version from image %s: %w", containerConfig.Image, err) + } + version = actualVersion + } + + productName, err := cfgContainer.ProductName() + if err != nil { + return err + } + onProgress(fmt.Sprintf("Validating license for %s:%s...", productName, version)) + + hostname, _ := os.Hostname() + licenseReq := &api.LicenseRequest{ + Product: api.ProductInfo{ + Name: productName, + Version: version, + }, + Credentials: api.CredentialsInfo{ + Token: token, + }, + Machine: api.MachineInfo{ + Hostname: hostname, + Platform: stdruntime.GOOS, + PlatformRelease: stdruntime.GOARCH, + }, + } + + if err := platformClient.GetLicense(ctx, licenseReq); err != nil { + return fmt.Errorf("license validation failed for %s:%s: %w", productName, version, err) + } + + return nil +} + // awaitStartup polls until one of two outcomes: // - Success: health endpoint returns 200 (license is valid, LocalStack is ready) // - Failure: container stops running (e.g., license activation failed), returns error with container logs diff --git a/internal/runtime/docker.go b/internal/runtime/docker.go index 5fa871e..01ce5bb 100644 --- a/internal/runtime/docker.go +++ b/internal/runtime/docker.go @@ -7,6 +7,7 @@ import ( "io" "log" "strconv" + "strings" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/image" @@ -148,3 +149,21 @@ func (d *DockerRuntime) Logs(ctx context.Context, containerID string, tail int) return string(logs), nil } + +func (d *DockerRuntime) GetImageVersion(ctx context.Context, imageName string) (string, error) { + inspect, err := d.client.ImageInspect(ctx, imageName) + if err != nil { + return "", fmt.Errorf("failed to inspect image: %w", err) + } + + // Get version from LOCALSTACK_BUILD_VERSION environment variable + if inspect.Config != nil && inspect.Config.Env != nil { + for _, env := range inspect.Config.Env { + if strings.HasPrefix(env, "LOCALSTACK_BUILD_VERSION=") { + return strings.TrimPrefix(env, "LOCALSTACK_BUILD_VERSION="), nil + } + } + } + + return "", fmt.Errorf("LOCALSTACK_BUILD_VERSION not found in image environment") +} diff --git a/internal/runtime/runtime.go b/internal/runtime/runtime.go index caeb6af..4f83f39 100644 --- a/internal/runtime/runtime.go +++ b/internal/runtime/runtime.go @@ -25,4 +25,5 @@ type Runtime interface { Remove(ctx context.Context, containerName string) error IsRunning(ctx context.Context, containerID string) (bool, error) Logs(ctx context.Context, containerID string, tail int) (string, error) + GetImageVersion(ctx context.Context, imageName string) (string, error) } diff --git a/test/integration/license_test.go b/test/integration/license_test.go new file mode 100644 index 0000000..f75789b --- /dev/null +++ b/test/integration/license_test.go @@ -0,0 +1,117 @@ +package integration_test + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "testing" + "time" + + "github.com/docker/docker/api/types/container" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const licenseContainerName = "localstack-aws" + +func TestLicenseValidationSuccess(t *testing.T) { + requireDocker(t) + authToken := os.Getenv("LOCALSTACK_AUTH_TOKEN") + require.NotEmpty(t, authToken, "LOCALSTACK_AUTH_TOKEN must be set to run this test") + + cleanupLicense() + t.Cleanup(cleanupLicense) + + validationErrors := make(chan error, 1) + + // Mock platform API that returns success + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/v1/license/request" && r.Method == http.MethodPost { + var req map[string]interface{} + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "invalid request", http.StatusBadRequest) + return + } + + // Validate with safe type assertions + product, ok := req["product"].(map[string]interface{}) + if !ok || product["name"] != "localstack-pro" { + validationErrors <- fmt.Errorf("invalid product field") + http.Error(w, "invalid product", http.StatusBadRequest) + return + } + + credentials, ok := req["credentials"].(map[string]interface{}) + if !ok || credentials["token"] != authToken { + validationErrors <- fmt.Errorf("invalid credentials field") + http.Error(w, "invalid credentials", http.StatusBadRequest) + return + } + + w.WriteHeader(http.StatusOK) + return + } + http.NotFound(w, r) + })) + defer mockServer.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + + cmd := exec.CommandContext(ctx, binaryPath(), "start") + cmd.Env = append( + os.Environ(), + "LOCALSTACK_API_ENDPOINT="+mockServer.URL, + ) + output, err := cmd.CombinedOutput() + + // Check for validation errors from handler + select { + case validationErr := <-validationErrors: + t.Fatalf("request validation failed: %v", validationErr) + default: + } + + require.NoError(t, err, "lstk start failed: %s", output) + + inspect, err := dockerClient.ContainerInspect(ctx, licenseContainerName) + require.NoError(t, err, "failed to inspect container") + assert.True(t, inspect.State.Running, "container should be running") +} + +func TestLicenseValidationFailure(t *testing.T) { + requireDocker(t) + cleanupLicense() + t.Cleanup(cleanupLicense) + + mockServer := createMockLicenseServer(false) + defer mockServer.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + + cmd := exec.CommandContext(ctx, binaryPath(), "start") + cmd.Env = append( + os.Environ(), + "LOCALSTACK_API_ENDPOINT="+mockServer.URL, + ) + output, err := cmd.CombinedOutput() + + require.Error(t, err, "expected lstk start to fail with forbidden license") + assert.Contains(t, string(output), "license validation failed") + assert.Contains(t, string(output), "invalid, inactive, or expired") + + // Verify container was not started + _, err = dockerClient.ContainerInspect(ctx, licenseContainerName) + assert.Error(t, err, "container should not exist after license failure") +} + +func cleanupLicense() { + ctx := context.Background() + _ = dockerClient.ContainerStop(ctx, licenseContainerName, container.StopOptions{}) + _ = dockerClient.ContainerRemove(ctx, licenseContainerName, container.RemoveOptions{Force: true}) +} diff --git a/test/integration/login_browser_flow_test.go b/test/integration/login_browser_flow_test.go index d03819d..c66e49a 100644 --- a/test/integration/login_browser_flow_test.go +++ b/test/integration/login_browser_flow_test.go @@ -2,7 +2,9 @@ package integration_test import ( "context" + "encoding/json" "net/http" + "net/http/httptest" "os/exec" "testing" "time" @@ -15,11 +17,32 @@ func TestBrowserFlowStoresToken(t *testing.T) { cleanup() t.Cleanup(cleanup) + // Mock server that handles both auth and license endpoints + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == "POST" && r.URL.Path == "/v1/auth/request": + w.WriteHeader(http.StatusCreated) + _ = json.NewEncoder(w).Encode(map[string]string{ + "id": "test-id", + "code": "TEST123", + "exchange_token": "test-exchange", + }) + case r.Method == "POST" && r.URL.Path == "/v1/license/request": + w.WriteHeader(http.StatusOK) + default: + w.WriteHeader(http.StatusNotFound) + } + })) + defer mockServer.Close() + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() cmd := exec.CommandContext(ctx, binaryPath(), "login") - cmd.Env = envWithoutAuthToken() + cmd.Env = append( + envWithout("LOCALSTACK_AUTH_TOKEN"), + "LOCALSTACK_API_ENDPOINT="+mockServer.URL, + ) // Keep stdin open so ENTER listener doesn't trigger immediately stdinPipe, err := cmd.StdinPipe() diff --git a/test/integration/login_device_flow_test.go b/test/integration/login_device_flow_test.go index 5fd4b58..13e2e09 100644 --- a/test/integration/login_device_flow_test.go +++ b/test/integration/login_device_flow_test.go @@ -16,7 +16,7 @@ import ( ) // createMockAPIServer creates a mock LocalStack API server for testing -func createMockAPIServer(t *testing.T, licenseToken string) *httptest.Server { +func createMockAPIServer(t *testing.T, licenseToken string, confirmed bool) *httptest.Server { authReqID := "test-auth-req-id" exchangeToken := "test-exchange-token" bearerToken := "Bearer test-bearer-token" @@ -35,7 +35,7 @@ func createMockAPIServer(t *testing.T, licenseToken string) *httptest.Server { case r.Method == "GET" && r.URL.Path == fmt.Sprintf("/v1/auth/request/%s", authReqID): w.WriteHeader(http.StatusOK) err := json.NewEncoder(w).Encode(map[string]bool{ - "confirmed": true, + "confirmed": confirmed, }) require.NoError(t, err) @@ -54,6 +54,9 @@ func createMockAPIServer(t *testing.T, licenseToken string) *httptest.Server { }) require.NoError(t, err) + case r.Method == "POST" && r.URL.Path == "/v1/license/request": + w.WriteHeader(http.StatusOK) + default: t.Logf("Unhandled request: %s %s", r.Method, r.URL.Path) w.WriteHeader(http.StatusNotFound) @@ -70,16 +73,17 @@ func TestDeviceFlowSuccess(t *testing.T) { require.NotEmpty(t, licenseToken, "LOCALSTACK_AUTH_TOKEN must be set to run this test") // Create mock API server that returns the real token - mockServer := createMockAPIServer(t, licenseToken) + mockServer := createMockAPIServer(t, licenseToken, true) defer mockServer.Close() ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() cmd := exec.CommandContext(ctx, binaryPath(), "login") - env := envWithoutAuthToken() - env = append(env, "LOCALSTACK_API_ENDPOINT="+mockServer.URL) - cmd.Env = env + cmd.Env = append( + envWithout("LOCALSTACK_AUTH_TOKEN"), + "LOCALSTACK_API_ENDPOINT="+mockServer.URL, + ) // Keep stdin open and get the pipe to simulate ENTER stdinPipe, err := cmd.StdinPipe() @@ -126,11 +130,17 @@ func TestDeviceFlowFailure_RequestNotConfirmed(t *testing.T) { cleanup() t.Cleanup(cleanup) + mockServer := createMockAPIServer(t, "", false) + defer mockServer.Close() + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() cmd := exec.CommandContext(ctx, binaryPath(), "login") - cmd.Env = envWithoutAuthToken() + cmd.Env = append( + envWithout("LOCALSTACK_AUTH_TOKEN"), + "LOCALSTACK_API_ENDPOINT="+mockServer.URL, + ) // Keep stdin open and get the pipe to simulate ENTER stdinPipe, err := cmd.StdinPipe() diff --git a/test/integration/main_test.go b/test/integration/main_test.go index e3502d2..2243e3d 100644 --- a/test/integration/main_test.go +++ b/test/integration/main_test.go @@ -4,6 +4,8 @@ import ( "context" "errors" "fmt" + "net/http" + "net/http/httptest" "os" "path/filepath" "runtime" @@ -76,10 +78,17 @@ func requireDocker(t *testing.T) { } } -func envWithoutAuthToken() []string { +func envWithout(keys ...string) []string { var env []string for _, e := range os.Environ() { - if !strings.HasPrefix(e, "LOCALSTACK_AUTH_TOKEN=") { + excluded := false + for _, key := range keys { + if strings.HasPrefix(e, key+"=") { + excluded = true + break + } + } + if !excluded { env = append(env, e) } } @@ -111,3 +120,17 @@ func keyringDelete(service, user string) error { } return err } + +func createMockLicenseServer(success bool) *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == "POST" && r.URL.Path == "/v1/license/request" { + if success { + w.WriteHeader(http.StatusOK) + } else { + w.WriteHeader(http.StatusForbidden) + } + return + } + w.WriteHeader(http.StatusNotFound) + })) +} diff --git a/test/integration/start_test.go b/test/integration/start_test.go index 2e56786..6e58bd0 100644 --- a/test/integration/start_test.go +++ b/test/integration/start_test.go @@ -27,11 +27,16 @@ func TestStartCommandSucceedsWithValidToken(t *testing.T) { cleanup() t.Cleanup(cleanup) + mockServer := createMockLicenseServer(true) + defer mockServer.Close() + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() cmd := exec.CommandContext(ctx, binaryPath(), "start") - cmd.Env = append(os.Environ(), "LOCALSTACK_AUTH_TOKEN="+authToken) + cmd.Env = append(os.Environ(), + "LOCALSTACK_API_ENDPOINT="+mockServer.URL, + ) output, err := cmd.CombinedOutput() require.NoError(t, err, "lstk start failed: %s", output) @@ -52,12 +57,18 @@ func TestStartCommandSucceedsWithKeyringToken(t *testing.T) { err := keyringSet(keyringService, keyringUser, authToken) require.NoError(t, err, "failed to store token in keyring") + mockServer := createMockLicenseServer(true) + defer mockServer.Close() + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() // Run without LOCALSTACK_AUTH_TOKEN should use keyring cmd := exec.CommandContext(ctx, binaryPath(), "start") - cmd.Env = envWithoutAuthToken() + cmd.Env = append( + envWithout("LOCALSTACK_AUTH_TOKEN"), + "LOCALSTACK_API_ENDPOINT="+mockServer.URL, + ) output, err := cmd.CombinedOutput() require.NoError(t, err, "lstk start failed: %s", output) @@ -72,15 +83,21 @@ func TestStartCommandFailsWithInvalidToken(t *testing.T) { cleanup() t.Cleanup(cleanup) + mockServer := createMockLicenseServer(false) + defer mockServer.Close() + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() cmd := exec.CommandContext(ctx, binaryPath(), "start") - cmd.Env = append(os.Environ(), "LOCALSTACK_AUTH_TOKEN=invalid-token") + cmd.Env = append(os.Environ(), + "LOCALSTACK_AUTH_TOKEN=invalid-token", + "LOCALSTACK_API_ENDPOINT="+mockServer.URL, + ) output, err := cmd.CombinedOutput() require.Error(t, err, "expected lstk start to fail with invalid token") - assert.Contains(t, string(output), "License activation failed") + assert.Contains(t, string(output), "license validation failed") } func cleanup() {