mirror of
https://github.com/luxfi/treasury.git
synced 2026-10-30 15:15:00 +00:00
Replace the ErrNotImplemented stub on Config.LoadFromKMS with a real implementation against a narrow KMSGetter interface. Adapter has zero direct dependency on luxfi/kms; the canonical luxfi/kms.GetWith satisfies KMSGetter via a 1-line wrapper at treasuryd startup. Signature: (ctx, KMSGetter, env) error. env is "sandbox" or "prod". KMS path scheme (per integration brief §2): shared/northcapital/<env>/clientID (required) shared/northcapital/<env>/developerAPIKey (required) The treasury-side adapter holds only the two payment-rail credentials (no webhook keys — webhook surface lives on the broker side). Error sentinels: ErrKMSKeyMissing — required key absent / empty value ErrKMSDecryption — transport / decryption failure The previous test pinning ErrNotImplemented is rewritten to assert LoadFromKMS is wired (not returning the sentinel). ErrNotImplemented is retained for any future Provider method that ships its scaffold ahead of its body. Tests: 11 cases, all pass under -race. Conformance: - sandbox + prod happy paths populate clientID + developerAPIKey - exact KMS path shape (shared/northcapital/<env>/<field>) - missing required key → ErrKMSKeyMissing (each of 2 required) - empty value treated as missing - decryption error → ErrKMSDecryption - transport error → ErrKMSDecryption - nil KMSGetter rejected - empty / unknown env rejected - kmsPath shape pinned Source-of-design: Public-Spec Source-ref: legal/partnerships/northcapital/ NorthCapital_Lux_Integration_Brief.md §2; github.com/luxfi/kms public Go surface (kms.Get / kms.GetWith) Clean-Team-Member: dev-agent (P0 NCPS LoadFromKMS completion) Signed-off-by: dev <[email protected]>
220 lines
5.6 KiB
Go
220 lines
5.6 KiB
Go
// Conformance tests for LoadFromKMS on the treasury-side adapter.
|
|
// Mirror of the broker-side suite; documents that the treasury
|
|
// surface fetches only the two required credentials (clientID +
|
|
// developerAPIKey), with no webhook keys.
|
|
//
|
|
// Source-of-design: Public-Spec
|
|
// Source-ref: legal/partnerships/northcapital/
|
|
// NorthCapital_Lux_Integration_Brief.md §2.
|
|
|
|
package northcapital
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"sync"
|
|
"testing"
|
|
)
|
|
|
|
// mockKMS is the in-memory KMSGetter used by every test in this file.
|
|
type mockKMS struct {
|
|
mu sync.Mutex
|
|
values map[string]string
|
|
errs map[string]error
|
|
called []string
|
|
notFound error
|
|
}
|
|
|
|
func newMockKMS() *mockKMS {
|
|
return &mockKMS{
|
|
values: make(map[string]string),
|
|
errs: make(map[string]error),
|
|
notFound: errors.New("zapclient: secret not found"),
|
|
}
|
|
}
|
|
|
|
func (m *mockKMS) Get(_ context.Context, name string) (string, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.called = append(m.called, name)
|
|
if err, ok := m.errs[name]; ok {
|
|
return "", err
|
|
}
|
|
if v, ok := m.values[name]; ok {
|
|
return v, nil
|
|
}
|
|
return "", m.notFound
|
|
}
|
|
|
|
func (m *mockKMS) calls() []string {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
out := make([]string, len(m.called))
|
|
copy(out, m.called)
|
|
return out
|
|
}
|
|
|
|
// --- happy paths ---
|
|
|
|
func TestLoadFromKMS_SandboxSuccess(t *testing.T) {
|
|
kms := newMockKMS()
|
|
kms.values["shared/northcapital/sandbox/clientID"] = "cid-sandbox"
|
|
kms.values["shared/northcapital/sandbox/developerAPIKey"] = "dak-sandbox"
|
|
|
|
var cfg Config
|
|
if err := cfg.LoadFromKMS(context.Background(), kms, "sandbox"); err != nil {
|
|
t.Fatalf("LoadFromKMS: %v", err)
|
|
}
|
|
if cfg.ClientID != "cid-sandbox" {
|
|
t.Fatalf("ClientID: got %q", cfg.ClientID)
|
|
}
|
|
if cfg.DeveloperAPIKey != "dak-sandbox" {
|
|
t.Fatalf("DeveloperAPIKey: got %q", cfg.DeveloperAPIKey)
|
|
}
|
|
|
|
// Path shape: treasury fetches ONLY clientID + developerAPIKey.
|
|
got := kms.calls()
|
|
sort.Strings(got)
|
|
want := []string{
|
|
"shared/northcapital/sandbox/clientID",
|
|
"shared/northcapital/sandbox/developerAPIKey",
|
|
}
|
|
sort.Strings(want)
|
|
if !equalStrSlice(got, want) {
|
|
t.Fatalf("KMS path shape: got %v, want %v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestLoadFromKMS_ProdSuccess(t *testing.T) {
|
|
kms := newMockKMS()
|
|
kms.values["shared/northcapital/prod/clientID"] = "cid-prod"
|
|
kms.values["shared/northcapital/prod/developerAPIKey"] = "dak-prod"
|
|
|
|
var cfg Config
|
|
if err := cfg.LoadFromKMS(context.Background(), kms, "prod"); err != nil {
|
|
t.Fatalf("LoadFromKMS: %v", err)
|
|
}
|
|
if cfg.ClientID != "cid-prod" || cfg.DeveloperAPIKey != "dak-prod" {
|
|
t.Fatalf("required fields: %+v", cfg)
|
|
}
|
|
}
|
|
|
|
// --- missing required keys ---
|
|
|
|
func TestLoadFromKMS_MissingClientID(t *testing.T) {
|
|
kms := newMockKMS()
|
|
kms.values["shared/northcapital/sandbox/developerAPIKey"] = "dak"
|
|
|
|
var cfg Config
|
|
err := cfg.LoadFromKMS(context.Background(), kms, "sandbox")
|
|
if !errors.Is(err, ErrKMSKeyMissing) {
|
|
t.Fatalf("got %v, want errors.Is ErrKMSKeyMissing", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadFromKMS_MissingDeveloperAPIKey(t *testing.T) {
|
|
kms := newMockKMS()
|
|
kms.values["shared/northcapital/sandbox/clientID"] = "cid"
|
|
|
|
var cfg Config
|
|
err := cfg.LoadFromKMS(context.Background(), kms, "sandbox")
|
|
if !errors.Is(err, ErrKMSKeyMissing) {
|
|
t.Fatalf("got %v, want errors.Is ErrKMSKeyMissing", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadFromKMS_EmptyValueTreatedAsMissing(t *testing.T) {
|
|
kms := newMockKMS()
|
|
kms.values["shared/northcapital/sandbox/clientID"] = ""
|
|
kms.values["shared/northcapital/sandbox/developerAPIKey"] = "dak"
|
|
|
|
var cfg Config
|
|
err := cfg.LoadFromKMS(context.Background(), kms, "sandbox")
|
|
if !errors.Is(err, ErrKMSKeyMissing) {
|
|
t.Fatalf("empty clientID: got %v, want ErrKMSKeyMissing", err)
|
|
}
|
|
}
|
|
|
|
// --- transport / decryption errors ---
|
|
|
|
func TestLoadFromKMS_DecryptionError(t *testing.T) {
|
|
kms := newMockKMS()
|
|
kms.errs["shared/northcapital/sandbox/clientID"] = fmt.Errorf("aead: decrypt: bad tag")
|
|
|
|
var cfg Config
|
|
err := cfg.LoadFromKMS(context.Background(), kms, "sandbox")
|
|
if !errors.Is(err, ErrKMSDecryption) {
|
|
t.Fatalf("got %v, want errors.Is ErrKMSDecryption", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadFromKMS_TransportError(t *testing.T) {
|
|
kms := newMockKMS()
|
|
kms.values["shared/northcapital/sandbox/clientID"] = "cid"
|
|
kms.errs["shared/northcapital/sandbox/developerAPIKey"] = fmt.Errorf("dial tcp: i/o timeout")
|
|
|
|
var cfg Config
|
|
err := cfg.LoadFromKMS(context.Background(), kms, "sandbox")
|
|
if !errors.Is(err, ErrKMSDecryption) {
|
|
t.Fatalf("got %v, want errors.Is ErrKMSDecryption", err)
|
|
}
|
|
}
|
|
|
|
// --- argument validation ---
|
|
|
|
func TestLoadFromKMS_NilGetter(t *testing.T) {
|
|
var cfg Config
|
|
err := cfg.LoadFromKMS(context.Background(), nil, "sandbox")
|
|
if err == nil {
|
|
t.Fatal("nil KMSGetter: expected error, got nil")
|
|
}
|
|
}
|
|
|
|
func TestLoadFromKMS_EmptyEnv(t *testing.T) {
|
|
kms := newMockKMS()
|
|
var cfg Config
|
|
err := cfg.LoadFromKMS(context.Background(), kms, "")
|
|
if err == nil {
|
|
t.Fatal("empty env: expected error, got nil")
|
|
}
|
|
}
|
|
|
|
func TestLoadFromKMS_UnknownEnv(t *testing.T) {
|
|
kms := newMockKMS()
|
|
var cfg Config
|
|
err := cfg.LoadFromKMS(context.Background(), kms, "staging")
|
|
if err == nil {
|
|
t.Fatal("unknown env: expected error, got nil")
|
|
}
|
|
}
|
|
|
|
// --- helpers ---
|
|
|
|
func equalStrSlice(a, b []string) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i := range a {
|
|
if a[i] != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func TestKMSPath(t *testing.T) {
|
|
cases := []struct {
|
|
env, field, want string
|
|
}{
|
|
{"sandbox", "clientID", "shared/northcapital/sandbox/clientID"},
|
|
{"prod", "developerAPIKey", "shared/northcapital/prod/developerAPIKey"},
|
|
}
|
|
for _, tc := range cases {
|
|
if got := kmsPath(tc.env, tc.field); got != tc.want {
|
|
t.Fatalf("kmsPath(%q,%q)=%q want %q", tc.env, tc.field, got, tc.want)
|
|
}
|
|
}
|
|
}
|