Services: Admin-Compliance, Backend-Compliance, AI-Compliance-SDK, Consent-SDK, Developer-Portal, PCA-Platform, DSMS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
619 lines
15 KiB
Go
619 lines
15 KiB
Go
package ucca
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
// ============================================================================
|
|
// Financial Policy Engine Tests
|
|
// ============================================================================
|
|
|
|
func TestFinancialPolicyEngine_NewEngine(t *testing.T) {
|
|
// Try to load the financial policy engine
|
|
engine, err := NewFinancialPolicyEngineFromPath("../../policies/financial_regulations_policy.yaml")
|
|
if err != nil {
|
|
t.Skipf("Skipping test - policy file not found: %v", err)
|
|
}
|
|
|
|
if engine == nil {
|
|
t.Fatal("Engine should not be nil")
|
|
}
|
|
|
|
version := engine.GetPolicyVersion()
|
|
if version == "" {
|
|
t.Error("Policy version should not be empty")
|
|
}
|
|
}
|
|
|
|
func TestFinancialPolicyEngine_IsApplicable(t *testing.T) {
|
|
engine := createTestFinancialEngine(t)
|
|
if engine == nil {
|
|
return
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
domain Domain
|
|
expected bool
|
|
}{
|
|
{"Banking domain is applicable", DomainBanking, true},
|
|
{"Finance domain is applicable", DomainFinance, true},
|
|
{"Insurance domain is applicable", DomainInsurance, true},
|
|
{"Investment domain is applicable", DomainInvestment, true},
|
|
{"Healthcare is not applicable", DomainHealthcare, false},
|
|
{"Retail is not applicable", DomainRetail, false},
|
|
{"Education is not applicable", DomainEducation, false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
intake := &UseCaseIntake{
|
|
Domain: tt.domain,
|
|
}
|
|
result := engine.IsApplicable(intake)
|
|
if result != tt.expected {
|
|
t.Errorf("IsApplicable() = %v, want %v", result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFinancialPolicyEngine_Evaluate_NonApplicableDomain(t *testing.T) {
|
|
engine := createTestFinancialEngine(t)
|
|
if engine == nil {
|
|
return
|
|
}
|
|
|
|
intake := &UseCaseIntake{
|
|
Domain: DomainHealthcare,
|
|
FinancialContext: &FinancialContext{
|
|
FinancialEntity: FinancialEntity{
|
|
Type: FinancialEntityCreditInstitution,
|
|
Regulated: true,
|
|
},
|
|
},
|
|
}
|
|
|
|
result := engine.Evaluate(intake)
|
|
|
|
if result.IsApplicable {
|
|
t.Error("Should not be applicable for healthcare domain")
|
|
}
|
|
if len(result.TriggeredRules) > 0 {
|
|
t.Error("No rules should trigger for non-applicable domain")
|
|
}
|
|
}
|
|
|
|
func TestFinancialPolicyEngine_Evaluate_MissingContext(t *testing.T) {
|
|
engine := createTestFinancialEngine(t)
|
|
if engine == nil {
|
|
return
|
|
}
|
|
|
|
intake := &UseCaseIntake{
|
|
Domain: DomainBanking,
|
|
FinancialContext: nil, // No financial context provided
|
|
}
|
|
|
|
result := engine.Evaluate(intake)
|
|
|
|
if !result.IsApplicable {
|
|
t.Error("Should be applicable for banking domain")
|
|
}
|
|
if !result.MissingContext {
|
|
t.Error("Should indicate missing financial context")
|
|
}
|
|
}
|
|
|
|
func TestFinancialPolicyEngine_Evaluate_RegulatedBank(t *testing.T) {
|
|
engine := createTestFinancialEngine(t)
|
|
if engine == nil {
|
|
return
|
|
}
|
|
|
|
intake := &UseCaseIntake{
|
|
Domain: DomainBanking,
|
|
FinancialContext: &FinancialContext{
|
|
FinancialEntity: FinancialEntity{
|
|
Type: FinancialEntityCreditInstitution,
|
|
Regulated: true,
|
|
SizeCategory: SizeCategoryLessSignificant,
|
|
},
|
|
ICTService: ICTService{
|
|
IsCritical: false,
|
|
IsOutsourced: false,
|
|
ProviderLocation: ProviderLocationEU,
|
|
},
|
|
AIApplication: FinancialAIApplication{
|
|
AffectsCustomerDecisions: false,
|
|
RiskAssessment: false,
|
|
},
|
|
},
|
|
}
|
|
|
|
result := engine.Evaluate(intake)
|
|
|
|
if !result.IsApplicable {
|
|
t.Error("Should be applicable for banking domain")
|
|
}
|
|
if result.MissingContext {
|
|
t.Error("Context should not be missing")
|
|
}
|
|
|
|
// Should trigger basic DORA/BAIT rules for regulated banks
|
|
if len(result.TriggeredRules) == 0 {
|
|
t.Error("Should trigger at least basic regulatory rules")
|
|
}
|
|
|
|
// Check for DORA control requirements
|
|
hasDORAControl := false
|
|
for _, ctrl := range result.RequiredControls {
|
|
if ctrl.Category == "DORA" || ctrl.Category == "BAIT" {
|
|
hasDORAControl = true
|
|
break
|
|
}
|
|
}
|
|
if !hasDORAControl {
|
|
t.Error("Should require DORA or BAIT controls for regulated bank")
|
|
}
|
|
}
|
|
|
|
func TestFinancialPolicyEngine_Evaluate_CriticalICTOutsourcing(t *testing.T) {
|
|
engine := createTestFinancialEngine(t)
|
|
if engine == nil {
|
|
return
|
|
}
|
|
|
|
intake := &UseCaseIntake{
|
|
Domain: DomainBanking,
|
|
FinancialContext: &FinancialContext{
|
|
FinancialEntity: FinancialEntity{
|
|
Type: FinancialEntityCreditInstitution,
|
|
Regulated: true,
|
|
},
|
|
ICTService: ICTService{
|
|
IsCritical: true,
|
|
IsOutsourced: true,
|
|
ProviderLocation: ProviderLocationEU,
|
|
},
|
|
},
|
|
}
|
|
|
|
result := engine.Evaluate(intake)
|
|
|
|
// Should have elevated risk for critical ICT outsourcing
|
|
if result.RiskScore == 0 {
|
|
t.Error("Risk score should be elevated for critical ICT outsourcing")
|
|
}
|
|
|
|
// Should require TPP risk management control
|
|
hasTPPControl := false
|
|
for _, ctrl := range result.RequiredControls {
|
|
if ctrl.ID == "CTRL-DORA-TPP-RISK-MANAGEMENT" || ctrl.ID == "CTRL-MARISK-OUTSOURCING" {
|
|
hasTPPControl = true
|
|
break
|
|
}
|
|
}
|
|
if !hasTPPControl {
|
|
t.Error("Should require TPP risk management for critical outsourcing")
|
|
}
|
|
|
|
// Should trigger escalation
|
|
if result.EscalationLevel == "" {
|
|
t.Error("Should trigger escalation for critical ICT outsourcing")
|
|
}
|
|
}
|
|
|
|
func TestFinancialPolicyEngine_Evaluate_UnvalidatedRiskModel(t *testing.T) {
|
|
engine := createTestFinancialEngine(t)
|
|
if engine == nil {
|
|
return
|
|
}
|
|
|
|
intake := &UseCaseIntake{
|
|
Domain: DomainBanking,
|
|
FinancialContext: &FinancialContext{
|
|
FinancialEntity: FinancialEntity{
|
|
Type: FinancialEntityCreditInstitution,
|
|
Regulated: true,
|
|
},
|
|
ICTService: ICTService{
|
|
IsCritical: false,
|
|
IsOutsourced: false,
|
|
},
|
|
AIApplication: FinancialAIApplication{
|
|
RiskAssessment: true,
|
|
ModelValidationDone: false, // Not validated!
|
|
},
|
|
},
|
|
}
|
|
|
|
result := engine.Evaluate(intake)
|
|
|
|
// Should block unvalidated risk models
|
|
if result.Feasibility != FeasibilityNO {
|
|
t.Error("Should block use case with unvalidated risk model")
|
|
}
|
|
|
|
// Should require model validation control
|
|
hasValidationControl := false
|
|
for _, ctrl := range result.RequiredControls {
|
|
if ctrl.ID == "CTRL-MARISK-MODEL-VALIDATION" {
|
|
hasValidationControl = true
|
|
break
|
|
}
|
|
}
|
|
if !hasValidationControl {
|
|
t.Error("Should require MaRisk model validation control")
|
|
}
|
|
}
|
|
|
|
func TestFinancialPolicyEngine_Evaluate_ValidatedRiskModel(t *testing.T) {
|
|
engine := createTestFinancialEngine(t)
|
|
if engine == nil {
|
|
return
|
|
}
|
|
|
|
intake := &UseCaseIntake{
|
|
Domain: DomainBanking,
|
|
FinancialContext: &FinancialContext{
|
|
FinancialEntity: FinancialEntity{
|
|
Type: FinancialEntityCreditInstitution,
|
|
Regulated: true,
|
|
},
|
|
ICTService: ICTService{
|
|
IsCritical: false,
|
|
IsOutsourced: false,
|
|
},
|
|
AIApplication: FinancialAIApplication{
|
|
RiskAssessment: true,
|
|
ModelValidationDone: true, // Validated!
|
|
},
|
|
},
|
|
}
|
|
|
|
result := engine.Evaluate(intake)
|
|
|
|
// Should not block validated risk models
|
|
if result.Feasibility == FeasibilityNO {
|
|
t.Error("Should not block use case with validated risk model")
|
|
}
|
|
}
|
|
|
|
func TestFinancialPolicyEngine_Evaluate_AlgorithmicTrading(t *testing.T) {
|
|
engine := createTestFinancialEngine(t)
|
|
if engine == nil {
|
|
return
|
|
}
|
|
|
|
intake := &UseCaseIntake{
|
|
Domain: DomainInvestment,
|
|
FinancialContext: &FinancialContext{
|
|
FinancialEntity: FinancialEntity{
|
|
Type: FinancialEntityInvestmentFirm,
|
|
Regulated: true,
|
|
},
|
|
AIApplication: FinancialAIApplication{
|
|
AlgorithmicTrading: true,
|
|
},
|
|
},
|
|
}
|
|
|
|
result := engine.Evaluate(intake)
|
|
|
|
// Should require algorithmic trading control
|
|
hasAlgoControl := false
|
|
for _, ctrl := range result.RequiredControls {
|
|
if ctrl.ID == "CTRL-FIN-ALGO-TRADING" {
|
|
hasAlgoControl = true
|
|
break
|
|
}
|
|
}
|
|
if !hasAlgoControl {
|
|
t.Error("Should require algorithmic trading control")
|
|
}
|
|
|
|
// Should trigger highest escalation
|
|
if result.EscalationLevel != "E3" {
|
|
t.Errorf("Should trigger E3 escalation for algo trading, got %s", result.EscalationLevel)
|
|
}
|
|
}
|
|
|
|
func TestFinancialPolicyEngine_Evaluate_CustomerDecisions(t *testing.T) {
|
|
engine := createTestFinancialEngine(t)
|
|
if engine == nil {
|
|
return
|
|
}
|
|
|
|
intake := &UseCaseIntake{
|
|
Domain: DomainBanking,
|
|
FinancialContext: &FinancialContext{
|
|
FinancialEntity: FinancialEntity{
|
|
Type: FinancialEntityCreditInstitution,
|
|
Regulated: true,
|
|
},
|
|
AIApplication: FinancialAIApplication{
|
|
AffectsCustomerDecisions: true,
|
|
},
|
|
},
|
|
}
|
|
|
|
result := engine.Evaluate(intake)
|
|
|
|
// Should require explainability control
|
|
hasExplainControl := false
|
|
for _, ctrl := range result.RequiredControls {
|
|
if ctrl.ID == "CTRL-FIN-AI-EXPLAINABILITY" {
|
|
hasExplainControl = true
|
|
break
|
|
}
|
|
}
|
|
if !hasExplainControl {
|
|
t.Error("Should require AI explainability control for customer decisions")
|
|
}
|
|
|
|
// Should trigger E2 escalation
|
|
if result.EscalationLevel != "E2" {
|
|
t.Errorf("Should trigger E2 escalation for customer decisions, got %s", result.EscalationLevel)
|
|
}
|
|
}
|
|
|
|
func TestFinancialPolicyEngine_Evaluate_AMLKYC(t *testing.T) {
|
|
engine := createTestFinancialEngine(t)
|
|
if engine == nil {
|
|
return
|
|
}
|
|
|
|
intake := &UseCaseIntake{
|
|
Domain: DomainBanking,
|
|
FinancialContext: &FinancialContext{
|
|
FinancialEntity: FinancialEntity{
|
|
Type: FinancialEntityCreditInstitution,
|
|
Regulated: true,
|
|
},
|
|
AIApplication: FinancialAIApplication{
|
|
AMLKYC: true,
|
|
},
|
|
},
|
|
}
|
|
|
|
result := engine.Evaluate(intake)
|
|
|
|
// Should require AML control
|
|
hasAMLControl := false
|
|
for _, ctrl := range result.RequiredControls {
|
|
if ctrl.ID == "CTRL-FIN-AML-AI" {
|
|
hasAMLControl = true
|
|
break
|
|
}
|
|
}
|
|
if !hasAMLControl {
|
|
t.Error("Should require AML AI control for KYC/AML use cases")
|
|
}
|
|
}
|
|
|
|
func TestFinancialPolicyEngine_Evaluate_ThirdCountryCritical(t *testing.T) {
|
|
engine := createTestFinancialEngine(t)
|
|
if engine == nil {
|
|
return
|
|
}
|
|
|
|
intake := &UseCaseIntake{
|
|
Domain: DomainBanking,
|
|
FinancialContext: &FinancialContext{
|
|
FinancialEntity: FinancialEntity{
|
|
Type: FinancialEntityCreditInstitution,
|
|
Regulated: true,
|
|
},
|
|
ICTService: ICTService{
|
|
IsCritical: true,
|
|
IsOutsourced: true,
|
|
ProviderLocation: ProviderLocationThirdCountry,
|
|
},
|
|
},
|
|
}
|
|
|
|
result := engine.Evaluate(intake)
|
|
|
|
// Should be conditional at minimum
|
|
if result.Feasibility == FeasibilityYES {
|
|
t.Error("Should not allow critical ICT in third country without conditions")
|
|
}
|
|
|
|
// Should have elevated risk
|
|
if result.RiskScore < 30 {
|
|
t.Error("Should have elevated risk score for third country critical ICT")
|
|
}
|
|
}
|
|
|
|
func TestFinancialPolicyEngine_Evaluate_ConcentrationRisk(t *testing.T) {
|
|
engine := createTestFinancialEngine(t)
|
|
if engine == nil {
|
|
return
|
|
}
|
|
|
|
intake := &UseCaseIntake{
|
|
Domain: DomainBanking,
|
|
FinancialContext: &FinancialContext{
|
|
FinancialEntity: FinancialEntity{
|
|
Type: FinancialEntityCreditInstitution,
|
|
Regulated: true,
|
|
},
|
|
ICTService: ICTService{
|
|
IsOutsourced: true,
|
|
ConcentrationRisk: true,
|
|
},
|
|
},
|
|
}
|
|
|
|
result := engine.Evaluate(intake)
|
|
|
|
// Should trigger escalation for concentration risk
|
|
if result.EscalationLevel == "" {
|
|
t.Error("Should trigger escalation for concentration risk")
|
|
}
|
|
|
|
// Should add risk
|
|
if result.RiskScore == 0 {
|
|
t.Error("Should add risk for concentration risk")
|
|
}
|
|
}
|
|
|
|
func TestFinancialPolicyEngine_Evaluate_InsuranceCompany(t *testing.T) {
|
|
engine := createTestFinancialEngine(t)
|
|
if engine == nil {
|
|
return
|
|
}
|
|
|
|
intake := &UseCaseIntake{
|
|
Domain: DomainInsurance,
|
|
FinancialContext: &FinancialContext{
|
|
FinancialEntity: FinancialEntity{
|
|
Type: FinancialEntityInsuranceCompany,
|
|
Regulated: true,
|
|
},
|
|
},
|
|
}
|
|
|
|
result := engine.Evaluate(intake)
|
|
|
|
if !result.IsApplicable {
|
|
t.Error("Should be applicable for insurance domain")
|
|
}
|
|
}
|
|
|
|
func TestFinancialPolicyEngine_GetAllControls(t *testing.T) {
|
|
engine := createTestFinancialEngine(t)
|
|
if engine == nil {
|
|
return
|
|
}
|
|
|
|
controls := engine.GetAllControls()
|
|
if len(controls) == 0 {
|
|
t.Error("Should have controls defined")
|
|
}
|
|
|
|
// Check for key DORA controls
|
|
keyControls := []string{
|
|
"CTRL-DORA-ICT-RISK-FRAMEWORK",
|
|
"CTRL-DORA-ICT-INCIDENT-MANAGEMENT",
|
|
"CTRL-DORA-TPP-RISK-MANAGEMENT",
|
|
"CTRL-MARISK-MODEL-VALIDATION",
|
|
"CTRL-BAIT-SDLC",
|
|
}
|
|
|
|
for _, key := range keyControls {
|
|
if _, ok := controls[key]; !ok {
|
|
t.Errorf("Should have control %s", key)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFinancialPolicyEngine_GetAllGaps(t *testing.T) {
|
|
engine := createTestFinancialEngine(t)
|
|
if engine == nil {
|
|
return
|
|
}
|
|
|
|
gaps := engine.GetAllGaps()
|
|
if len(gaps) == 0 {
|
|
t.Error("Should have gaps defined")
|
|
}
|
|
|
|
// Check for key gaps
|
|
keyGaps := []string{
|
|
"GAP_DORA_NOT_IMPLEMENTED",
|
|
"GAP_MARISK_MODEL_NOT_VALIDATED",
|
|
}
|
|
|
|
for _, key := range keyGaps {
|
|
if _, ok := gaps[key]; !ok {
|
|
t.Errorf("Should have gap %s", key)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFinancialPolicyEngine_GetAllStopLines(t *testing.T) {
|
|
engine := createTestFinancialEngine(t)
|
|
if engine == nil {
|
|
return
|
|
}
|
|
|
|
stopLines := engine.GetAllStopLines()
|
|
if len(stopLines) == 0 {
|
|
t.Error("Should have stop lines defined")
|
|
}
|
|
|
|
// Check for key stop lines
|
|
keyStopLines := []string{
|
|
"STOP_MARISK_UNVALIDATED_RISK_MODEL",
|
|
"STOP_ALGO_TRADING_WITHOUT_APPROVAL",
|
|
}
|
|
|
|
for _, key := range keyStopLines {
|
|
if _, ok := stopLines[key]; !ok {
|
|
t.Errorf("Should have stop line %s", key)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFinancialPolicyEngine_Determinism(t *testing.T) {
|
|
engine := createTestFinancialEngine(t)
|
|
if engine == nil {
|
|
return
|
|
}
|
|
|
|
intake := &UseCaseIntake{
|
|
Domain: DomainBanking,
|
|
FinancialContext: &FinancialContext{
|
|
FinancialEntity: FinancialEntity{
|
|
Type: FinancialEntityCreditInstitution,
|
|
Regulated: true,
|
|
},
|
|
ICTService: ICTService{
|
|
IsCritical: true,
|
|
IsOutsourced: true,
|
|
},
|
|
AIApplication: FinancialAIApplication{
|
|
AffectsCustomerDecisions: true,
|
|
RiskAssessment: true,
|
|
ModelValidationDone: true,
|
|
},
|
|
},
|
|
}
|
|
|
|
// Run evaluation multiple times
|
|
var lastResult *FinancialAssessmentResult
|
|
for i := 0; i < 10; i++ {
|
|
result := engine.Evaluate(intake)
|
|
if lastResult != nil {
|
|
if result.Feasibility != lastResult.Feasibility {
|
|
t.Error("Feasibility should be deterministic")
|
|
}
|
|
if result.RiskScore != lastResult.RiskScore {
|
|
t.Error("Risk score should be deterministic")
|
|
}
|
|
if len(result.TriggeredRules) != len(lastResult.TriggeredRules) {
|
|
t.Error("Triggered rules should be deterministic")
|
|
}
|
|
if len(result.RequiredControls) != len(lastResult.RequiredControls) {
|
|
t.Error("Required controls should be deterministic")
|
|
}
|
|
}
|
|
lastResult = result
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Helper Functions
|
|
// ============================================================================
|
|
|
|
func createTestFinancialEngine(t *testing.T) *FinancialPolicyEngine {
|
|
engine, err := NewFinancialPolicyEngineFromPath("../../policies/financial_regulations_policy.yaml")
|
|
if err != nil {
|
|
t.Skipf("Skipping test - policy file not found: %v", err)
|
|
return nil
|
|
}
|
|
return engine
|
|
}
|