|
5 | 5 | "os" |
6 | 6 | "path/filepath" |
7 | 7 | "testing" |
| 8 | + |
| 9 | + "github.com/atomikpanda/dotular/internal/config" |
8 | 10 | ) |
9 | 11 |
|
10 | 12 | func writeTestConfig(t *testing.T, content string) string { |
@@ -32,7 +34,7 @@ func TestBuildRoot(t *testing.T) { |
32 | 34 | names[cmd.Name()] = true |
33 | 35 | } |
34 | 36 |
|
35 | | - expected := []string{"apply", "push", "pull", "sync", "list", "status", "platform", "verify", "encrypt", "decrypt", "tag", "log", "registry"} |
| 37 | + expected := []string{"add", "apply", "push", "pull", "sync", "list", "status", "platform", "verify", "encrypt", "decrypt", "tag", "log", "registry"} |
36 | 38 | for _, name := range expected { |
37 | 39 | if !names[name] { |
38 | 40 | t.Errorf("missing subcommand %q", name) |
@@ -509,3 +511,258 @@ func TestRegistryUpdateCmdExecute(t *testing.T) { |
509 | 511 | t.Fatal(err) |
510 | 512 | } |
511 | 513 | } |
| 514 | + |
| 515 | +// --- add command tests ------------------------------------------------------- |
| 516 | + |
| 517 | +func TestAddCmdDef(t *testing.T) { |
| 518 | + cmd := addCmd() |
| 519 | + if cmd.Use != "add <module> <path>" { |
| 520 | + t.Errorf("Use = %q", cmd.Use) |
| 521 | + } |
| 522 | +} |
| 523 | + |
| 524 | +func TestAddCmdFile(t *testing.T) { |
| 525 | + dir := t.TempDir() |
| 526 | + cfgPath := filepath.Join(dir, "dotular.yaml") |
| 527 | + os.WriteFile(cfgPath, []byte("modules: []\n"), 0o644) |
| 528 | + |
| 529 | + // Create a source file. |
| 530 | + srcFile := filepath.Join(dir, "myfile.txt") |
| 531 | + os.WriteFile(srcFile, []byte("hello"), 0o644) |
| 532 | + |
| 533 | + root := buildRoot() |
| 534 | + root.SetArgs([]string{"add", "--config", cfgPath, "mymod", srcFile}) |
| 535 | + if err := root.Execute(); err != nil { |
| 536 | + t.Fatal(err) |
| 537 | + } |
| 538 | + |
| 539 | + // Verify the file was copied into the module store. |
| 540 | + stored := filepath.Join(dir, "mymod", "myfile.txt") |
| 541 | + data, err := os.ReadFile(stored) |
| 542 | + if err != nil { |
| 543 | + t.Fatalf("stored file not found: %v", err) |
| 544 | + } |
| 545 | + if string(data) != "hello" { |
| 546 | + t.Errorf("stored content = %q", string(data)) |
| 547 | + } |
| 548 | + |
| 549 | + // Verify the config was updated. |
| 550 | + cfg, err := loadConfigFrom(cfgPath) |
| 551 | + if err != nil { |
| 552 | + t.Fatal(err) |
| 553 | + } |
| 554 | + mod := cfg.Module("mymod") |
| 555 | + if mod == nil { |
| 556 | + t.Fatal("module 'mymod' not found in config") |
| 557 | + } |
| 558 | + if len(mod.Items) != 1 { |
| 559 | + t.Fatalf("expected 1 item, got %d", len(mod.Items)) |
| 560 | + } |
| 561 | + if mod.Items[0].File != "myfile.txt" { |
| 562 | + t.Errorf("item file = %q", mod.Items[0].File) |
| 563 | + } |
| 564 | +} |
| 565 | + |
| 566 | +func TestAddCmdDirectory(t *testing.T) { |
| 567 | + dir := t.TempDir() |
| 568 | + cfgPath := filepath.Join(dir, "dotular.yaml") |
| 569 | + os.WriteFile(cfgPath, []byte("modules: []\n"), 0o644) |
| 570 | + |
| 571 | + // Create a source directory. |
| 572 | + srcDir := filepath.Join(dir, "mydir") |
| 573 | + os.MkdirAll(filepath.Join(srcDir, "sub"), 0o755) |
| 574 | + os.WriteFile(filepath.Join(srcDir, "a.txt"), []byte("aaa"), 0o644) |
| 575 | + os.WriteFile(filepath.Join(srcDir, "sub", "b.txt"), []byte("bbb"), 0o644) |
| 576 | + |
| 577 | + root := buildRoot() |
| 578 | + root.SetArgs([]string{"add", "--config", cfgPath, "mymod", srcDir}) |
| 579 | + if err := root.Execute(); err != nil { |
| 580 | + t.Fatal(err) |
| 581 | + } |
| 582 | + |
| 583 | + // Verify the directory was copied. |
| 584 | + data, err := os.ReadFile(filepath.Join(dir, "mymod", "mydir", "sub", "b.txt")) |
| 585 | + if err != nil { |
| 586 | + t.Fatalf("stored file not found: %v", err) |
| 587 | + } |
| 588 | + if string(data) != "bbb" { |
| 589 | + t.Errorf("stored content = %q", string(data)) |
| 590 | + } |
| 591 | + |
| 592 | + // Verify the config. |
| 593 | + cfg, err := loadConfigFrom(cfgPath) |
| 594 | + if err != nil { |
| 595 | + t.Fatal(err) |
| 596 | + } |
| 597 | + mod := cfg.Module("mymod") |
| 598 | + if mod == nil { |
| 599 | + t.Fatal("module 'mymod' not found") |
| 600 | + } |
| 601 | + if mod.Items[0].Directory != "mydir" { |
| 602 | + t.Errorf("item directory = %q", mod.Items[0].Directory) |
| 603 | + } |
| 604 | +} |
| 605 | + |
| 606 | +func TestAddCmdToExistingModule(t *testing.T) { |
| 607 | + dir := t.TempDir() |
| 608 | + cfgPath := filepath.Join(dir, "dotular.yaml") |
| 609 | + os.WriteFile(cfgPath, []byte(` |
| 610 | +modules: |
| 611 | + - name: existing |
| 612 | + items: |
| 613 | + - package: git |
| 614 | + via: brew |
| 615 | +`), 0o644) |
| 616 | + |
| 617 | + srcFile := filepath.Join(dir, "extra.txt") |
| 618 | + os.WriteFile(srcFile, []byte("extra"), 0o644) |
| 619 | + |
| 620 | + root := buildRoot() |
| 621 | + root.SetArgs([]string{"add", "--config", cfgPath, "existing", srcFile}) |
| 622 | + if err := root.Execute(); err != nil { |
| 623 | + t.Fatal(err) |
| 624 | + } |
| 625 | + |
| 626 | + cfg, err := loadConfigFrom(cfgPath) |
| 627 | + if err != nil { |
| 628 | + t.Fatal(err) |
| 629 | + } |
| 630 | + mod := cfg.Module("existing") |
| 631 | + if mod == nil { |
| 632 | + t.Fatal("module 'existing' not found") |
| 633 | + } |
| 634 | + if len(mod.Items) != 2 { |
| 635 | + t.Fatalf("expected 2 items, got %d", len(mod.Items)) |
| 636 | + } |
| 637 | +} |
| 638 | + |
| 639 | +func TestAddCmdWithLink(t *testing.T) { |
| 640 | + dir := t.TempDir() |
| 641 | + cfgPath := filepath.Join(dir, "dotular.yaml") |
| 642 | + os.WriteFile(cfgPath, []byte("modules: []\n"), 0o644) |
| 643 | + |
| 644 | + srcFile := filepath.Join(dir, "linkme.txt") |
| 645 | + os.WriteFile(srcFile, []byte("data"), 0o644) |
| 646 | + |
| 647 | + root := buildRoot() |
| 648 | + root.SetArgs([]string{"add", "--config", cfgPath, "--link", "linkmod", srcFile}) |
| 649 | + if err := root.Execute(); err != nil { |
| 650 | + t.Fatal(err) |
| 651 | + } |
| 652 | + |
| 653 | + cfg, err := loadConfigFrom(cfgPath) |
| 654 | + if err != nil { |
| 655 | + t.Fatal(err) |
| 656 | + } |
| 657 | + mod := cfg.Module("linkmod") |
| 658 | + if mod == nil { |
| 659 | + t.Fatal("module not found") |
| 660 | + } |
| 661 | + if !mod.Items[0].Link { |
| 662 | + t.Error("expected link=true") |
| 663 | + } |
| 664 | +} |
| 665 | + |
| 666 | +func TestAddCmdWithDirection(t *testing.T) { |
| 667 | + dir := t.TempDir() |
| 668 | + cfgPath := filepath.Join(dir, "dotular.yaml") |
| 669 | + os.WriteFile(cfgPath, []byte("modules: []\n"), 0o644) |
| 670 | + |
| 671 | + srcFile := filepath.Join(dir, "syncme.txt") |
| 672 | + os.WriteFile(srcFile, []byte("data"), 0o644) |
| 673 | + |
| 674 | + root := buildRoot() |
| 675 | + root.SetArgs([]string{"add", "--config", cfgPath, "--direction", "sync", "syncmod", srcFile}) |
| 676 | + if err := root.Execute(); err != nil { |
| 677 | + t.Fatal(err) |
| 678 | + } |
| 679 | + |
| 680 | + cfg, err := loadConfigFrom(cfgPath) |
| 681 | + if err != nil { |
| 682 | + t.Fatal(err) |
| 683 | + } |
| 684 | + mod := cfg.Module("syncmod") |
| 685 | + if mod == nil { |
| 686 | + t.Fatal("module not found") |
| 687 | + } |
| 688 | + if mod.Items[0].Direction != "sync" { |
| 689 | + t.Errorf("direction = %q, want sync", mod.Items[0].Direction) |
| 690 | + } |
| 691 | +} |
| 692 | + |
| 693 | +func TestAddCmdMissingPath(t *testing.T) { |
| 694 | + dir := t.TempDir() |
| 695 | + cfgPath := filepath.Join(dir, "dotular.yaml") |
| 696 | + os.WriteFile(cfgPath, []byte("modules: []\n"), 0o644) |
| 697 | + |
| 698 | + root := buildRoot() |
| 699 | + root.SetArgs([]string{"add", "--config", cfgPath, "mymod", "/nonexistent/path"}) |
| 700 | + if err := root.Execute(); err == nil { |
| 701 | + t.Error("expected error for nonexistent source path") |
| 702 | + } |
| 703 | +} |
| 704 | + |
| 705 | +func TestAddCmdRequiresArgs(t *testing.T) { |
| 706 | + root := buildRoot() |
| 707 | + root.SetArgs([]string{"add"}) |
| 708 | + if err := root.Execute(); err == nil { |
| 709 | + t.Error("expected error for missing args") |
| 710 | + } |
| 711 | +} |
| 712 | + |
| 713 | +func TestCopyFileSimple(t *testing.T) { |
| 714 | + dir := t.TempDir() |
| 715 | + src := filepath.Join(dir, "src.txt") |
| 716 | + dst := filepath.Join(dir, "dst.txt") |
| 717 | + os.WriteFile(src, []byte("content"), 0o644) |
| 718 | + |
| 719 | + if err := copyFileSimple(src, dst); err != nil { |
| 720 | + t.Fatal(err) |
| 721 | + } |
| 722 | + |
| 723 | + data, _ := os.ReadFile(dst) |
| 724 | + if string(data) != "content" { |
| 725 | + t.Errorf("copied = %q", string(data)) |
| 726 | + } |
| 727 | + |
| 728 | + // Verify permissions are preserved. |
| 729 | + srcInfo, _ := os.Stat(src) |
| 730 | + dstInfo, _ := os.Stat(dst) |
| 731 | + if srcInfo.Mode().Perm() != dstInfo.Mode().Perm() { |
| 732 | + t.Errorf("permissions: src=%o, dst=%o", srcInfo.Mode().Perm(), dstInfo.Mode().Perm()) |
| 733 | + } |
| 734 | +} |
| 735 | + |
| 736 | +func TestCopyDirRecursive(t *testing.T) { |
| 737 | + dir := t.TempDir() |
| 738 | + src := filepath.Join(dir, "src") |
| 739 | + dst := filepath.Join(dir, "dst") |
| 740 | + os.MkdirAll(filepath.Join(src, "sub"), 0o755) |
| 741 | + os.WriteFile(filepath.Join(src, "a.txt"), []byte("aaa"), 0o644) |
| 742 | + os.WriteFile(filepath.Join(src, "sub", "b.txt"), []byte("bbb"), 0o644) |
| 743 | + |
| 744 | + if err := copyDirRecursive(src, dst); err != nil { |
| 745 | + t.Fatal(err) |
| 746 | + } |
| 747 | + |
| 748 | + data, err := os.ReadFile(filepath.Join(dst, "a.txt")) |
| 749 | + if err != nil { |
| 750 | + t.Fatal(err) |
| 751 | + } |
| 752 | + if string(data) != "aaa" { |
| 753 | + t.Errorf("a.txt = %q", string(data)) |
| 754 | + } |
| 755 | + |
| 756 | + data, err = os.ReadFile(filepath.Join(dst, "sub", "b.txt")) |
| 757 | + if err != nil { |
| 758 | + t.Fatal(err) |
| 759 | + } |
| 760 | + if string(data) != "bbb" { |
| 761 | + t.Errorf("sub/b.txt = %q", string(data)) |
| 762 | + } |
| 763 | +} |
| 764 | + |
| 765 | +// loadConfigFrom is a helper that loads config from a specific path. |
| 766 | +func loadConfigFrom(path string) (config.Config, error) { |
| 767 | + return config.Load(path) |
| 768 | +} |
0 commit comments