mirror of
https://github.com/larksuite/cli.git
synced 2026-07-07 17:45:15 +08:00
270 lines
8.1 KiB
Go
270 lines
8.1 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package util
|
|
|
|
import (
|
|
"bytes"
|
|
"net/http"
|
|
"os"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/larksuite/cli/internal/envvars"
|
|
)
|
|
|
|
// unsetEnv clears key for the duration of the test and restores its original value.
|
|
func unsetEnv(t *testing.T, key string) {
|
|
t.Helper()
|
|
old, had := os.LookupEnv(key)
|
|
_ = os.Unsetenv(key)
|
|
t.Cleanup(func() {
|
|
if had {
|
|
_ = os.Setenv(key, old)
|
|
} else {
|
|
_ = os.Unsetenv(key)
|
|
}
|
|
})
|
|
}
|
|
|
|
// unsetSecPluginEnv clears SEC-related environment variables for deterministic tests.
|
|
func unsetSecPluginEnv(t *testing.T) {
|
|
t.Helper()
|
|
// Ensure developer machine env doesn't accidentally enable SEC plugin mode
|
|
// and change expectations for SharedTransport().
|
|
unsetEnv(t, envvars.CliSecEnable)
|
|
unsetEnv(t, envvars.CliSecProxy)
|
|
unsetEnv(t, envvars.CliSecCA)
|
|
unsetEnv(t, envvars.CliSecAuth)
|
|
}
|
|
|
|
// TestDetectProxyEnv verifies proxy environment detection priority and empty-state behavior.
|
|
func TestDetectProxyEnv(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
unsetSecPluginEnv(t)
|
|
|
|
// Clear all proxy env vars first
|
|
for _, k := range proxyEnvKeys {
|
|
t.Setenv(k, "")
|
|
}
|
|
|
|
key, val := DetectProxyEnv()
|
|
if key != "" || val != "" {
|
|
t.Errorf("expected no proxy, got %s=%s", key, val)
|
|
}
|
|
|
|
t.Setenv("HTTPS_PROXY", "http://proxy:8888")
|
|
key, val = DetectProxyEnv()
|
|
if key != "HTTPS_PROXY" || val != "http://proxy:8888" {
|
|
t.Errorf("expected HTTPS_PROXY=http://proxy:8888, got %s=%s", key, val)
|
|
}
|
|
}
|
|
|
|
// TestSharedTransport_DefaultReturnsStdlibSingleton verifies the default shared transport.
|
|
func TestSharedTransport_DefaultReturnsStdlibSingleton(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
unsetSecPluginEnv(t)
|
|
t.Setenv(EnvNoProxy, "")
|
|
tr := SharedTransport()
|
|
if tr != http.DefaultTransport {
|
|
t.Error("SharedTransport should return http.DefaultTransport when LARK_CLI_NO_PROXY is unset")
|
|
}
|
|
}
|
|
|
|
// TestSharedTransport_NoProxyReturnsClone verifies that disabling proxying returns a cloned transport.
|
|
func TestSharedTransport_NoProxyReturnsClone(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
unsetSecPluginEnv(t)
|
|
t.Setenv(EnvNoProxy, "1")
|
|
tr := SharedTransport()
|
|
if tr == http.DefaultTransport {
|
|
t.Fatal("SharedTransport should return a clone, not DefaultTransport, when LARK_CLI_NO_PROXY is set")
|
|
}
|
|
ht, ok := tr.(*http.Transport)
|
|
if !ok {
|
|
t.Fatalf("expected *http.Transport, got %T", tr)
|
|
}
|
|
if ht.Proxy != nil {
|
|
t.Error("no-proxy transport should have Proxy == nil")
|
|
}
|
|
}
|
|
|
|
// TestSharedTransport_NoProxyIsCachedSingleton verifies singleton caching for the no-proxy transport.
|
|
func TestSharedTransport_NoProxyIsCachedSingleton(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
unsetSecPluginEnv(t)
|
|
t.Setenv(EnvNoProxy, "1")
|
|
a := SharedTransport()
|
|
b := SharedTransport()
|
|
if a != b {
|
|
t.Error("repeated SharedTransport calls with LARK_CLI_NO_PROXY set must return the same instance")
|
|
}
|
|
}
|
|
|
|
// TestSharedTransport_EnvUnsetAfterSetFallsBackToDefault verifies fallback to the stdlib transport after unsetting EnvNoProxy.
|
|
func TestSharedTransport_EnvUnsetAfterSetFallsBackToDefault(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
unsetSecPluginEnv(t)
|
|
// Simulate a process that first runs with LARK_CLI_NO_PROXY=1 (populating
|
|
// the no-proxy singleton), then unsets it. Subsequent calls must return
|
|
// http.DefaultTransport, NOT the cached no-proxy clone.
|
|
t.Setenv(EnvNoProxy, "1")
|
|
noProxy := SharedTransport()
|
|
if noProxy == http.DefaultTransport {
|
|
t.Fatal("precondition: first call with env set should not return DefaultTransport")
|
|
}
|
|
|
|
t.Setenv(EnvNoProxy, "")
|
|
after := SharedTransport()
|
|
if after != http.DefaultTransport {
|
|
t.Errorf("after unsetting LARK_CLI_NO_PROXY, SharedTransport must return http.DefaultTransport, got %T (%p)", after, after)
|
|
}
|
|
}
|
|
|
|
// TestSharedTransport_NoProxyOverridesSystemProxy verifies that EnvNoProxy disables system proxies.
|
|
func TestSharedTransport_NoProxyOverridesSystemProxy(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
unsetSecPluginEnv(t)
|
|
t.Setenv("HTTPS_PROXY", "http://should-be-ignored:8888")
|
|
t.Setenv(EnvNoProxy, "1")
|
|
|
|
ht, ok := SharedTransport().(*http.Transport)
|
|
if !ok {
|
|
t.Fatalf("expected *http.Transport, got %T", SharedTransport())
|
|
}
|
|
if ht.Proxy != nil {
|
|
t.Error("LARK_CLI_NO_PROXY should override system proxy settings")
|
|
}
|
|
}
|
|
|
|
// TestWarnIfProxied_WithProxy verifies that proxy detection emits a warning.
|
|
func TestWarnIfProxied_WithProxy(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
unsetSecPluginEnv(t)
|
|
// Reset the once guard for this test
|
|
proxyWarningOnce = sync.Once{}
|
|
|
|
t.Setenv("HTTPS_PROXY", "http://corp-proxy:3128")
|
|
|
|
var buf bytes.Buffer
|
|
WarnIfProxied(&buf)
|
|
|
|
out := buf.String()
|
|
if out == "" {
|
|
t.Error("expected warning output when proxy is set")
|
|
}
|
|
if !bytes.Contains([]byte(out), []byte("HTTPS_PROXY")) {
|
|
t.Errorf("warning should mention HTTPS_PROXY, got: %s", out)
|
|
}
|
|
if !bytes.Contains([]byte(out), []byte(EnvNoProxy)) {
|
|
t.Errorf("warning should mention %s, got: %s", EnvNoProxy, out)
|
|
}
|
|
}
|
|
|
|
// TestWarnIfProxied_WithoutProxy verifies that no warning is emitted without proxy settings.
|
|
func TestWarnIfProxied_WithoutProxy(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
unsetSecPluginEnv(t)
|
|
proxyWarningOnce = sync.Once{}
|
|
|
|
for _, k := range proxyEnvKeys {
|
|
t.Setenv(k, "")
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
WarnIfProxied(&buf)
|
|
|
|
if buf.Len() != 0 {
|
|
t.Errorf("expected no output when no proxy is set, got: %s", buf.String())
|
|
}
|
|
}
|
|
|
|
// TestWarnIfProxied_SilentWhenDisabled verifies that EnvNoProxy suppresses warnings.
|
|
func TestWarnIfProxied_SilentWhenDisabled(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
unsetSecPluginEnv(t)
|
|
proxyWarningOnce = sync.Once{}
|
|
|
|
t.Setenv("HTTPS_PROXY", "http://proxy:8080")
|
|
t.Setenv(EnvNoProxy, "1")
|
|
|
|
var buf bytes.Buffer
|
|
WarnIfProxied(&buf)
|
|
|
|
if buf.Len() != 0 {
|
|
t.Errorf("expected no warning when proxy is disabled, got: %s", buf.String())
|
|
}
|
|
}
|
|
|
|
// TestWarnIfProxied_OnlyOnce verifies that proxy warnings are emitted only once.
|
|
func TestWarnIfProxied_OnlyOnce(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
unsetSecPluginEnv(t)
|
|
proxyWarningOnce = sync.Once{}
|
|
|
|
t.Setenv("HTTP_PROXY", "http://proxy:1234")
|
|
|
|
var buf bytes.Buffer
|
|
WarnIfProxied(&buf)
|
|
first := buf.String()
|
|
|
|
WarnIfProxied(&buf)
|
|
second := buf.String()
|
|
|
|
if first == "" {
|
|
t.Error("expected warning on first call")
|
|
}
|
|
if second != first {
|
|
t.Error("expected no additional output on second call")
|
|
}
|
|
}
|
|
|
|
// TestRedactProxyURL verifies redaction of proxy credentials across supported formats.
|
|
func TestRedactProxyURL(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
unsetSecPluginEnv(t)
|
|
tests := []struct {
|
|
input string
|
|
want string
|
|
}{
|
|
{"http://proxy:8080", "http://proxy:8080"},
|
|
{"http://user:pass@proxy:8080", "http://***@proxy:8080/"},
|
|
{"http://user:p%40ss@proxy:8080/path", "http://***@proxy:8080/path"},
|
|
{"http://user@proxy:8080", "http://***@proxy:8080/"},
|
|
{"socks5://admin:secret@10.0.0.1:1080", "socks5://***@10.0.0.1:1080/"},
|
|
{"user:pass@proxy:8080", "***@proxy:8080"},
|
|
{"admin:s3cret@10.0.0.1:3128", "***@10.0.0.1:3128"},
|
|
{"not-a-url", "not-a-url"},
|
|
{"", ""},
|
|
}
|
|
for _, tt := range tests {
|
|
got := redactProxyURL(tt.input)
|
|
if got != tt.want {
|
|
t.Errorf("redactProxyURL(%q) = %q, want %q", tt.input, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestWarnIfProxied_RedactsCredentials verifies that warning output never leaks credentials.
|
|
func TestWarnIfProxied_RedactsCredentials(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
unsetSecPluginEnv(t)
|
|
proxyWarningOnce = sync.Once{}
|
|
|
|
t.Setenv("HTTPS_PROXY", "http://admin:s3cret@proxy:8080")
|
|
|
|
var buf bytes.Buffer
|
|
WarnIfProxied(&buf)
|
|
|
|
out := buf.String()
|
|
if bytes.Contains([]byte(out), []byte("s3cret")) {
|
|
t.Errorf("warning should not contain proxy password, got: %s", out)
|
|
}
|
|
if bytes.Contains([]byte(out), []byte("admin")) {
|
|
t.Errorf("warning should not contain proxy username, got: %s", out)
|
|
}
|
|
if !bytes.Contains([]byte(out), []byte("***@proxy:8080")) {
|
|
t.Errorf("warning should contain redacted proxy URL, got: %s", out)
|
|
}
|
|
}
|