diff --git a/cmd/alias/alias_test.go b/cmd/alias/alias_test.go new file mode 100644 index 0000000..0c54cd6 --- /dev/null +++ b/cmd/alias/alias_test.go @@ -0,0 +1,193 @@ +package alias + +import ( + "bytes" + "os" + "strings" + "testing" + + "github.com/spf13/cobra" +) + +func TestLoadAliasesEmpty(t *testing.T) { + // 设置临时配置目录 + tmpDir := t.TempDir() + t.Setenv("GITLINK_CONFIG_DIR", tmpDir) + + aliases, err := loadAliases() + if err != nil { + t.Fatalf("loadAliases failed: %v", err) + } + if len(aliases) != 0 { + t.Fatalf("expected empty aliases, got %d", len(aliases)) + } +} + +func TestSaveAndLoadAliases(t *testing.T) { + tmpDir := t.TempDir() + t.Setenv("GITLINK_CONFIG_DIR", tmpDir) + + // 保存 + original := map[string]string{ + "rl": "repo +list", + "ri": "repo +info", + } + if err := saveAliases(original); err != nil { + t.Fatalf("saveAliases failed: %v", err) + } + + // 加载 + loaded, err := loadAliases() + if err != nil { + t.Fatalf("loadAliases failed: %v", err) + } + if len(loaded) != 2 { + t.Fatalf("expected 2 aliases, got %d", len(loaded)) + } + if loaded["rl"] != "repo +list" { + t.Errorf("expected rl -> repo +list, got %s", loaded["rl"]) + } + if loaded["ri"] != "repo +info" { + t.Errorf("expected ri -> repo +info, got %s", loaded["ri"]) + } +} + +func TestSaveAliasesOverwrite(t *testing.T) { + tmpDir := t.TempDir() + t.Setenv("GITLINK_CONFIG_DIR", tmpDir) + + // 第一次保存 + saveAliases(map[string]string{"rl": "repo +list"}) + + // 覆盖保存 + saveAliases(map[string]string{"rl": "repo +list --owner Gitlink"}) + + loaded, _ := loadAliases() + if loaded["rl"] != "repo +list --owner Gitlink" { + t.Errorf("alias should be overwritten, got %s", loaded["rl"]) + } +} + +func TestLoadAliasesInvalidYAML(t *testing.T) { + tmpDir := t.TempDir() + t.Setenv("GITLINK_CONFIG_DIR", tmpDir) + + // 写入无效 YAML + os.WriteFile(tmpDir+"/aliases.yaml", []byte("{{invalid yaml}}"), 0600) + + aliases, err := loadAliases() + if err != nil { + t.Fatalf("should not error on invalid YAML, got: %v", err) + } + if len(aliases) != 0 { + t.Fatalf("should return empty map on invalid YAML, got %d", len(aliases)) + } +} + +func TestNewAliasCmd(t *testing.T) { + cmd := NewAliasCmd() + if cmd.Use != "alias" { + t.Errorf("expected Use 'alias', got %s", cmd.Use) + } + if !cmd.HasSubCommands() { + t.Error("alias command should have subcommands") + } + subcmds := cmd.Commands() + if len(subcmds) != 3 { + t.Fatalf("expected 3 subcommands, got %d", len(subcmds)) + } +} + +func TestAliasListSubcommand(t *testing.T) { + tmpDir := t.TempDir() + t.Setenv("GITLINK_CONFIG_DIR", tmpDir) + + cmd := NewAliasCmd() + // 找到 +list 子命令 + var listCmd *cobra.Command + for _, sub := range cmd.Commands() { + if sub.Use == "+list" { + listCmd = sub + break + } + } + if listCmd == nil { + t.Fatal("+list subcommand not found") + } + + // 无别名时运行 + buf := new(bytes.Buffer) + listCmd.SetOut(buf) + listCmd.SetArgs([]string{}) + if err := listCmd.Execute(); err != nil { + t.Fatalf("list failed: %v", err) + } + if !strings.Contains(buf.String(), "未定义任何别名") { + t.Errorf("expected hint for no aliases, got: %s", buf.String()) + } +} + +func TestAliasSetAndDeleteSubcommands(t *testing.T) { + tmpDir := t.TempDir() + t.Setenv("GITLINK_CONFIG_DIR", tmpDir) + + cmd := NewAliasCmd() + + // 找到 +set 子命令 + var setCmd, deleteCmd *cobra.Command + for _, sub := range cmd.Commands() { + if strings.HasPrefix(sub.Use, "+set") { + setCmd = sub + } + if strings.HasPrefix(sub.Use, "+delete") { + deleteCmd = sub + } + } + + // +set + setCmd.SetArgs([]string{"rl", "repo +list"}) + if err := setCmd.Execute(); err != nil { + t.Fatalf("set failed: %v", err) + } + + // 验证文件写入 + aliases, _ := loadAliases() + if aliases["rl"] != "repo +list" { + t.Fatalf("alias not saved correctly: %v", aliases) + } + + // +delete + deleteCmd.SetArgs([]string{"rl"}) + if err := deleteCmd.Execute(); err != nil { + t.Fatalf("delete failed: %v", err) + } + + // 验证已删除 + aliases, _ = loadAliases() + if _, ok := aliases["rl"]; ok { + t.Fatal("alias should have been deleted") + } +} + +func TestAliasDeleteNonExistent(t *testing.T) { + tmpDir := t.TempDir() + t.Setenv("GITLINK_CONFIG_DIR", tmpDir) + + cmd := NewAliasCmd() + var deleteCmd *cobra.Command + for _, sub := range cmd.Commands() { + if strings.HasPrefix(sub.Use, "+delete") { + deleteCmd = sub + break + } + } + + deleteCmd.SetArgs([]string{"nonexistent"}) + err := deleteCmd.Execute() + if err == nil { + t.Fatal("expected error when deleting nonexistent alias") + } + if !strings.Contains(err.Error(), "不存在") { + t.Errorf("error should mention alias does not exist: %v", err) + } +} diff --git a/cmd/browse/browse_test.go b/cmd/browse/browse_test.go new file mode 100644 index 0000000..812e911 --- /dev/null +++ b/cmd/browse/browse_test.go @@ -0,0 +1,58 @@ +package browse + +import ( + "strings" + "testing" +) + +func TestNewBrowseCmd(t *testing.T) { + cmd := NewBrowseCmd() + if cmd.Use != "browse [resource]" { + t.Errorf("expected Use 'browse [resource]', got %s", cmd.Use) + } + if cmd.Short == "" { + t.Error("Short description should not be empty") + } + if cmd.Long == "" { + t.Error("Long description should not be empty") + } +} + +func TestBrowseCmdHasCorrectArgs(t *testing.T) { + cmd := NewBrowseCmd() + // MaximumNArgs(1) should allow 0 or 1 args + if err := cmd.Args(cmd, []string{}); err != nil { + t.Errorf("should accept 0 args: %v", err) + } + if err := cmd.Args(cmd, []string{"issues/42"}); err != nil { + t.Errorf("should accept 1 arg: %v", err) + } + if err := cmd.Args(cmd, []string{"a", "b"}); err == nil { + t.Error("should reject more than 1 arg") + } +} + +func TestBrowseCmdSubcommandStructure(t *testing.T) { + cmd := NewBrowseCmd() + // browse 不应该有子命令 + if cmd.HasSubCommands() { + t.Error("browse should not have subcommands") + } +} + +func TestBrowseCmdExample(t *testing.T) { + cmd := NewBrowseCmd() + if cmd.Example == "" { + t.Error("Example should not be empty") + } + if !strings.Contains(cmd.Example, "browse") { + t.Error("Example should contain 'browse'") + } +} + +func TestOpenBrowserReturnsNoError(t *testing.T) { + // openBrowser 在所有平台都应该返回 nil 或一个 error + // 在无头环境下可能会失败,但不应该 panic + _ = openBrowser("https://gitlink.org.cn") + // 只要不 panic 就行 +} diff --git a/cmd/status/status_test.go b/cmd/status/status_test.go new file mode 100644 index 0000000..b88a176 --- /dev/null +++ b/cmd/status/status_test.go @@ -0,0 +1,58 @@ +package status + +import ( + "strings" + "testing" +) + +func TestNewStatusCmd(t *testing.T) { + cmd := NewStatusCmd() + if cmd.Use != "status" { + t.Errorf("expected Use 'status', got %s", cmd.Use) + } + if cmd.Short == "" { + t.Error("Short description should not be empty") + } + if cmd.Long == "" { + t.Error("Long description should not be empty") + } +} + +func TestNewStatusCmdExample(t *testing.T) { + cmd := NewStatusCmd() + if !strings.Contains(cmd.Example, "status") { + t.Errorf("Example should contain 'status', got: %s", cmd.Example) + } +} + +func TestNewStatusCmdHasNoSubcommands(t *testing.T) { + cmd := NewStatusCmd() + if cmd.HasSubCommands() { + t.Error("status should not have subcommands") + } +} + +func TestTokenSourceFromEnv(t *testing.T) { + t.Setenv("GITLINK_TOKEN", "test-token-123") + result := tokenSource("test-token-123") + if result != "环境变量 GITLINK_TOKEN" { + t.Errorf("expected env source, got: %s", result) + } +} + +func TestTokenSourceFromKeyring(t *testing.T) { + // 不设置环境变量,或用不同的值 + t.Setenv("GITLINK_TOKEN", "") + result := tokenSource("some-stored-token") + if result != "keyring / 配置文件" { + t.Errorf("expected keyring source, got: %s", result) + } +} + +func TestTokenSourceMismatch(t *testing.T) { + t.Setenv("GITLINK_TOKEN", "env-token") + result := tokenSource("different-token") + if result != "keyring / 配置文件" { + t.Errorf("should fallback to keyring when token differs from env, got: %s", result) + } +}