Files
larksuite-cli/internal/transport/transport_test.go
liangshuo-1 4710a294f5 refactor(transport): own all HTTP transport in internal/transport, fix util layering inversion (#1213)
internal/util imported internal/proxyplugin (SharedTransport, FallbackTransport,
NewHTTPClient, and WarnIfProxied via proxyPluginStatus), so a foundational util
package depended up into a feature package, pulling binding/core/vfs into the
transitive cone of every util importer.

Move internal/proxyplugin -> internal/transport and make it the single owner of
outbound transport: fold the two SharedTransport functions into one Shared()
(proxy-plugin override -> LARK_CLI_NO_PROXY -> http.DefaultTransport), and move
Fallback/NewHTTPClient/WarnIfProxied/DetectProxyEnv/noProxyTransport out of the
now-deleted internal/util/proxy.go into the new package. The proxy-plugin probe
is demoted to a private pluginTransport(); the duplicate redactProxyURL collapses
to one. internal/util keeps no proxy code and is a leaf again.

Re-point all consumers (registry, doctor, config, auth, cmdutil, update) to
internal/transport. Behavior-preserving: package move + symbol rename + dedup.
Two new tests lock the fail-closed contract (plugin overrides NO_PROXY; malformed
config never falls through to direct egress).
2026-06-02 16:10:35 +08:00

196 lines
5.9 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package transport
import (
"io"
"net/http"
"net/url"
"strings"
"sync"
"testing"
)
func resetProxyPluginState() {
loadOnce = sync.Once{}
loadCfg = nil
loadErr = nil
proxyPluginTransport = sync.OnceValue(buildProxyPluginTransport)
cachedBlockedTransport = sync.OnceValue(buildBlockedTransport)
}
func TestPluginTransport_NotConfigured(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
unsetProxyPluginEnv(t)
resetProxyPluginState()
tr, ok := pluginTransport()
if ok {
t.Fatalf("pluginTransport() ok = true, want false")
}
if tr != nil {
t.Fatalf("pluginTransport() transport = %T, want nil", tr)
}
}
func TestPluginTransport_EnabledReturnsFixedProxy(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
unsetProxyPluginEnv(t)
resetProxyPluginState()
cfgPath := Path()
writeFile(t, cfgPath, []byte(`{
"LARKSUITE_CLI_PROXY_ENABLE": true,
"LARKSUITE_CLI_PROXY_ADDRESS": "http://127.0.0.1:3128",
"LARKSUITE_CLI_CA_PATH": ""
}`), 0600)
rt, ok := pluginTransport()
if !ok {
t.Fatal("pluginTransport() ok = false, want true")
}
tr, ok := rt.(*http.Transport)
if !ok {
t.Fatalf("pluginTransport() = %T, want *http.Transport", rt)
}
u, err := tr.Proxy(&http.Request{URL: &url.URL{Scheme: "https", Host: "open.feishu.cn"}})
if err != nil {
t.Fatalf("Proxy() error = %v", err)
}
if u == nil || u.String() != "http://127.0.0.1:3128" {
t.Fatalf("Proxy() = %v, want http://127.0.0.1:3128", u)
}
}
func TestPluginTransport_InvalidConfigWithNonTransportDefaultFailsClosed(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
unsetProxyPluginEnv(t)
resetProxyPluginState()
restoreDefaultTransport := replaceDefaultTransport(okRoundTripper{})
defer restoreDefaultTransport()
writeFile(t, Path(), []byte(`{`), 0600)
rt, ok := pluginTransport()
if !ok {
t.Fatal("pluginTransport() ok = false, want true")
}
if rt == http.DefaultTransport {
t.Fatalf("pluginTransport() returned http.DefaultTransport, want fail-closed transport")
}
resp, err := rt.RoundTrip(&http.Request{URL: &url.URL{Scheme: "https", Host: "open.feishu.cn"}})
if err == nil {
t.Fatalf("RoundTrip() error = nil, response = %#v; want fail-closed error", resp)
}
if resp != nil {
t.Fatalf("RoundTrip() response = %#v, want nil", resp)
}
}
func TestPluginTransport_InvalidConfigReturnsCachedInstance(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
unsetProxyPluginEnv(t)
resetProxyPluginState()
writeFile(t, Path(), []byte(`{`), 0600)
a, ok := pluginTransport()
if !ok {
t.Fatal("pluginTransport() ok = false, want true")
}
b, ok := pluginTransport()
if !ok {
t.Fatal("pluginTransport() ok = false, want true")
}
if a != b {
t.Fatalf("pluginTransport() returned different instances on repeated calls; blocked transport must be cached")
}
}
func TestBuildProxyPluginTransport_InvalidConfigFailsClosed(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
unsetProxyPluginEnv(t)
resetProxyPluginState()
writeFile(t, Path(), []byte(`{`), 0600)
rt := buildProxyPluginTransport()
if rt == http.DefaultTransport {
t.Fatalf("buildProxyPluginTransport() returned http.DefaultTransport, want fail-closed transport")
}
resp, err := rt.RoundTrip(&http.Request{URL: &url.URL{Scheme: "https", Host: "open.feishu.cn"}})
if err == nil {
t.Fatalf("RoundTrip() error = nil, response = %#v; want fail-closed error", resp)
}
if resp != nil {
t.Fatalf("RoundTrip() response = %#v, want nil", resp)
}
}
func TestBuildProxyPluginTransport_NonTransportDefaultFailsClosed(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
unsetProxyPluginEnv(t)
resetProxyPluginState()
restoreDefaultTransport := replaceDefaultTransport(okRoundTripper{})
defer restoreDefaultTransport()
rt := buildProxyPluginTransport()
if rt == http.DefaultTransport {
t.Fatalf("buildProxyPluginTransport() returned http.DefaultTransport, want fail-closed transport")
}
resp, err := rt.RoundTrip(&http.Request{URL: &url.URL{Scheme: "https", Host: "open.feishu.cn"}})
if err == nil {
t.Fatalf("RoundTrip() error = nil, response = %#v; want fail-closed error", resp)
}
if resp != nil {
t.Fatalf("RoundTrip() response = %#v, want nil", resp)
}
}
// TestPluginTransport_InvalidConfigBlockerIsConcreteTransport guards the
// fail-closed invariant that Fallback relies on: even when
// http.DefaultTransport is not an *http.Transport, an invalid proxy config must
// produce a blocked transport that is itself a concrete *http.Transport. If it
// were a bare RoundTripper, Fallback would downcast-fail and
// silently degrade it into a direct-egress transport.
func TestPluginTransport_InvalidConfigBlockerIsConcreteTransport(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
unsetProxyPluginEnv(t)
resetProxyPluginState()
restoreDefaultTransport := replaceDefaultTransport(okRoundTripper{})
defer restoreDefaultTransport()
writeFile(t, Path(), []byte(`{`), 0600)
rt, ok := pluginTransport()
if !ok {
t.Fatal("pluginTransport() ok = false, want true")
}
if _, isTransport := rt.(*http.Transport); !isTransport {
t.Fatalf("pluginTransport() blocked transport = %T, want *http.Transport so Fallback cannot degrade it to direct egress", rt)
}
// Must remain fail-closed.
resp, err := rt.RoundTrip(&http.Request{URL: &url.URL{Scheme: "https", Host: "open.feishu.cn"}})
if err == nil {
t.Fatalf("RoundTrip() error = nil, response = %#v; want fail-closed error", resp)
}
if resp != nil {
t.Fatalf("RoundTrip() response = %#v, want nil", resp)
}
}
type okRoundTripper struct{}
func (okRoundTripper) RoundTrip(*http.Request) (*http.Response, error) {
return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(""))}, nil
}
func replaceDefaultTransport(rt http.RoundTripper) func() {
original := http.DefaultTransport
http.DefaultTransport = rt
return func() {
http.DefaultTransport = original
}
}