mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
40dd2b90d2
Follow-up to #8900. Moves the gzip encoding and decoding config validation off the Go `validateAndInjectDefaults` methods and onto embedded Rego policies, injecting defaults and reporting value errors. Each config registers its recognized options via `config.RegisterConfigSpec` so unknown-option warnings live with the owning struct. Field type validation stays in the Go decode step. --------- Signed-off-by: Sebastian Spaink <sebastianspaink@gmail.com>
227 lines
5.7 KiB
Go
227 lines
5.7 KiB
Go
package metrics
|
|
|
|
import (
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/open-policy-agent/opa/v1/config"
|
|
)
|
|
|
|
func TestConfigValidation(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
input: `{}`,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
input: `{"prom": {}}`,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
input: `{"prom": {"http_request_duration_seconds": {}}}`,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
input: `{"prom": {"http_request_duration_seconds": {"buckets": []}}}`,
|
|
wantErr: false,
|
|
},
|
|
|
|
{
|
|
input: `{"prom": {"http_request_duration_seconds": {"buckets": ["not-a-array"]}}}`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
input: `{"prom": {"http_request_duration_seconds": {"buckets": [1]}}}`,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
input: `{"prom": {"http_request_duration_seconds": {"buckets": "1"}}}`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
input: `{"prom": {"http_request_duration_seconds": {"buckets": [0.001, "1", "2"]}}}`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
input: `{"prom": {"http_request_duration_seconds": {"buckets": ["one", "two", "three"]}}}`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
input: `{"prom": {"http_request_duration_seconds": {"buckets": ["0.1", "0.2", "0.3", "4"]}}}`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
input: `{"prom": {"random_key": 0}}`,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
input: `{"prom": {"http_request_duration_seconds": {"random_key": 0}}}`,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
input: `{"prom": [1, 2, 3]}`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
input: `{"prom": {"http_request_duration_seconds": [1]}}`,
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for i, test := range tests {
|
|
t.Run(fmt.Sprintf("TestConfigValidation_case_%d", i), func(t *testing.T) {
|
|
_, err := NewConfigBuilder().WithBytes([]byte(test.input)).Parse()
|
|
if err != nil && !test.wantErr {
|
|
t.Fail()
|
|
}
|
|
if err == nil && test.wantErr {
|
|
t.Fail()
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConfigValue(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expectedValue []float64
|
|
}{
|
|
{
|
|
input: `{}`,
|
|
expectedValue: defaultHTTPRequestBuckets,
|
|
},
|
|
{
|
|
input: `{"prom": {}}`,
|
|
expectedValue: defaultHTTPRequestBuckets,
|
|
},
|
|
{
|
|
input: `{"prom": {"http_request_duration_seconds": {}}}`,
|
|
expectedValue: defaultHTTPRequestBuckets,
|
|
},
|
|
{
|
|
input: `{"prom": {"http_request_duration_seconds": {"buckets": []}}}`,
|
|
expectedValue: []float64{},
|
|
},
|
|
{
|
|
input: `{"prom": {"http_request_duration_seconds": {"buckets":[0.1, 0.2, 0.3, 4]}}}`,
|
|
expectedValue: []float64{0.1, 0.2, 0.3, 4},
|
|
},
|
|
}
|
|
|
|
for i, test := range tests {
|
|
t.Run(fmt.Sprintf("TestConfigValue_case_%d", i), func(t *testing.T) {
|
|
config, err := NewConfigBuilder().WithBytes([]byte(test.input)).Parse()
|
|
if err != nil {
|
|
t.Fail()
|
|
}
|
|
if !valuesAreEqual(config.Prom.HTTPRequestDurationSeconds.Buckets, test.expectedValue) {
|
|
t.Fail()
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func valuesAreEqual(a []float64, b []float64) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
|
|
for i, v := range a {
|
|
if v != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// TestConfigRejectsWrongShapedOptions covers options the validation policy has to
|
|
// reject itself. Defaults are merged over the raw config, so an option that
|
|
// should hold an object would otherwise be silently replaced by the defaults
|
|
// instead of reaching the Go unmarshal that used to report the type error.
|
|
func TestConfigRejectsWrongShapedOptions(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
wantErr string
|
|
}{
|
|
{
|
|
input: `{"prom": [1, 2, 3]}`,
|
|
wantErr: "invalid value for server.metrics.prom field, should be an object",
|
|
},
|
|
{
|
|
input: `{"prom": "nope"}`,
|
|
wantErr: "invalid value for server.metrics.prom field, should be an object",
|
|
},
|
|
{
|
|
input: `{"prom": {"http_request_duration_seconds": [1]}}`,
|
|
wantErr: "invalid value for server.metrics.prom.http_request_duration_seconds field, should be an object",
|
|
},
|
|
{
|
|
input: `{"prom": {"http_request_duration_seconds": {"buckets": ["a"]}}}`,
|
|
wantErr: "buckets field, should be an array of numbers",
|
|
},
|
|
{
|
|
input: `[1, 2, 3]`,
|
|
wantErr: "config must be an object",
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.input, func(t *testing.T) {
|
|
_, err := NewConfigBuilder().WithBytes([]byte(test.input)).Parse()
|
|
if err == nil {
|
|
t.Fatalf("expected error containing %q, got none", test.wantErr)
|
|
}
|
|
if !strings.Contains(err.Error(), test.wantErr) {
|
|
t.Fatalf("expected error containing %q, got %q", test.wantErr, err.Error())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestConfigNoWarningsForKnownMetricsOptions verifies the server.metrics spec
|
|
// registered by this package lets config.ParseConfig recognize the section's
|
|
// options. Without the registration the section is treated as open, so this also
|
|
// guards against the unknown-option warning below silently going missing.
|
|
func TestConfigNoWarningsForKnownMetricsOptions(t *testing.T) {
|
|
raw := []byte(`{"server": {"metrics": {"prom": {"http_request_duration_seconds": {"buckets": [0.1, 1]}}}}}`)
|
|
conf, err := config.ParseConfig(raw, "id")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(conf.Warnings) != 0 {
|
|
t.Fatalf("expected no warnings, got %v", conf.Warnings)
|
|
}
|
|
}
|
|
|
|
func TestConfigWarnsOnUnknownMetricsOption(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
raw string
|
|
want string
|
|
}{
|
|
{
|
|
raw: `{"server": {"metrics": {"typo": 5}}}`,
|
|
want: `unknown configuration option "server.metrics.typo" encountered`,
|
|
},
|
|
{
|
|
raw: `{"server": {"metrics": {"prom": {"typo": 5}}}}`,
|
|
want: `unknown configuration option "server.metrics.prom.typo" encountered`,
|
|
},
|
|
} {
|
|
t.Run(tc.raw, func(t *testing.T) {
|
|
conf, err := config.ParseConfig([]byte(tc.raw), "id")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !slices.Contains(conf.Warnings, tc.want) {
|
|
t.Fatalf("expected warning %q, got %v", tc.want, conf.Warnings)
|
|
}
|
|
})
|
|
}
|
|
}
|