Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
138 lines
4.8 KiB
Go
138 lines
4.8 KiB
Go
package opensearch
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/Tencent/WeKnora/internal/types"
|
|
)
|
|
|
|
// TestNewOpenSearchClient_RejectsEmptyAddr verifies the constructor
|
|
// refuses an empty Addr — operators who forget to set OPENSEARCH_ADDR
|
|
// get a clear ErrConfigInvalid at registration instead of a cryptic
|
|
// transport error later.
|
|
func TestNewOpenSearchClient_RejectsEmptyAddr(t *testing.T) {
|
|
t.Parallel()
|
|
_, err := NewOpenSearchClient(&types.ConnectionConfig{Addr: ""})
|
|
if !errors.Is(err, ErrConfigInvalid) {
|
|
t.Fatalf("empty addr: want ErrConfigInvalid, got %v", err)
|
|
}
|
|
_, err = NewOpenSearchClient(nil)
|
|
if !errors.Is(err, ErrConfigInvalid) {
|
|
t.Fatalf("nil cfg: want ErrConfigInvalid, got %v", err)
|
|
}
|
|
}
|
|
|
|
// TestNewOpenSearchClient_Succeeds_OnValidAddr is a smoke test for the
|
|
// happy path — a non-empty Addr must produce a non-nil *osapi.Client.
|
|
// We don't probe the cluster (no network), just verify the constructor
|
|
// returns successfully.
|
|
func TestNewOpenSearchClient_Succeeds_OnValidAddr(t *testing.T) {
|
|
t.Parallel()
|
|
client, err := NewOpenSearchClient(&types.ConnectionConfig{
|
|
Addr: "https://opensearch.example.com:9200",
|
|
Username: "admin",
|
|
Password: "secret", // not a real password — wire-format only
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewOpenSearchClient: %v", err)
|
|
}
|
|
if client == nil {
|
|
t.Fatal("client must be non-nil on success")
|
|
}
|
|
}
|
|
|
|
// TestBuildHTTPTransport_DefaultsToSecureTLS pins the default
|
|
// InsecureSkipVerify=false. Operators who don't opt in MUST get a
|
|
// strict-verify HTTPS client.
|
|
func TestBuildHTTPTransport_DefaultsToSecureTLS(t *testing.T) {
|
|
t.Parallel()
|
|
tr := buildHTTPTransport(false)
|
|
if tr.TLSClientConfig == nil {
|
|
t.Fatal("TLSClientConfig must be set")
|
|
}
|
|
if tr.TLSClientConfig.InsecureSkipVerify {
|
|
t.Fatal("default InsecureSkipVerify must be false (secure)")
|
|
}
|
|
}
|
|
|
|
// TestBuildHTTPTransport_InsecureSkipVerifyOptIn confirms the flag
|
|
// flows through to tls.Config. Self-signed dev clusters can opt in;
|
|
// production deployments MUST leave it false.
|
|
func TestBuildHTTPTransport_InsecureSkipVerifyOptIn(t *testing.T) {
|
|
t.Parallel()
|
|
tr := buildHTTPTransport(true)
|
|
if tr.TLSClientConfig == nil {
|
|
t.Fatal("TLSClientConfig must be set")
|
|
}
|
|
if !tr.TLSClientConfig.InsecureSkipVerify {
|
|
t.Fatal("InsecureSkipVerify=true must propagate to tls.Config")
|
|
}
|
|
}
|
|
|
|
// TestBuildHTTPTransport_TLS12MinVersion pins the TLS 1.2 hard floor.
|
|
// TLS 1.3 negotiates automatically when both ends support it; 1.0 / 1.1
|
|
// must be rejected at handshake time.
|
|
func TestBuildHTTPTransport_TLS12MinVersion(t *testing.T) {
|
|
t.Parallel()
|
|
tr := buildHTTPTransport(false)
|
|
if tr.TLSClientConfig.MinVersion != tls.VersionTLS12 {
|
|
t.Fatalf("MinVersion: want tls.VersionTLS12 (0x%x), got 0x%x",
|
|
tls.VersionTLS12, tr.TLSClientConfig.MinVersion)
|
|
}
|
|
}
|
|
|
|
// TestBuildHTTPTransport_CipherSuitesExcludeNonECDHE pins the explicit
|
|
// cipher list to forward-secrecy-only ECDHE suites. TLS 1.2's default
|
|
// list includes TLS_RSA_* (no forward secrecy); we drop them. TLS 1.3
|
|
// cipher selection is automatic so this only matters for the 1.2 path.
|
|
func TestBuildHTTPTransport_CipherSuitesExcludeNonECDHE(t *testing.T) {
|
|
t.Parallel()
|
|
tr := buildHTTPTransport(false)
|
|
cs := tr.TLSClientConfig.CipherSuites
|
|
if len(cs) == 0 {
|
|
t.Fatal("CipherSuites must be explicitly pinned (got empty)")
|
|
}
|
|
allowed := map[uint16]bool{
|
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256: true,
|
|
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256: true,
|
|
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384: true,
|
|
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384: true,
|
|
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305: true,
|
|
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305: true,
|
|
}
|
|
for _, suite := range cs {
|
|
if !allowed[suite] {
|
|
t.Fatalf("cipher suite 0x%x is not in the forward-secrecy whitelist", suite)
|
|
}
|
|
}
|
|
// Spot-check that a known-weak non-ECDHE cipher is NOT in the list.
|
|
for _, suite := range cs {
|
|
if suite == tls.TLS_RSA_WITH_AES_128_GCM_SHA256 {
|
|
t.Fatal("TLS_RSA_WITH_AES_128_GCM_SHA256 must NOT be present (no forward secrecy)")
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestBuildHTTPTransport_PoolTuning pins the connection-pool settings.
|
|
// The Go default MaxIdleConnsPerHost is 2, which is too low for concurrent
|
|
// queries across multiple knowledge bases.
|
|
func TestBuildHTTPTransport_PoolTuning(t *testing.T) {
|
|
t.Parallel()
|
|
tr := buildHTTPTransport(false)
|
|
if tr.MaxIdleConnsPerHost < 32 {
|
|
t.Errorf("MaxIdleConnsPerHost: want >=32 for concurrent KB queries, got %d",
|
|
tr.MaxIdleConnsPerHost)
|
|
}
|
|
if tr.IdleConnTimeout == 0 {
|
|
t.Error("IdleConnTimeout must be set (Go default of 0 leaks idle conns)")
|
|
}
|
|
if tr.ResponseHeaderTimeout == 0 {
|
|
t.Error("ResponseHeaderTimeout must be set (Go default of 0 means unbounded)")
|
|
}
|
|
if tr.ExpectContinueTimeout == 0 {
|
|
t.Error("ExpectContinueTimeout must be set (avoids 100-continue stalls)")
|
|
}
|
|
}
|