Files
goclaw/tests/integration/web_search_tenant_isolation_test.go
viettranx 18e48cd0fc test(integration): add web_search tenant isolation and migration hook tests
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.
2026-04-18 17:47:36 +07:00

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")
}
}