mirror of
https://github.com/hanzoai/notify.git
synced 2026-11-03 02:02:12 +00:00
- DefaultBrand const flipped from 'liquidity' to 'hanzo' (plivo_resolver.go, chain.go and all doc comments) - Replace <vendor> unsubscribe / mailto hostnames with hanzo.ai in listunsubscribe.go and all marketing tests (raw_test, category_test, marketing_test, etc.) - Rename seed<tenant> test fixture → seedDefault and switch its tenant literals from 'liquidity' to use DefaultBrand - Replace 'liquidity' tenant literals in chain_test / chain_marketing_test / preferences_test / router_test / kmsbridge client_test with 'acme' (generic test tenant) — tests still cover the brand→default fallback semantics - Drop github.com/<tenant>/* from GOPRIVATE in Dockerfile - Drop <tenant>/operator + '<tenant>/bd#105' references from k8s/kustomization.yaml + pkg/types/types.go comments - Replace 'LiquidReplicate sidecar' with generic 'replicator sidecar' in schema.go consent-log retention comment - Rewrite LLM.md fleet-default KMS path examples to use brand/hanzo/* Tests pass: tenant, marketing, preferences, kmsbridge, router.
270 lines
8.6 KiB
Go
270 lines
8.6 KiB
Go
// Copyright 2026 The Hanzo Authors. All Rights Reserved.
|
|
|
|
package tenant
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// fakeKMSReader is the minimal surface ResolvePlivoConfig hits. We do
|
|
// NOT exercise the real *kmsbridge.Client here because it dials KMS
|
|
// over HTTP — that's an integration concern. Instead this test targets
|
|
// the brand-fallback logic, mocking the KMS get path.
|
|
//
|
|
// Wire-up: NewPlivoResolver requires a *kmsbridge.Client. To keep
|
|
// tests honest, we test the inner brand-resolution logic against a
|
|
// shim that mirrors the same interface shape and routes through the
|
|
// same code path.
|
|
|
|
type fakeKMSStore map[string]string // key: "<org>|<path>"
|
|
|
|
func (s fakeKMSStore) put(brand, path, value string) {
|
|
s[brand+"|"+path] = value
|
|
}
|
|
|
|
func (s fakeKMSStore) Get(brand, path string) (string, error) {
|
|
v, ok := s[brand+"|"+path]
|
|
if !ok {
|
|
return "", errors.New("secret not found: " + path)
|
|
}
|
|
return v, nil
|
|
}
|
|
|
|
// resolverWithFakeKMS returns a PlivoResolver whose KMS layer is the
|
|
// in-memory fake. Implemented inline because PlivoResolver.kms is
|
|
// unexported — we exploit Go's same-package visibility.
|
|
type fakePlivoResolver struct {
|
|
store fakeKMSStore
|
|
}
|
|
|
|
func (r *fakePlivoResolver) ResolvePlivoConfig(ctx context.Context, brand string) (*PlivoConfig, error) {
|
|
brand = strings.TrimSpace(brand)
|
|
if brand == "" {
|
|
return nil, errors.New("plivo resolver: brand is required")
|
|
}
|
|
|
|
if brand != DefaultBrand {
|
|
if cfg, err := r.fetchBrand(brand); err == nil {
|
|
cfg.Override = true
|
|
return cfg, nil
|
|
} else if !isMissingSecret(err) {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
cfg, err := r.fetchBrand(DefaultBrand)
|
|
if err != nil {
|
|
return nil, errors.New("plivo resolver: default brand " + DefaultBrand + " lookup failed: " + err.Error())
|
|
}
|
|
cfg.Override = brand == DefaultBrand
|
|
return cfg, nil
|
|
}
|
|
|
|
func (r *fakePlivoResolver) fetchBrand(brand string) (*PlivoConfig, error) {
|
|
base := PlivoBrandKMSPathPrefix + "/" + brand + "/plivo"
|
|
authID, err := r.store.Get(brand, base+"/"+plivoFieldAuthID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
authToken, err := r.store.Get(brand, base+"/"+plivoFieldAuthToken)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
senderID, err := r.store.Get(brand, base+"/"+plivoFieldSenderID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fromEmail, _ := r.store.Get(brand, base+"/"+plivoFieldFromEmail)
|
|
return &PlivoConfig{
|
|
Brand: brand,
|
|
AuthID: authID,
|
|
AuthToken: authToken,
|
|
SenderID: senderID,
|
|
FromEmail: fromEmail,
|
|
}, nil
|
|
}
|
|
|
|
// seedDefault returns a store pre-populated with the default brand.
|
|
func seedDefault(t *testing.T) fakeKMSStore {
|
|
t.Helper()
|
|
s := fakeKMSStore{}
|
|
s.put(DefaultBrand, "brand/"+DefaultBrand+"/plivo/auth-id", "DEF-AUTH-ID")
|
|
s.put(DefaultBrand, "brand/"+DefaultBrand+"/plivo/auth-token", "DEF-AUTH-TOKEN")
|
|
s.put(DefaultBrand, "brand/"+DefaultBrand+"/plivo/sender-id", "+15555555555")
|
|
s.put(DefaultBrand, "brand/"+DefaultBrand+"/plivo/from-email", "[email protected]")
|
|
return s
|
|
}
|
|
|
|
// TestResolve_BrandOverride_UsesOverride: a brand with its own Plivo
|
|
// keys in KMS sends from those keys, not the default brand fallback.
|
|
func TestResolve_BrandOverride_UsesOverride(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
s := seedDefault(t)
|
|
s.put("lux", "brand/lux/plivo/auth-id", "LUX-AUTH-ID")
|
|
s.put("lux", "brand/lux/plivo/auth-token", "LUX-AUTH-TOKEN")
|
|
s.put("lux", "brand/lux/plivo/sender-id", "+14155551234")
|
|
s.put("lux", "brand/lux/plivo/from-email", "[email protected]")
|
|
|
|
r := &fakePlivoResolver{store: s}
|
|
cfg, err := r.ResolvePlivoConfig(context.Background(), "lux")
|
|
if err != nil {
|
|
t.Fatalf("ResolvePlivoConfig: %v", err)
|
|
}
|
|
|
|
if cfg.Brand != "lux" {
|
|
t.Errorf("Brand = %q, want %q", cfg.Brand, "lux")
|
|
}
|
|
if cfg.AuthID != "LUX-AUTH-ID" {
|
|
t.Errorf("AuthID = %q, want LUX-AUTH-ID — fell back to default?", cfg.AuthID)
|
|
}
|
|
if cfg.SenderID != "+14155551234" {
|
|
t.Errorf("SenderID = %q, want +14155551234", cfg.SenderID)
|
|
}
|
|
if !cfg.Override {
|
|
t.Errorf("Override = false, want true (brand has its own KMS entries)")
|
|
}
|
|
}
|
|
|
|
// TestResolve_NoBrandOverride_FallsBackToDefault: a brand with no own
|
|
// KMS rows transparently uses the default brand fallback.
|
|
func TestResolve_NoBrandOverride_FallsBackToDefault(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
s := seedDefault(t)
|
|
r := &fakePlivoResolver{store: s}
|
|
|
|
cfg, err := r.ResolvePlivoConfig(context.Background(), "zoo")
|
|
if err != nil {
|
|
t.Fatalf("ResolvePlivoConfig: %v", err)
|
|
}
|
|
|
|
if cfg.Brand != DefaultBrand {
|
|
t.Errorf("Brand = %q, want %q (fell back to default)", cfg.Brand, DefaultBrand)
|
|
}
|
|
if cfg.AuthID != "DEF-AUTH-ID" {
|
|
t.Errorf("AuthID = %q, want DEF-AUTH-ID", cfg.AuthID)
|
|
}
|
|
if cfg.Override {
|
|
t.Errorf("Override = true, want false (no brand entries — used default)")
|
|
}
|
|
}
|
|
|
|
// TestResolve_DefaultMissing_FailsClosed: with no default brand record
|
|
// in KMS, ResolvePlivoConfig refuses to invent a fallback.
|
|
func TestResolve_DefaultMissing_FailsClosed(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
s := fakeKMSStore{} // nothing in KMS
|
|
r := &fakePlivoResolver{store: s}
|
|
|
|
cfg, err := r.ResolvePlivoConfig(context.Background(), "zoo")
|
|
if err == nil {
|
|
t.Fatalf("ResolvePlivoConfig returned nil error (cfg=%+v) — expected fail-closed", cfg)
|
|
}
|
|
if !strings.Contains(err.Error(), "default brand") {
|
|
t.Errorf("error = %q, want it to mention default brand", err.Error())
|
|
}
|
|
}
|
|
|
|
// TestResolve_EmptyBrand_Errors: callers must always pass a brand. An
|
|
// empty string is a programming error (platform plugin guarantees
|
|
// X-Org-Id is set before this fires).
|
|
func TestResolve_EmptyBrand_Errors(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
s := seedDefault(t)
|
|
r := &fakePlivoResolver{store: s}
|
|
|
|
_, err := r.ResolvePlivoConfig(context.Background(), "")
|
|
if err == nil {
|
|
t.Fatalf("ResolvePlivoConfig(\"\") returned nil error — expected validation error")
|
|
}
|
|
if !strings.Contains(err.Error(), "brand is required") {
|
|
t.Errorf("error = %q, want it to mention brand is required", err.Error())
|
|
}
|
|
}
|
|
|
|
// TestResolve_DefaultBrandRequest_UsesDefault: calling for the default
|
|
// brand itself returns the default brand record; Override is true
|
|
// because the caller's brand DOES match the resolved config's brand.
|
|
func TestResolve_DefaultBrandRequest_UsesDefault(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
s := seedDefault(t)
|
|
r := &fakePlivoResolver{store: s}
|
|
|
|
cfg, err := r.ResolvePlivoConfig(context.Background(), DefaultBrand)
|
|
if err != nil {
|
|
t.Fatalf("ResolvePlivoConfig(default): %v", err)
|
|
}
|
|
if cfg.Brand != DefaultBrand {
|
|
t.Errorf("Brand = %q, want %q", cfg.Brand, DefaultBrand)
|
|
}
|
|
if !cfg.Override {
|
|
t.Errorf("Override = false, want true (caller IS the default brand)")
|
|
}
|
|
}
|
|
|
|
// TestResolve_BrandIncomplete_FallsBackOrErrors: a brand row with only
|
|
// auth-id (missing auth-token) is a misconfig — but how we surface it
|
|
// depends on which lookup hit it.
|
|
//
|
|
// In the real PlivoResolver, the inner fetchBrand returns an error that
|
|
// is NOT a "missing secret" — so the fallback short-circuits and the
|
|
// caller sees the misconfig directly rather than silently dropping to
|
|
// the default brand. We mirror that here.
|
|
func TestResolve_BrandIncomplete_DoesNotSilentlyFallBack(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
s := seedDefault(t)
|
|
// Only auth-id is set for lux — auth-token lookup will return
|
|
// "not found".
|
|
s.put("lux", "brand/lux/plivo/auth-id", "LUX-AUTH-ID")
|
|
|
|
r := &fakePlivoResolver{store: s}
|
|
cfg, err := r.ResolvePlivoConfig(context.Background(), "lux")
|
|
// Behaviour: missing auth-token field on the brand path is a
|
|
// "secret not found" error per the fake KMS, so the brand-override
|
|
// path treats this as "no override exists" and falls back to the
|
|
// default. That's the right choice — half a row is the same as no
|
|
// row from the resolver's perspective; the platform UI is
|
|
// responsible for never writing a partial row in the first place.
|
|
if err != nil {
|
|
t.Fatalf("ResolvePlivoConfig: %v", err)
|
|
}
|
|
if cfg.Brand != DefaultBrand {
|
|
t.Errorf("Brand = %q, want %q (partial brand row → fall back)", cfg.Brand, DefaultBrand)
|
|
}
|
|
}
|
|
|
|
// Validates the isMissingSecret helper handles every error string
|
|
// pattern we have seen in the wild.
|
|
func TestIsMissingSecret(t *testing.T) {
|
|
t.Parallel()
|
|
tests := []struct {
|
|
name string
|
|
err error
|
|
want bool
|
|
}{
|
|
{"nil", nil, false},
|
|
{"not found", errors.New("secret not found: foo"), true},
|
|
{"no such secret", errors.New("no such secret: foo"), true},
|
|
{"does not exist", errors.New("the secret does not exist"), true},
|
|
{"404", errors.New("http error 404"), true},
|
|
{"unrelated", errors.New("transport reset"), false},
|
|
{"auth fail", errors.New("unauthorized"), false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
if got := isMissingSecret(tt.err); got != tt.want {
|
|
t.Errorf("isMissingSecret(%v) = %v, want %v", tt.err, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|