Files
aml/pkg/sanctions/persist_test.go
T
zeekayandHanzo Dev 07e69e3ee2 luxfi/aml — real-time AML/CFT transaction monitoring
A Go AML engine: a configurable rules DSL over expr-lang with weight-of-evidence
scoring, sanctions screening against OFAC/UN/EU/HMT, case management, and scored
alerting. Single binary, embedded SQLite per org, embedded UI.

Surface (all /v1, org-scoped):
  /v1/aml/transactions        ingest and score
  /v1/aml/rules  /rules/test  the rules DSL and its dry-run
  /v1/aml/sanctions/search    list screening
  /v1/aml/cases               investigation workflow
  /v1/aml/health

Build: make build produces one static binary with the UI embedded (embedui tag);
bare Go tooling needs no Node. Third-party license and notice texts are generated
from the module graph the linker actually walks and ship beside every artifact.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-28 22:51:25 -07:00

155 lines
4.0 KiB
Go

// Copyright 2024-2026 Lux Partners Limited. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package sanctions
import (
"context"
"sync"
"testing"
"time"
"github.com/luxfi/aml/pkg/types"
)
// MemorySanctionsStore is an in-memory SanctionsStore for testing.
type MemorySanctionsStore struct {
mu sync.RWMutex
entries []types.SanctionsEntry
lastRefresh time.Time
}
func NewMemorySanctionsStore() *MemorySanctionsStore {
return &MemorySanctionsStore{}
}
func (m *MemorySanctionsStore) Save(_ context.Context, entries []types.SanctionsEntry) error {
m.mu.Lock()
defer m.mu.Unlock()
m.entries = append(m.entries, entries...)
m.lastRefresh = time.Now().UTC()
return nil
}
func (m *MemorySanctionsStore) Search(_ context.Context, name, dob string) ([]SearchResult, error) {
m.mu.RLock()
defer m.mu.RUnlock()
var results []SearchResult
for _, e := range m.entries {
score := TokenMatch(name, e.Name)
if score >= MatchThreshold {
results = append(results, SearchResult{Entry: e, Score: score})
continue
}
for _, alias := range e.Aliases {
aliasScore := TokenMatch(name, alias)
if aliasScore >= MatchThreshold {
results = append(results, SearchResult{Entry: e, Score: aliasScore})
break
}
}
}
return results, nil
}
func (m *MemorySanctionsStore) Count(_ context.Context) (int, error) {
m.mu.RLock()
defer m.mu.RUnlock()
return len(m.entries), nil
}
func (m *MemorySanctionsStore) LastRefresh(_ context.Context) (time.Time, error) {
m.mu.RLock()
defer m.mu.RUnlock()
return m.lastRefresh, nil
}
func TestMemorySanctionsStore_Save(t *testing.T) {
store := NewMemorySanctionsStore()
entries := []types.SanctionsEntry{
{ID: "e-1", Name: "John Doe", Type: types.SanctionIndividual},
{ID: "e-2", Name: "Acme Corp", Type: types.SanctionEntity},
}
err := store.Save(context.Background(), entries)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
count, err := store.Count(context.Background())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if count != 2 {
t.Fatalf("expected 2, got %d", count)
}
}
func TestMemorySanctionsStore_Search(t *testing.T) {
store := NewMemorySanctionsStore()
store.Save(context.Background(), []types.SanctionsEntry{
{ID: "e-1", Name: "John Smith", Aliases: []string{"J. Smith", "Johnny Smith"}},
{ID: "e-2", Name: "Jane Doe"},
{ID: "e-3", Name: "Unrelated Person"},
})
// Exact-ish match
results, err := store.Search(context.Background(), "John Smith", "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(results) == 0 {
t.Fatal("expected at least 1 result for 'John Smith'")
}
if results[0].Entry.Name != "John Smith" {
t.Fatalf("expected John Smith, got %q", results[0].Entry.Name)
}
// No match
results, err = store.Search(context.Background(), "Totally Different Name", "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(results) != 0 {
t.Fatalf("expected 0 results, got %d", len(results))
}
}
func TestMemorySanctionsStore_SearchAlias(t *testing.T) {
store := NewMemorySanctionsStore()
store.Save(context.Background(), []types.SanctionsEntry{
{ID: "e-alias", Name: "Muhammad al-Bashir", Aliases: []string{"Omar al-Bashir", "Bashir"}},
})
results, err := store.Search(context.Background(), "Omar al-Bashir", "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(results) == 0 {
t.Fatal("expected alias match")
}
}
func TestMemorySanctionsStore_LastRefresh(t *testing.T) {
store := NewMemorySanctionsStore()
before, _ := store.LastRefresh(context.Background())
if !before.IsZero() {
t.Fatal("expected zero time before any save")
}
store.Save(context.Background(), []types.SanctionsEntry{
{ID: "e-1", Name: "Test"},
})
after, _ := store.LastRefresh(context.Background())
if after.IsZero() {
t.Fatal("expected non-zero time after save")
}
}
func TestSanctionsStoreInterface(t *testing.T) {
var _ SanctionsStore = (*MemorySanctionsStore)(nil)
}