forked from Gitlink/gitlink-cli
test(i18n): close auth config validation gaps
This commit is contained in:
parent
9f0c63d2fb
commit
ceede4ea59
|
|
@ -23,5 +23,8 @@ jobs:
|
|||
- name: Validate i18n messages
|
||||
run: go run ./internal/i18n/cmd/check
|
||||
|
||||
- name: Scan i18n key references
|
||||
run: go run ./internal/i18n/cmd/check --scan-code
|
||||
|
||||
- name: Run Go tests
|
||||
run: go test ./...
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import (
|
|||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/term"
|
||||
|
|
@ -18,6 +17,11 @@ import (
|
|||
|
||||
const envTokenVar = "GITLINK_TOKEN"
|
||||
|
||||
var (
|
||||
storeToken = internalAuth.StoreToken
|
||||
loadToken = internalAuth.LoadToken
|
||||
)
|
||||
|
||||
func NewAuthCmd(tr *i18n.Translator) *cobra.Command {
|
||||
if tr == nil {
|
||||
tr = i18n.Default()
|
||||
|
|
@ -42,22 +46,21 @@ func newLoginCmd(tr *i18n.Translator) *cobra.Command {
|
|||
if tokenMode {
|
||||
return loginWithToken(cmd.InOrStdin(), cmd.OutOrStdout(), tr)
|
||||
}
|
||||
return loginWithPassword(cmd.OutOrStdout(), tr)
|
||||
return loginWithPassword(cmd.InOrStdin(), cmd.OutOrStdout(), tr)
|
||||
},
|
||||
}
|
||||
cmd.Flags().BoolVar(&tokenMode, "token", false, tr.T("flag.auth.token"))
|
||||
return cmd
|
||||
}
|
||||
|
||||
func loginWithPassword(out io.Writer, tr *i18n.Translator) error {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
func loginWithPassword(in io.Reader, out io.Writer, tr *i18n.Translator) error {
|
||||
reader := bufio.NewReader(in)
|
||||
fmt.Fprint(out, tr.T("prompt.auth.username"))
|
||||
username, _ := reader.ReadString('\n')
|
||||
username = strings.TrimSpace(username)
|
||||
|
||||
fmt.Fprint(out, tr.T("prompt.auth.password"))
|
||||
passwordBytes, err := term.ReadPassword(int(syscall.Stdin))
|
||||
passwordBytes, err := readPassword(in, reader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read password: %w", err)
|
||||
}
|
||||
|
|
@ -73,6 +76,20 @@ func loginWithPassword(out io.Writer, tr *i18n.Translator) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func readPassword(in io.Reader, reader *bufio.Reader) ([]byte, error) {
|
||||
if file, ok := in.(*os.File); ok {
|
||||
fd := int(file.Fd())
|
||||
if term.IsTerminal(fd) {
|
||||
return term.ReadPassword(fd)
|
||||
}
|
||||
}
|
||||
password, err := reader.ReadString('\n')
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
return []byte(strings.TrimRight(password, "\r\n")), nil
|
||||
}
|
||||
|
||||
func loginWithToken(in io.Reader, out io.Writer, tr *i18n.Translator) error {
|
||||
reader := bufio.NewReader(in)
|
||||
fmt.Fprint(out, tr.T("prompt.auth.token"))
|
||||
|
|
@ -83,7 +100,7 @@ func loginWithToken(in io.Reader, out io.Writer, tr *i18n.Translator) error {
|
|||
return errors.New(tr.T("error.auth.token_empty"))
|
||||
}
|
||||
|
||||
if err := internalAuth.StoreToken(token); err != nil {
|
||||
if err := storeToken(token); err != nil {
|
||||
return errors.New(tr.Tf("error.auth.store_token_failed", i18n.Args{"message": err.Error()}))
|
||||
}
|
||||
|
||||
|
|
@ -97,7 +114,7 @@ func newLogoutCmd(tr *i18n.Translator) *cobra.Command {
|
|||
Short: tr.T("cmd.auth.logout.short"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if err := internalAuth.DeleteToken(); err != nil {
|
||||
return fmt.Errorf("failed to delete token: %w", err)
|
||||
return errors.New(tr.Tf("error.auth.delete_token_failed", i18n.Args{"message": err.Error()}))
|
||||
}
|
||||
fmt.Fprintln(cmd.OutOrStdout(), tr.T("success.auth.logged_out"))
|
||||
return nil
|
||||
|
|
@ -115,7 +132,7 @@ func newStatusCmd(tr *i18n.Translator) *cobra.Command {
|
|||
fmt.Fprintln(cmd.OutOrStdout(), tr.Tf("success.auth.logged_in_via_env", i18n.Args{"env": envTokenVar}))
|
||||
}
|
||||
|
||||
token, err := internalAuth.LoadToken()
|
||||
token, err := loadToken()
|
||||
if err != nil || token == "" {
|
||||
if os.Getenv(envTokenVar) == "" {
|
||||
fmt.Fprintln(cmd.OutOrStdout(), tr.T("warning.auth.not_logged_in"))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/internal/i18n"
|
||||
)
|
||||
|
||||
func TestAuthTokenLoginUsesLocalizedPrompt(t *testing.T) {
|
||||
tr, err := i18n.New(i18n.Options{Locale: "zh-CN"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
oldStoreToken := storeToken
|
||||
var storedToken string
|
||||
storeToken = func(token string) error {
|
||||
storedToken = token
|
||||
return nil
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
storeToken = oldStoreToken
|
||||
})
|
||||
|
||||
cmd := NewAuthCmd(tr)
|
||||
cmd.SetArgs([]string{"login", "--token"})
|
||||
cmd.SetIn(strings.NewReader("test-token\n"))
|
||||
var out bytes.Buffer
|
||||
cmd.SetOut(&out)
|
||||
cmd.SetErr(&out)
|
||||
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if storedToken != "test-token" {
|
||||
t.Fatalf("stored token = %q, want test-token", storedToken)
|
||||
}
|
||||
output := out.String()
|
||||
if !strings.Contains(output, "粘贴你的访问 Token") {
|
||||
t.Fatalf("expected localized token prompt, got:\n%s", output)
|
||||
}
|
||||
if !strings.Contains(output, "Token 已保存") {
|
||||
t.Fatalf("expected localized token saved message, got:\n%s", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthStatusUsesLocalizedNotLoggedIn(t *testing.T) {
|
||||
t.Setenv("GITLINK_CONFIG_DIR", t.TempDir())
|
||||
t.Setenv(envTokenVar, "")
|
||||
|
||||
tr, err := i18n.New(i18n.Options{Locale: "zh-CN"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
oldLoadToken := loadToken
|
||||
loadToken = func() (string, error) {
|
||||
return "", os.ErrNotExist
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
loadToken = oldLoadToken
|
||||
})
|
||||
|
||||
cmd := NewAuthCmd(tr)
|
||||
cmd.SetArgs([]string{"status"})
|
||||
var out bytes.Buffer
|
||||
cmd.SetOut(&out)
|
||||
cmd.SetErr(&out)
|
||||
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
output := out.String()
|
||||
if !strings.Contains(output, "未登录") {
|
||||
t.Fatalf("expected localized not logged in message, got:\n%s", output)
|
||||
}
|
||||
if !strings.Contains(output, "gitlink-cli auth login") {
|
||||
t.Fatalf("expected login hint, got:\n%s", output)
|
||||
}
|
||||
}
|
||||
|
|
@ -49,7 +49,10 @@ func newSetCmd(tr *i18n.Translator) *cobra.Command {
|
|||
if err := internalConfig.Set(args[0], args[1]); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("✓ %s = %s\n", args[0], args[1])
|
||||
fmt.Fprintln(cmd.OutOrStdout(), tr.Tf("success.config.set", i18n.Args{
|
||||
"key": args[0],
|
||||
"value": args[1],
|
||||
}))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/internal/i18n"
|
||||
)
|
||||
|
||||
func TestConfigSetUsesCommandOutputAndI18n(t *testing.T) {
|
||||
t.Setenv("GITLINK_CONFIG_DIR", t.TempDir())
|
||||
|
||||
tr, err := i18n.New(i18n.Options{Locale: "zh-CN"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cmd := NewConfigCmd(tr)
|
||||
cmd.SetArgs([]string{"set", "lang", "zh-CN"})
|
||||
var out bytes.Buffer
|
||||
cmd.SetOut(&out)
|
||||
cmd.SetErr(&out)
|
||||
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
output := out.String()
|
||||
if !strings.Contains(output, "已设置 lang = zh-CN") {
|
||||
t.Fatalf("expected localized config set output, got:\n%s", output)
|
||||
}
|
||||
}
|
||||
|
|
@ -55,6 +55,13 @@ Do not invent numbered keys such as `msg001`. Prefer names that describe ownersh
|
|||
|
||||
Use `go run ./internal/i18n/cmd/check --fix` to format locale JSON.
|
||||
|
||||
Use `go run ./internal/i18n/cmd/check --scan-code` before opening a PR. The scanner is intentionally lightweight:
|
||||
|
||||
- Name command-construction translators `tr` when calling `tr.T(...)` or `tr.Tf(...)`.
|
||||
- Use `ctx.Tr.T(...)` or `ctx.Tr.Tf(...)` in runtime shortcut code.
|
||||
- Avoid calling translator methods through other variable names such as `translator.T(...)`; the current scan may not detect them.
|
||||
- Do not add new `i18n.Default().T(...)` or `i18n.Default().Tf(...)` usages.
|
||||
|
||||
## Runtime Access
|
||||
|
||||
Command construction receives `*i18n.Translator` from `NewRootCmd`. Shortcut execution receives the same translator through `RuntimeContext.Tr`.
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@
|
|||
"cmd.webhook.test.short": "Trigger a test delivery for a webhook",
|
||||
"cmd.webhook.update.short": "Update a repository webhook while preserving unspecified fields when available",
|
||||
"cmd.webhook.view.short": "View webhook details",
|
||||
"error.auth.delete_token_failed": "failed to delete token: {message}",
|
||||
"error.auth.login_failed": "login failed: {message}",
|
||||
"error.auth.store_token_failed": "failed to store token: {message}",
|
||||
"error.auth.token_empty": "token cannot be empty",
|
||||
|
|
@ -178,6 +179,7 @@
|
|||
"success.auth.logged_out": "✓ Logged out",
|
||||
"success.auth.token_saved": "✓ Token saved",
|
||||
"success.config.initialized": "✓ Config initialized at {path}",
|
||||
"success.config.set": "✓ {key} = {value}",
|
||||
"warning.auth.not_logged_in": "✗ Not logged in",
|
||||
"warning.auth.token_unverified": "✓ Token stored (but cannot verify: {message})",
|
||||
"warning.auth.user_unavailable": "✓ Token stored (user info unavailable)"
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@
|
|||
"cmd.webhook.test.short": "触发 Webhook 测试投递",
|
||||
"cmd.webhook.update.short": "更新仓库 Webhook,并在可用时保留未指定字段",
|
||||
"cmd.webhook.view.short": "查看 Webhook 详情",
|
||||
"error.auth.delete_token_failed": "删除 Token 失败:{message}",
|
||||
"error.auth.login_failed": "登录失败:{message}",
|
||||
"error.auth.store_token_failed": "保存 Token 失败:{message}",
|
||||
"error.auth.token_empty": "Token 不能为空",
|
||||
|
|
@ -178,6 +179,7 @@
|
|||
"success.auth.logged_out": "✓ 已退出登录",
|
||||
"success.auth.token_saved": "✓ Token 已保存",
|
||||
"success.config.initialized": "✓ 配置已初始化:{path}",
|
||||
"success.config.set": "✓ 已设置 {key} = {value}",
|
||||
"warning.auth.not_logged_in": "✗ 未登录",
|
||||
"warning.auth.token_unverified": "✓ Token 已保存(但无法验证:{message})",
|
||||
"warning.auth.user_unavailable": "✓ Token 已保存(用户信息不可用)"
|
||||
|
|
|
|||
Loading…
Reference in New Issue