mirror of
https://github.com/larksuite/cli.git
synced 2026-07-04 23:15:25 +08:00
Two issues caught in review of #1132 that the existing tests missed because they constructed RuntimeContext/CliConfig directly, bypassing the credential edge where the bug lives. P1 — Lang dropped at credential boundary credential.Account had no Lang field, so AccountFromCliConfig and ToCliConfig silently dropped cfg.Lang. The production Factory builds CliConfig via acct.ToCliConfig() (factory_default.go Phase 3), which meant RuntimeContext.Lang() always returned "" in production and shortcuts/mail/mail_signature.go always fell back to zh_cn — defeating the whole point of persisting --lang. Fix: add Lang i18n.Lang to Account and copy it in both directions. Regression test: TestFullChain_LangSurvivesProductionPath walks the real path (SaveMultiAppConfig -> DefaultAccountProvider.ResolveAccount -> ToCliConfig) and asserts Lang survives, so any future field added to CliConfig forces the same audit. P2 — priorLang ignored CurrentApp in multi-profile workspaces priorLang scanned all Apps and returned the first non-empty Lang. If a user had multiple profiles and the active one disagreed with Apps[0], a re-bind without --lang would silently inherit the wrong profile's preference. Fix: read multi.CurrentAppConfig("").Lang instead. Regression tests cover CurrentApp wins over Apps[0], single-app fallback, and malformed bytes. Change-Id: If7a276605f84f398cec329c2c942b471b4c32749
161 lines
4.9 KiB
Go
161 lines
4.9 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package credential_test
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
extcred "github.com/larksuite/cli/extension/credential"
|
|
envprovider "github.com/larksuite/cli/extension/credential/env"
|
|
"github.com/larksuite/cli/internal/core"
|
|
"github.com/larksuite/cli/internal/credential"
|
|
"github.com/larksuite/cli/internal/envvars"
|
|
"github.com/larksuite/cli/internal/i18n"
|
|
"github.com/larksuite/cli/internal/keychain"
|
|
)
|
|
|
|
type noopKC struct{}
|
|
|
|
func (n *noopKC) Get(service, account string) (string, error) { return "", nil }
|
|
func (n *noopKC) Set(service, account, value string) error { return nil }
|
|
func (n *noopKC) Remove(service, account string) error { return nil }
|
|
|
|
func TestFullChain_EnvWins(t *testing.T) {
|
|
t.Setenv(envvars.CliAppID, "env_app")
|
|
t.Setenv(envvars.CliAppSecret, "env_secret")
|
|
t.Setenv(envvars.CliUserAccessToken, "env_uat")
|
|
|
|
ep := &envprovider.Provider{}
|
|
cp := credential.NewCredentialProvider(
|
|
[]extcred.Provider{ep},
|
|
nil, nil, nil,
|
|
)
|
|
|
|
acct, err := cp.ResolveAccount(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if acct.AppID != "env_app" {
|
|
t.Errorf("expected env_app, got %s", acct.AppID)
|
|
}
|
|
|
|
result, err := cp.ResolveToken(context.Background(), credential.TokenSpec{
|
|
Type: credential.TokenTypeUAT, AppID: "env_app",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.Token != "env_uat" {
|
|
t.Errorf("expected env_uat, got %s", result.Token)
|
|
}
|
|
}
|
|
|
|
func TestFullChain_Fallthrough(t *testing.T) {
|
|
// env provider returns nil (no env vars set), falls through to default token
|
|
ep := &envprovider.Provider{}
|
|
mock := &mockDefaultTokenProvider{token: "mock_tok", scopes: "drive:read"}
|
|
|
|
cp := credential.NewCredentialProvider(
|
|
[]extcred.Provider{ep},
|
|
nil, mock, nil,
|
|
)
|
|
result, err := cp.ResolveToken(context.Background(), credential.TokenSpec{
|
|
Type: credential.TokenTypeUAT, AppID: "app1",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.Token != "mock_tok" || result.Scopes != "drive:read" {
|
|
t.Errorf("unexpected: %+v", result)
|
|
}
|
|
}
|
|
|
|
type mockDefaultTokenProvider struct {
|
|
token string
|
|
scopes string
|
|
}
|
|
|
|
func (m *mockDefaultTokenProvider) ResolveToken(ctx context.Context, req credential.TokenSpec) (*credential.TokenResult, error) {
|
|
return &credential.TokenResult{Token: m.token, Scopes: m.scopes}, nil
|
|
}
|
|
|
|
func TestFullChain_ConfigStrictMode(t *testing.T) {
|
|
t.Setenv(envvars.CliAppID, "")
|
|
t.Setenv(envvars.CliAppSecret, "")
|
|
dir := t.TempDir()
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", dir)
|
|
|
|
botMode := core.StrictModeBot
|
|
multi := &core.MultiAppConfig{
|
|
Apps: []core.AppConfig{{
|
|
AppId: "cfg_app",
|
|
AppSecret: core.PlainSecret("cfg_secret"),
|
|
Brand: core.BrandLark,
|
|
StrictMode: &botMode,
|
|
}},
|
|
}
|
|
if err := core.SaveMultiAppConfig(multi); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
ep := &envprovider.Provider{}
|
|
defaultAcct := credential.NewDefaultAccountProvider(func() keychain.KeychainAccess { return &noopKC{} }, "")
|
|
|
|
cp := credential.NewCredentialProvider(
|
|
[]extcred.Provider{ep},
|
|
defaultAcct, nil, nil,
|
|
)
|
|
|
|
acct, err := cp.ResolveAccount(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if acct.SupportedIdentities != uint8(extcred.SupportsBot) {
|
|
t.Errorf("expected SupportsBot (%d), got %d", extcred.SupportsBot, acct.SupportedIdentities)
|
|
}
|
|
}
|
|
|
|
// TestFullChain_LangSurvivesProductionPath exercises the exact data flow the
|
|
// production Factory uses (factory_default.go Phase 3): disk → multi config →
|
|
// DefaultAccountProvider.ResolveAccount → Account → ToCliConfig. If Lang gets
|
|
// dropped at the credential boundary (as it would when Account lacks the field),
|
|
// shortcuts/common/runner.go RuntimeContext.Lang() returns "" and downstream
|
|
// consumers (mail signature, etc.) silently fall back to defaults — defeating
|
|
// the whole point of persisting --lang.
|
|
func TestFullChain_LangSurvivesProductionPath(t *testing.T) {
|
|
t.Setenv(envvars.CliAppID, "")
|
|
t.Setenv(envvars.CliAppSecret, "")
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
|
|
multi := &core.MultiAppConfig{
|
|
Apps: []core.AppConfig{{
|
|
AppId: "cfg_app",
|
|
AppSecret: core.PlainSecret("cfg_secret"),
|
|
Brand: core.BrandFeishu,
|
|
Lang: i18n.LangJaJP,
|
|
}},
|
|
}
|
|
if err := core.SaveMultiAppConfig(multi); err != nil {
|
|
t.Fatalf("SaveMultiAppConfig: %v", err)
|
|
}
|
|
|
|
defaultAcct := credential.NewDefaultAccountProvider(func() keychain.KeychainAccess { return &noopKC{} }, "")
|
|
acct, err := defaultAcct.ResolveAccount(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("ResolveAccount: %v", err)
|
|
}
|
|
if acct.Lang != i18n.LangJaJP {
|
|
t.Errorf("Account.Lang = %q, want %q (DefaultAccountProvider must propagate Lang from config)", acct.Lang, i18n.LangJaJP)
|
|
}
|
|
|
|
cfg := acct.ToCliConfig()
|
|
if cfg == nil {
|
|
t.Fatal("ToCliConfig() = nil")
|
|
}
|
|
if cfg.Lang != i18n.LangJaJP {
|
|
t.Errorf("CliConfig.Lang = %q, want %q (this is the value RuntimeContext.Lang() reads in production)", cfg.Lang, i18n.LangJaJP)
|
|
}
|
|
}
|