Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:22:54 +08:00

266 lines
8.7 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
//go:build authsidecar
package sidecar
import (
"bytes"
"errors"
"io"
"net/http"
"testing"
"github.com/larksuite/cli/sidecar"
)
// failingBody is a ReadCloser that errors on Read and tracks Close calls.
type failingBody struct {
err error
closed bool
readCall bool
}
func (b *failingBody) Read(p []byte) (int, error) {
b.readCall = true
return 0, b.err
}
func (b *failingBody) Close() error {
b.closed = true
return nil
}
func TestInterceptor_PreRoundTrip(t *testing.T) {
key := []byte("test-key-for-hmac-signing-32byte!")
interceptor := &Interceptor{key: key, sidecarHost: "127.0.0.1:16384"}
body := []byte(`{"msg":"hello"}`)
req, _ := http.NewRequest("POST", "https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=chat_id", io.NopCloser(bytes.NewReader(body)))
req.Header.Set("Authorization", "Bearer "+sidecar.SentinelUAT)
req.Header.Set("X-Cli-Source", "lark-cli")
post := interceptor.PreRoundTrip(req)
if post != nil {
t.Error("expected nil post hook")
}
// URL should be rewritten to sidecar
if req.URL.Scheme != "http" {
t.Errorf("scheme = %q, want %q", req.URL.Scheme, "http")
}
if req.URL.Host != "127.0.0.1:16384" {
t.Errorf("host = %q, want %q", req.URL.Host, "127.0.0.1:16384")
}
// Original target should be preserved
target := req.Header.Get(sidecar.HeaderProxyTarget)
if target != "https://open.feishu.cn" {
t.Errorf("target = %q, want %q", target, "https://open.feishu.cn")
}
// Identity should be user (from SentinelUAT)
if identity := req.Header.Get(sidecar.HeaderProxyIdentity); identity != sidecar.IdentityUser {
t.Errorf("identity = %q, want %q", identity, sidecar.IdentityUser)
}
// Authorization should be stripped
if auth := req.Header.Get("Authorization"); auth != "" {
t.Errorf("Authorization header should be stripped, got %q", auth)
}
// HMAC headers should be set
if sig := req.Header.Get(sidecar.HeaderProxySignature); sig == "" {
t.Error("signature header should be set")
}
if ts := req.Header.Get(sidecar.HeaderProxyTimestamp); ts == "" {
t.Error("timestamp header should be set")
}
if sha := req.Header.Get(sidecar.HeaderBodySHA256); sha == "" {
t.Error("body SHA256 header should be set")
}
if v := req.Header.Get(sidecar.HeaderProxyVersion); v != sidecar.ProtocolV1 {
t.Errorf("version header = %q, want %q", v, sidecar.ProtocolV1)
}
// Non-proxy headers should be preserved
if src := req.Header.Get("X-Cli-Source"); src != "lark-cli" {
t.Errorf("X-Cli-Source should be preserved, got %q", src)
}
// Body should still be readable
readBody, _ := io.ReadAll(req.Body)
if !bytes.Equal(readBody, body) {
t.Errorf("body should be preserved after PreRoundTrip")
}
}
func TestInterceptor_BotIdentity(t *testing.T) {
interceptor := &Interceptor{key: []byte("key"), sidecarHost: "127.0.0.1:16384"}
req, _ := http.NewRequest("GET", "https://open.feishu.cn/open-apis/calendar/v4/events", nil)
req.Header.Set("Authorization", "Bearer "+sidecar.SentinelTAT)
interceptor.PreRoundTrip(req)
if identity := req.Header.Get(sidecar.HeaderProxyIdentity); identity != sidecar.IdentityBot {
t.Errorf("identity = %q, want %q", identity, sidecar.IdentityBot)
}
}
func TestInterceptor_NonSentinelToken_PassThrough(t *testing.T) {
interceptor := &Interceptor{key: []byte("key"), sidecarHost: "127.0.0.1:16384"}
origURL := "https://some-cdn.example.com/presigned-download?token=abc"
req, _ := http.NewRequest("GET", origURL, nil)
req.Header.Set("Authorization", "Bearer some-real-token")
post := interceptor.PreRoundTrip(req)
// Should NOT be rewritten — no sentinel token
if post != nil {
t.Error("expected nil post hook for pass-through")
}
if req.URL.String() != origURL {
t.Errorf("URL should be unchanged, got %q", req.URL.String())
}
if req.Header.Get(sidecar.HeaderProxyTarget) != "" {
t.Error("proxy target header should not be set for pass-through")
}
if req.Header.Get("Authorization") != "Bearer some-real-token" {
t.Error("Authorization should be preserved for pass-through")
}
}
func TestInterceptor_NoAuth_PassThrough(t *testing.T) {
interceptor := &Interceptor{key: []byte("key"), sidecarHost: "127.0.0.1:16384"}
origURL := "https://cdn.feishu.cn/download/file"
req, _ := http.NewRequest("GET", origURL, nil)
interceptor.PreRoundTrip(req)
// No Authorization header at all — should pass through
if req.URL.String() != origURL {
t.Errorf("URL should be unchanged for no-auth request, got %q", req.URL.String())
}
}
func TestInterceptor_MCP_UAT(t *testing.T) {
interceptor := &Interceptor{key: []byte("key"), sidecarHost: "127.0.0.1:16384"}
req, _ := http.NewRequest("POST", "https://mcp.feishu.cn/mcp/v1/tools/call", bytes.NewReader([]byte(`{"jsonrpc":"2.0"}`)))
req.Header.Set(sidecar.HeaderMCPUAT, sidecar.SentinelUAT)
interceptor.PreRoundTrip(req)
// Should be intercepted and rewritten
if req.URL.Host != "127.0.0.1:16384" {
t.Errorf("host = %q, want sidecar host", req.URL.Host)
}
if identity := req.Header.Get(sidecar.HeaderProxyIdentity); identity != sidecar.IdentityUser {
t.Errorf("identity = %q, want %q", identity, sidecar.IdentityUser)
}
if ah := req.Header.Get(sidecar.HeaderProxyAuthHeader); ah != sidecar.HeaderMCPUAT {
t.Errorf("auth header = %q, want %q", ah, sidecar.HeaderMCPUAT)
}
// MCP sentinel should be stripped
if v := req.Header.Get(sidecar.HeaderMCPUAT); v != "" {
t.Errorf("MCP-UAT should be stripped, got %q", v)
}
}
func TestInterceptor_MCP_TAT(t *testing.T) {
interceptor := &Interceptor{key: []byte("key"), sidecarHost: "127.0.0.1:16384"}
req, _ := http.NewRequest("POST", "https://mcp.feishu.cn/mcp/v1/tools/call", bytes.NewReader([]byte(`{}`)))
req.Header.Set(sidecar.HeaderMCPTAT, sidecar.SentinelTAT)
interceptor.PreRoundTrip(req)
if identity := req.Header.Get(sidecar.HeaderProxyIdentity); identity != sidecar.IdentityBot {
t.Errorf("identity = %q, want %q", identity, sidecar.IdentityBot)
}
if ah := req.Header.Get(sidecar.HeaderProxyAuthHeader); ah != sidecar.HeaderMCPTAT {
t.Errorf("auth header = %q, want %q", ah, sidecar.HeaderMCPTAT)
}
}
func TestInterceptor_StandardAuth_SetsAuthorizationHeader(t *testing.T) {
interceptor := &Interceptor{key: []byte("key"), sidecarHost: "127.0.0.1:16384"}
req, _ := http.NewRequest("GET", "https://open.feishu.cn/open-apis/test", nil)
req.Header.Set("Authorization", "Bearer "+sidecar.SentinelUAT)
interceptor.PreRoundTrip(req)
if ah := req.Header.Get(sidecar.HeaderProxyAuthHeader); ah != "Authorization" {
t.Errorf("auth header = %q, want %q", ah, "Authorization")
}
}
// TestInterceptor_BodyReadError verifies that when io.ReadAll on the request
// body fails partway, PreRoundTrip skips the rewrite entirely rather than
// signing a truncated body (which would produce a misleading HMAC mismatch on
// the sidecar side) and releases the original body.
func TestInterceptor_BodyReadError(t *testing.T) {
interceptor := &Interceptor{key: []byte("key"), sidecarHost: "127.0.0.1:16384"}
const origURL = "https://open.feishu.cn/open-apis/im/v1/messages"
body := &failingBody{err: errors.New("disk gremlin")}
req, _ := http.NewRequest("POST", origURL, body)
req.Header.Set("Authorization", "Bearer "+sidecar.SentinelUAT)
post := interceptor.PreRoundTrip(req)
if post != nil {
t.Error("expected nil post hook on body read failure")
}
// Original body must be closed to avoid leaking fd/pipe-like resources.
if !body.readCall {
t.Error("expected ReadAll to have attempted reading from the body")
}
if !body.closed {
t.Error("expected original body to be Close()'d after read failure")
}
// URL must NOT be rewritten — request should fall through to the next
// layer (credential) which can surface a meaningful error.
if req.URL.String() != origURL {
t.Errorf("URL should be unchanged on read failure, got %q", req.URL.String())
}
// No proxy/HMAC headers should leak onto the request.
for _, h := range []string{
sidecar.HeaderProxyVersion,
sidecar.HeaderProxyTarget,
sidecar.HeaderProxySignature,
sidecar.HeaderProxyTimestamp,
sidecar.HeaderBodySHA256,
sidecar.HeaderProxyIdentity,
sidecar.HeaderProxyAuthHeader,
} {
if v := req.Header.Get(h); v != "" {
t.Errorf("%s should not be set on read failure, got %q", h, v)
}
}
}
func TestInterceptor_EmptyBody(t *testing.T) {
interceptor := &Interceptor{key: []byte("key"), sidecarHost: "127.0.0.1:16384"}
req, _ := http.NewRequest("GET", "https://open.feishu.cn/path", nil)
req.Header.Set("Authorization", "Bearer "+sidecar.SentinelTAT)
interceptor.PreRoundTrip(req)
sha := req.Header.Get(sidecar.HeaderBodySHA256)
expectedEmpty := sidecar.BodySHA256(nil)
if sha != expectedEmpty {
t.Errorf("body SHA256 = %q, want empty-string SHA256 %q", sha, expectedEmpty)
}
}