|
1 | 1 | package integration_test |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "bytes" |
| 5 | + "context" |
| 6 | + "io" |
4 | 7 | "os" |
5 | 8 | "os/exec" |
6 | 9 | "path/filepath" |
7 | 10 | "runtime" |
8 | 11 | "strings" |
9 | 12 | "testing" |
| 13 | + "time" |
10 | 14 |
|
| 15 | + "github.com/creack/pty" |
| 16 | + "github.com/localstack/lstk/test/integration/env" |
11 | 17 | "github.com/stretchr/testify/assert" |
12 | 18 | "github.com/stretchr/testify/require" |
13 | 19 | ) |
@@ -205,6 +211,154 @@ func TestUpdateHomebrew(t *testing.T) { |
205 | 211 | assert.Contains(t, updateStr, "brew upgrade", "should mention brew upgrade") |
206 | 212 | } |
207 | 213 |
|
| 214 | +func TestUpdateNotification(t *testing.T) { |
| 215 | + if runtime.GOOS == "windows" { |
| 216 | + t.Skip("PTY not supported on Windows") |
| 217 | + } |
| 218 | + |
| 219 | + ctx := testContext(t) |
| 220 | + |
| 221 | + // Build a fake old version to a temp location |
| 222 | + tmpBinary := filepath.Join(t.TempDir(), "lstk") |
| 223 | + repoRoot, err := filepath.Abs("../..") |
| 224 | + require.NoError(t, err) |
| 225 | + |
| 226 | + buildCmd := exec.CommandContext(ctx, "go", "build", |
| 227 | + "-ldflags", "-X github.com/localstack/lstk/internal/version.version=0.0.1", |
| 228 | + "-o", tmpBinary, |
| 229 | + ".", |
| 230 | + ) |
| 231 | + buildCmd.Dir = repoRoot |
| 232 | + out, err := buildCmd.CombinedOutput() |
| 233 | + require.NoError(t, err, "go build failed: %s", string(out)) |
| 234 | + |
| 235 | + // Mock API server so license validation fails fast after the notification |
| 236 | + mockServer := createMockLicenseServer(false) |
| 237 | + defer mockServer.Close() |
| 238 | + |
| 239 | + t.Run("skip", func(t *testing.T) { |
| 240 | + configFile := filepath.Join(t.TempDir(), "config.toml") |
| 241 | + require.NoError(t, os.WriteFile(configFile, []byte("update_prompt = true\n"), 0o644)) |
| 242 | + |
| 243 | + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) |
| 244 | + defer cancel() |
| 245 | + |
| 246 | + cmd := exec.CommandContext(ctx, tmpBinary, "--config", configFile) |
| 247 | + cmd.Env = env.Without(env.AuthToken).With(env.AuthToken, "fake-token").With(env.APIEndpoint, mockServer.URL) |
| 248 | + |
| 249 | + ptmx, err := pty.Start(cmd) |
| 250 | + require.NoError(t, err, "failed to start command in PTY") |
| 251 | + defer func() { _ = ptmx.Close() }() |
| 252 | + |
| 253 | + output := &syncBuffer{} |
| 254 | + outputCh := make(chan struct{}) |
| 255 | + go func() { |
| 256 | + _, _ = io.Copy(output, ptmx) |
| 257 | + close(outputCh) |
| 258 | + }() |
| 259 | + |
| 260 | + require.Eventually(t, func() bool { |
| 261 | + return bytes.Contains(output.Bytes(), []byte("new version is available")) |
| 262 | + }, 10*time.Second, 100*time.Millisecond, "update notification prompt should appear") |
| 263 | + |
| 264 | + _, err = ptmx.Write([]byte("s")) |
| 265 | + require.NoError(t, err) |
| 266 | + |
| 267 | + _ = cmd.Wait() |
| 268 | + <-outputCh |
| 269 | + |
| 270 | + assert.Contains(t, output.String(), "Update available: 0.0.1") |
| 271 | + }) |
| 272 | + |
| 273 | + t.Run("never", func(t *testing.T) { |
| 274 | + configFile := filepath.Join(t.TempDir(), "config.toml") |
| 275 | + require.NoError(t, os.WriteFile(configFile, []byte("update_prompt = true\n"), 0o644)) |
| 276 | + |
| 277 | + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) |
| 278 | + defer cancel() |
| 279 | + |
| 280 | + cmd := exec.CommandContext(ctx, tmpBinary, "--config", configFile) |
| 281 | + cmd.Env = env.Without(env.AuthToken).With(env.AuthToken, "fake-token").With(env.APIEndpoint, mockServer.URL) |
| 282 | + |
| 283 | + ptmx, err := pty.Start(cmd) |
| 284 | + require.NoError(t, err, "failed to start command in PTY") |
| 285 | + defer func() { _ = ptmx.Close() }() |
| 286 | + |
| 287 | + output := &syncBuffer{} |
| 288 | + outputCh := make(chan struct{}) |
| 289 | + go func() { |
| 290 | + _, _ = io.Copy(output, ptmx) |
| 291 | + close(outputCh) |
| 292 | + }() |
| 293 | + |
| 294 | + require.Eventually(t, func() bool { |
| 295 | + return bytes.Contains(output.Bytes(), []byte("new version is available")) |
| 296 | + }, 10*time.Second, 100*time.Millisecond, "update notification prompt should appear") |
| 297 | + |
| 298 | + _, err = ptmx.Write([]byte("n")) |
| 299 | + require.NoError(t, err) |
| 300 | + |
| 301 | + _ = cmd.Wait() |
| 302 | + <-outputCh |
| 303 | + |
| 304 | + assert.Contains(t, output.String(), "Update available: 0.0.1") |
| 305 | + |
| 306 | + // Verify config was updated to disable future prompts |
| 307 | + configData, err := os.ReadFile(configFile) |
| 308 | + require.NoError(t, err) |
| 309 | + assert.Contains(t, string(configData), "update_prompt = false") |
| 310 | + }) |
| 311 | + |
| 312 | + t.Run("update", func(t *testing.T) { |
| 313 | + // Copy binary since it will be replaced during the update |
| 314 | + updateBinary := filepath.Join(t.TempDir(), "lstk") |
| 315 | + data, err := os.ReadFile(tmpBinary) |
| 316 | + require.NoError(t, err) |
| 317 | + require.NoError(t, os.WriteFile(updateBinary, data, 0o755)) |
| 318 | + |
| 319 | + configFile := filepath.Join(t.TempDir(), "config.toml") |
| 320 | + require.NoError(t, os.WriteFile(configFile, []byte("update_prompt = true\n"), 0o644)) |
| 321 | + |
| 322 | + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) |
| 323 | + defer cancel() |
| 324 | + |
| 325 | + cmd := exec.CommandContext(ctx, updateBinary, "--config", configFile) |
| 326 | + cmd.Env = env.Without(env.AuthToken).With(env.AuthToken, "fake-token").With(env.APIEndpoint, mockServer.URL) |
| 327 | + |
| 328 | + ptmx, err := pty.Start(cmd) |
| 329 | + require.NoError(t, err, "failed to start command in PTY") |
| 330 | + defer func() { _ = ptmx.Close() }() |
| 331 | + |
| 332 | + output := &syncBuffer{} |
| 333 | + outputCh := make(chan struct{}) |
| 334 | + go func() { |
| 335 | + _, _ = io.Copy(output, ptmx) |
| 336 | + close(outputCh) |
| 337 | + }() |
| 338 | + |
| 339 | + require.Eventually(t, func() bool { |
| 340 | + return bytes.Contains(output.Bytes(), []byte("new version is available")) |
| 341 | + }, 10*time.Second, 100*time.Millisecond, "update notification prompt should appear") |
| 342 | + |
| 343 | + _, err = ptmx.Write([]byte("u")) |
| 344 | + require.NoError(t, err) |
| 345 | + |
| 346 | + err = cmd.Wait() |
| 347 | + <-outputCh |
| 348 | + |
| 349 | + out := output.String() |
| 350 | + require.NoError(t, err, "update should succeed: %s", out) |
| 351 | + assert.Contains(t, out, "Update available: 0.0.1") |
| 352 | + assert.Contains(t, out, "Updated to") |
| 353 | + |
| 354 | + // Verify the binary was actually replaced |
| 355 | + verCmd := exec.CommandContext(ctx, updateBinary, "--version") |
| 356 | + verOut, err := verCmd.CombinedOutput() |
| 357 | + require.NoError(t, err) |
| 358 | + assert.NotContains(t, string(verOut), "0.0.1", "binary should no longer be the old version") |
| 359 | + }) |
| 360 | +} |
| 361 | + |
208 | 362 | func npmPlatformPackage() string { |
209 | 363 | return "lstk_" + runtime.GOOS + "_" + runtime.GOARCH |
210 | 364 | } |
0 commit comments