test: 补充 alias/browse/status 单元测试

- 新增 cmd/alias/alias_test.go: 8 个测试(加载/保存/覆盖/删除/无效YAML/子命令结构)
- 新增 cmd/browse/browse_test.go: 5 个测试(命令结构/参数校验/示例内容)
- 新增 cmd/status/status_test.go: 5 个测试(命令结构/token来源判断)

关联 Issue: #14
This commit is contained in:
wyxfzgg 2026-06-03 12:39:48 +08:00
parent b887e5053d
commit 9474c1f432
3 changed files with 309 additions and 0 deletions

193
cmd/alias/alias_test.go Normal file
View File

@ -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)
}
}

58
cmd/browse/browse_test.go Normal file
View File

@ -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 就行
}

58
cmd/status/status_test.go Normal file
View File

@ -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)
}
}