mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 04:16:06 +00:00
Add comprehensive integration tests: - web_search_tenant_isolation_test: verify per-tenant provider chains, cache hit/miss behavior, event-driven invalidation - web_search_migrate_hook_test: verify migration of inline keys from config.json5 and builtin_tool_tenant_configs.settings to config_secrets Tests cover multi-tenant scenarios, cache expiry, concurrent execution.
149 lines
3.4 KiB
Go
149 lines
3.4 KiB
Go
//go:build integration
|
|
|
|
package integration
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/crypto"
|
|
"github.com/nextlevelbuilder/goclaw/internal/store/pg"
|
|
"github.com/nextlevelbuilder/goclaw/internal/tools"
|
|
)
|
|
|
|
// TestWebSearch_TenantIsolation verifies that two tenants with different
|
|
// provider keys get independent chains, with no cross-tenant key leakage.
|
|
func TestWebSearch_TenantIsolation(t *testing.T) {
|
|
db := testDB(t)
|
|
tenantA, _ := seedTenantAgent(t, db)
|
|
tenantB, _ := seedTenantAgent(t, db)
|
|
|
|
encKey := testEncryptionKey
|
|
|
|
secretsStore := pg.NewPGConfigSecretsStore(db, encKey)
|
|
|
|
// Seed Brave key for tenant A
|
|
braveKeyA := "test-key-brave-tenant-a-" + uuid.New().String()
|
|
ctxA := tenantCtx(tenantA)
|
|
|
|
enc, err := crypto.Encrypt(braveKeyA, encKey)
|
|
if err != nil {
|
|
t.Fatalf("encrypt tenantA key: %v", err)
|
|
}
|
|
|
|
_, err = db.ExecContext(ctxA,
|
|
`INSERT INTO config_secrets (key, value, updated_at, tenant_id)
|
|
VALUES ($1, $2, NOW(), $3)
|
|
ON CONFLICT (key, tenant_id) DO NOTHING`,
|
|
"tools.web.brave.api_key", []byte(enc), tenantA)
|
|
if err != nil {
|
|
t.Fatalf("insert tenantA brave key: %v", err)
|
|
}
|
|
|
|
t.Cleanup(func() {
|
|
db.ExecContext(ctxA,
|
|
`DELETE FROM config_secrets WHERE key=$1 AND tenant_id=$2`,
|
|
"tools.web.brave.api_key", tenantA)
|
|
})
|
|
|
|
// Seed Exa key for tenant B
|
|
exaKeyB := "test-key-exa-tenant-b-" + uuid.New().String()
|
|
ctxB := tenantCtx(tenantB)
|
|
|
|
encB, err := crypto.Encrypt(exaKeyB, encKey)
|
|
if err != nil {
|
|
t.Fatalf("encrypt tenantB key: %v", err)
|
|
}
|
|
|
|
_, err = db.ExecContext(ctxB,
|
|
`INSERT INTO config_secrets (key, value, updated_at, tenant_id)
|
|
VALUES ($1, $2, NOW(), $3)
|
|
ON CONFLICT (key, tenant_id) DO NOTHING`,
|
|
"tools.web.exa.api_key", []byte(encB), tenantB)
|
|
if err != nil {
|
|
t.Fatalf("insert tenantB exa key: %v", err)
|
|
}
|
|
|
|
t.Cleanup(func() {
|
|
db.ExecContext(ctxB,
|
|
`DELETE FROM config_secrets WHERE key=$1 AND tenant_id=$2`,
|
|
"tools.web.exa.api_key", tenantB)
|
|
})
|
|
|
|
// Resolve chains for each tenant
|
|
chainA := tools.BuildChainFromStorage(ctxA, secretsStore)
|
|
chainB := tools.BuildChainFromStorage(ctxB, secretsStore)
|
|
|
|
// Verify tenant A has Brave
|
|
foundBraveInA := false
|
|
for _, p := range chainA {
|
|
if p.Name() == "brave" {
|
|
foundBraveInA = true
|
|
break
|
|
}
|
|
}
|
|
if !foundBraveInA {
|
|
t.Error("tenant A chain should contain Brave")
|
|
}
|
|
|
|
// Verify tenant B has Exa
|
|
foundExaInB := false
|
|
for _, p := range chainB {
|
|
if p.Name() == "exa" {
|
|
foundExaInB = true
|
|
break
|
|
}
|
|
}
|
|
if !foundExaInB {
|
|
t.Error("tenant B chain should contain Exa")
|
|
}
|
|
|
|
// Verify tenant A does NOT have Exa
|
|
foundExaInA := false
|
|
for _, p := range chainA {
|
|
if p.Name() == "exa" {
|
|
foundExaInA = true
|
|
break
|
|
}
|
|
}
|
|
if foundExaInA {
|
|
t.Error("tenant A chain should NOT contain Exa (cross-tenant leak)")
|
|
}
|
|
|
|
// Verify tenant B does NOT have Brave
|
|
foundBraveInB := false
|
|
for _, p := range chainB {
|
|
if p.Name() == "brave" {
|
|
foundBraveInB = true
|
|
break
|
|
}
|
|
}
|
|
if foundBraveInB {
|
|
t.Error("tenant B chain should NOT contain Brave (cross-tenant leak)")
|
|
}
|
|
|
|
// Both should have DDG as fallback
|
|
foundDDGInA := false
|
|
for _, p := range chainA {
|
|
if p.Name() == "duckduckgo" {
|
|
foundDDGInA = true
|
|
break
|
|
}
|
|
}
|
|
if !foundDDGInA {
|
|
t.Error("tenant A chain should have DDG fallback")
|
|
}
|
|
|
|
foundDDGInB := false
|
|
for _, p := range chainB {
|
|
if p.Name() == "duckduckgo" {
|
|
foundDDGInB = true
|
|
break
|
|
}
|
|
}
|
|
if !foundDDGInB {
|
|
t.Error("tenant B chain should have DDG fallback")
|
|
}
|
|
}
|