|
4 | 4 | package plugin |
5 | 5 |
|
6 | 6 | import ( |
| 7 | + "os" |
| 8 | + "os/exec" |
| 9 | + "path/filepath" |
7 | 10 | "testing" |
8 | 11 |
|
9 | 12 | "go.jetify.com/devbox/nix/flake" |
@@ -399,3 +402,151 @@ func TestIsSSHURL(t *testing.T) { |
399 | 402 | }) |
400 | 403 | } |
401 | 404 | } |
| 405 | + |
| 406 | +// setupLocalGitRepo creates a temporary bare git repo with a plugin.json file. |
| 407 | +// Returns the file:// URL to the repo. |
| 408 | +func setupLocalGitRepo(t *testing.T, content string) string { |
| 409 | + t.Helper() |
| 410 | + |
| 411 | + if _, err := exec.LookPath("git"); err != nil { |
| 412 | + t.Skip("skipping: git not found in PATH") |
| 413 | + } |
| 414 | + |
| 415 | + // Create a working repo, commit a file, then clone it as bare. |
| 416 | + workDir := t.TempDir() |
| 417 | + runGit := func(args ...string) { |
| 418 | + t.Helper() |
| 419 | + cmd := exec.Command("git", args...) |
| 420 | + cmd.Dir = workDir |
| 421 | + cmd.Env = append(os.Environ(), |
| 422 | + "GIT_AUTHOR_NAME=test", |
| 423 | + "GIT_AUTHOR_EMAIL=test@test.com", |
| 424 | + "GIT_COMMITTER_NAME=test", |
| 425 | + "GIT_COMMITTER_EMAIL=test@test.com", |
| 426 | + ) |
| 427 | + out, err := cmd.CombinedOutput() |
| 428 | + if err != nil { |
| 429 | + t.Fatalf("git %v failed: %v\n%s", args, err, out) |
| 430 | + } |
| 431 | + } |
| 432 | + |
| 433 | + runGit("init") |
| 434 | + runGit("checkout", "-b", "main") |
| 435 | + if err := os.WriteFile(filepath.Join(workDir, "plugin.json"), []byte(content), 0o644); err != nil { |
| 436 | + t.Fatal(err) |
| 437 | + } |
| 438 | + runGit("add", "plugin.json") |
| 439 | + runGit("commit", "-m", "init") |
| 440 | + |
| 441 | + // Clone to bare repo so file:// clone works cleanly. |
| 442 | + bareDir := t.TempDir() |
| 443 | + cmd := exec.Command("git", "clone", "--bare", workDir, bareDir) |
| 444 | + if out, err := cmd.CombinedOutput(); err != nil { |
| 445 | + t.Fatalf("bare clone failed: %v\n%s", err, out) |
| 446 | + } |
| 447 | + |
| 448 | + return "file://" + bareDir |
| 449 | +} |
| 450 | + |
| 451 | +func TestGitPluginFileContentCache(t *testing.T) { |
| 452 | + // Clear the git cache before and after the test to avoid pollution. |
| 453 | + if err := gitCache.Clear(); err != nil { |
| 454 | + t.Fatal(err) |
| 455 | + } |
| 456 | + t.Cleanup(func() { _ = gitCache.Clear() }) |
| 457 | + |
| 458 | + repoURL := setupLocalGitRepo(t, `{"name": "test-plugin"}`) |
| 459 | + |
| 460 | + plugin := &gitPlugin{ |
| 461 | + ref: &flake.Ref{ |
| 462 | + Type: flake.TypeGit, |
| 463 | + URL: repoURL, |
| 464 | + Ref: "main", |
| 465 | + }, |
| 466 | + name: "test-cache-plugin", |
| 467 | + } |
| 468 | + |
| 469 | + // First call — populates the cache via git clone. |
| 470 | + content1, err := plugin.FileContent("plugin.json") |
| 471 | + if err != nil { |
| 472 | + t.Fatalf("first FileContent call failed: %v", err) |
| 473 | + } |
| 474 | + if string(content1) != `{"name": "test-plugin"}` { |
| 475 | + t.Fatalf("unexpected content: %s", content1) |
| 476 | + } |
| 477 | + |
| 478 | + // Delete the source repo. If the cache is working, FileContent should |
| 479 | + // still return the cached value without attempting a clone. |
| 480 | + repoPath := repoURL[len("file://"):] |
| 481 | + if err := os.RemoveAll(repoPath); err != nil { |
| 482 | + t.Fatalf("failed to remove repo: %v", err) |
| 483 | + } |
| 484 | + |
| 485 | + content2, err := plugin.FileContent("plugin.json") |
| 486 | + if err != nil { |
| 487 | + t.Fatalf("second FileContent call should have used cache but failed: %v", err) |
| 488 | + } |
| 489 | + if string(content2) != string(content1) { |
| 490 | + t.Fatalf("cached content mismatch: got %s, want %s", content2, content1) |
| 491 | + } |
| 492 | +} |
| 493 | + |
| 494 | +func TestGitPluginFileContentCacheRespectsEnvVar(t *testing.T) { |
| 495 | + if err := gitCache.Clear(); err != nil { |
| 496 | + t.Fatal(err) |
| 497 | + } |
| 498 | + t.Cleanup(func() { _ = gitCache.Clear() }) |
| 499 | + |
| 500 | + repoURL := setupLocalGitRepo(t, `{"name": "ttl-test"}`) |
| 501 | + |
| 502 | + plugin := &gitPlugin{ |
| 503 | + ref: &flake.Ref{ |
| 504 | + Type: flake.TypeGit, |
| 505 | + URL: repoURL, |
| 506 | + Ref: "main", |
| 507 | + }, |
| 508 | + name: "test-ttl-plugin", |
| 509 | + } |
| 510 | + |
| 511 | + // Set a very short TTL so the cache expires immediately. |
| 512 | + t.Setenv("DEVBOX_X_GITHUB_PLUGIN_CACHE_TTL", "1ns") |
| 513 | + |
| 514 | + content, err := plugin.FileContent("plugin.json") |
| 515 | + if err != nil { |
| 516 | + t.Fatalf("FileContent failed: %v", err) |
| 517 | + } |
| 518 | + if string(content) != `{"name": "ttl-test"}` { |
| 519 | + t.Fatalf("unexpected content: %s", content) |
| 520 | + } |
| 521 | + |
| 522 | + // With a 1ns TTL the cache entry should already be expired. |
| 523 | + // Delete the source repo — if the expired cache is not served, |
| 524 | + // this will attempt a fresh clone and fail, proving the TTL works. |
| 525 | + repoPath := repoURL[len("file://"):] |
| 526 | + if err := os.RemoveAll(repoPath); err != nil { |
| 527 | + t.Fatalf("failed to remove repo: %v", err) |
| 528 | + } |
| 529 | + _, err = plugin.FileContent("plugin.json") |
| 530 | + if err == nil { |
| 531 | + t.Fatal("expected error after cache expiry with deleted repo, but got nil") |
| 532 | + } |
| 533 | +} |
| 534 | + |
| 535 | +func TestGitPluginFileContentCacheInvalidTTL(t *testing.T) { |
| 536 | + t.Setenv("DEVBOX_X_GITHUB_PLUGIN_CACHE_TTL", "not-a-duration") |
| 537 | + t.Cleanup(func() { _ = gitCache.Clear() }) |
| 538 | + |
| 539 | + plugin := &gitPlugin{ |
| 540 | + ref: &flake.Ref{ |
| 541 | + Type: flake.TypeGit, |
| 542 | + URL: "file:///doesnt-matter", |
| 543 | + Ref: "main", |
| 544 | + }, |
| 545 | + name: "test-invalid-ttl", |
| 546 | + } |
| 547 | + |
| 548 | + _, err := plugin.FileContent("plugin.json") |
| 549 | + if err == nil { |
| 550 | + t.Fatal("expected error for invalid TTL, got nil") |
| 551 | + } |
| 552 | +} |
0 commit comments