bf9395e022
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
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)
|
|
}
|
|
}
|