Skip to content

Commit ceaca8c

Browse files
committed
Add comprehensive unit tests for short_failure_message fallback
Add three new test functions to thoroughly test the short_failure_message fallback behavior in deny-by-default rego evaluations: - TestDenyByDefaultWithShortFailureMessage: Tests the complete fallback chain (custom message > short_failure_message > "denied") with 4 scenarios - TestDenyByDefaultShortFailureMessageOnlyAppliedToDenyByDefault: Verifies that the option is silently ignored for constraints evaluator - TestDenyByDefaultShortFailureMessageWithEntityName: Tests that entity names are properly included in error details Also enhanced the godoc for WithShortFailureMessage() to clearly document the fallback priority, when the option applies, and its type-specific behavior. All tests pass and linting is clean.
1 parent 128bbfc commit ceaca8c

File tree

2 files changed

+191
-0
lines changed

2 files changed

+191
-0
lines changed

internal/engine/eval/rego/eval.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,13 @@ func enrichInputWithEntityProps(
189189
}
190190

191191
// WithShortFailureMessage returns an Option that sets the short failure message for deny-by-default evaluations.
192+
// This message will be used as a fallback when the rego policy doesn't provide a custom "message" field,
193+
// but before defaulting to the generic "denied" message.
194+
//
195+
// The fallback priority is: custom rego message > short_failure_message > "denied"
196+
//
197+
// This option only applies to deny-by-default evaluation type and is silently ignored for other evaluator types
198+
// (such as constraints evaluator).
192199
func WithShortFailureMessage(msg string) interfaces.Option {
193200
return func(eval interfaces.Evaluator) error {
194201
if e, ok := eval.(*Evaluator); ok {

internal/engine/eval/rego/rego_test.go

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -856,3 +856,187 @@ allow {
856856
})
857857
require.ErrorIs(t, err, interfaces.ErrEvaluationFailed, "should have failed the evaluation")
858858
}
859+
860+
func TestDenyByDefaultWithShortFailureMessage(t *testing.T) {
861+
t.Parallel()
862+
863+
tests := []struct {
864+
name string
865+
shortFailureMessage string
866+
regoDef string
867+
expectedMessage string
868+
expectedDetails string
869+
}{
870+
{
871+
name: "uses custom message when provided in rego",
872+
shortFailureMessage: "Repository does not meet security requirements",
873+
regoDef: `
874+
package minder
875+
876+
default allow = false
877+
878+
allow {
879+
input.ingested.secure == true
880+
}
881+
882+
message = "Custom rego message: security check failed"`,
883+
expectedMessage: "Custom rego message: security check failed",
884+
expectedDetails: "Custom rego message: security check failed",
885+
},
886+
{
887+
name: "falls back to short_failure_message when no custom message",
888+
shortFailureMessage: "Repository does not meet security requirements",
889+
regoDef: `
890+
package minder
891+
892+
default allow = false
893+
894+
allow {
895+
input.ingested.secure == true
896+
}`,
897+
expectedMessage: "Repository does not meet security requirements",
898+
expectedDetails: "Repository does not meet security requirements",
899+
},
900+
{
901+
name: "falls back to denied when neither message is provided",
902+
shortFailureMessage: "",
903+
regoDef: `
904+
package minder
905+
906+
default allow = false
907+
908+
allow {
909+
input.ingested.secure == true
910+
}`,
911+
expectedMessage: "denied",
912+
expectedDetails: "denied",
913+
},
914+
{
915+
name: "empty custom message falls back to short_failure_message",
916+
shortFailureMessage: "Short failure message used",
917+
regoDef: `
918+
package minder
919+
920+
default allow = false
921+
922+
allow {
923+
input.ingested.secure == true
924+
}
925+
926+
message = ""`,
927+
expectedMessage: "Short failure message used",
928+
expectedDetails: "Short failure message used",
929+
},
930+
}
931+
932+
for _, tt := range tests {
933+
t.Run(tt.name, func(t *testing.T) {
934+
t.Parallel()
935+
936+
e, err := rego.NewRegoEvaluator(
937+
&minderv1.RuleType_Definition_Eval_Rego{
938+
Type: rego.DenyByDefaultEvaluationType.String(),
939+
Def: tt.regoDef,
940+
},
941+
rego.WithShortFailureMessage(tt.shortFailureMessage),
942+
)
943+
require.NoError(t, err, "could not create evaluator")
944+
945+
emptyPol := map[string]any{}
946+
947+
// Test failure case - ingested data does not match
948+
res, err := e.Eval(context.Background(), emptyPol, nil, &interfaces.Ingested{
949+
Object: map[string]any{
950+
"secure": false,
951+
},
952+
})
953+
954+
require.ErrorIs(t, err, interfaces.ErrEvaluationFailed, "should have failed the evaluation")
955+
956+
// Check the error details
957+
evalErr, ok := err.(*engerrors.EvaluationError)
958+
require.True(t, ok, "error should be an EvaluationError")
959+
assert.Contains(t, evalErr.Details(), tt.expectedDetails, "error details should contain expected message")
960+
961+
// Check the output matches the message
962+
assert.Equal(t, tt.expectedMessage, res.Output, "output should match expected message")
963+
})
964+
}
965+
}
966+
967+
func TestDenyByDefaultShortFailureMessageOnlyAppliedToDenyByDefault(t *testing.T) {
968+
t.Parallel()
969+
970+
// Test that WithShortFailureMessage is silently ignored for constraints evaluator
971+
e, err := rego.NewRegoEvaluator(
972+
&minderv1.RuleType_Definition_Eval_Rego{
973+
Type: rego.ConstraintsEvaluationType.String(),
974+
Def: `
975+
package minder
976+
977+
violations[{"msg": msg}] {
978+
input.ingested.data != "foo"
979+
msg := "data did not contain foo"
980+
}`,
981+
},
982+
rego.WithShortFailureMessage("This should be ignored"),
983+
)
984+
require.NoError(t, err, "could not create evaluator")
985+
986+
emptyPol := map[string]any{}
987+
988+
res, err := e.Eval(context.Background(), emptyPol, nil, &interfaces.Ingested{
989+
Object: map[string]any{
990+
"data": "bar",
991+
},
992+
})
993+
require.ErrorIs(t, err, interfaces.ErrEvaluationFailed, "should have failed the evaluation")
994+
assert.ErrorContains(t, err, "data did not contain foo", "should use constraints message, not short_failure_message")
995+
assert.Equal(t, []any{"data did not contain foo"}, res.Output)
996+
}
997+
998+
func TestDenyByDefaultShortFailureMessageWithEntityName(t *testing.T) {
999+
t.Parallel()
1000+
1001+
e, err := rego.NewRegoEvaluator(
1002+
&minderv1.RuleType_Definition_Eval_Rego{
1003+
Type: rego.DenyByDefaultEvaluationType.String(),
1004+
Def: `
1005+
package minder
1006+
1007+
default allow = false
1008+
1009+
allow {
1010+
input.ingested.compliant == true
1011+
}`,
1012+
},
1013+
rego.WithShortFailureMessage("Repository is not compliant"),
1014+
)
1015+
require.NoError(t, err, "could not create evaluator")
1016+
1017+
emptyPol := map[string]any{}
1018+
1019+
// Create a repository entity to test entity name inclusion
1020+
repo := &minderv1.Repository{
1021+
Owner: "testorg",
1022+
Name: "testrepo",
1023+
}
1024+
1025+
res, err := e.Eval(context.Background(), emptyPol, repo, &interfaces.Ingested{
1026+
Object: map[string]any{
1027+
"compliant": false,
1028+
},
1029+
})
1030+
1031+
require.ErrorIs(t, err, interfaces.ErrEvaluationFailed, "should have failed the evaluation")
1032+
1033+
evalErr, ok := err.(*engerrors.EvaluationError)
1034+
require.True(t, ok, "error should be an EvaluationError")
1035+
1036+
// Check that details include both the message and entity name
1037+
details := evalErr.Details()
1038+
assert.Contains(t, details, "Repository is not compliant", "details should contain short failure message")
1039+
assert.Contains(t, details, "testorg/testrepo", "details should contain entity name")
1040+
1041+
assert.Equal(t, "Repository is not compliant", res.Output, "output should be the short failure message")
1042+
}

0 commit comments

Comments
 (0)