Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion eval/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ var exprEnvOptions = []expr.Option{
// Inject a custom isSorted function into the environment.
functions.IsSorted(),

// Provide a constant timestamp to the expression environment.
// Provide a constant timestamp to the expression environment.
expr.DisableBuiltin("now"),
expr.Function("now", func(...any) (any, error) {
return time.Date(2024, 2, 26, 0, 0, 0, 0, time.UTC).Format(time.RFC3339), nil
Expand Down
191 changes: 191 additions & 0 deletions tests/regressions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
// Copyright 2024 Peter Olds <me@polds.dev>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package tests

import (
"encoding/json"
"fmt"
"os"
"strconv"
"testing"

"gopkg.in/yaml.v3"

"github.com/polds/expr-playground/eval"
)

// TestExamples serves as a regression test for the examples presented in the playground UI.
// If any of the examples change, this test will fail, to help ensure the playground UI is
// updated accordingly, and especially so we don't accidentally push a broken sample.
func TestExamples(t *testing.T) {
examples := setup(t)

// lookup should exactly match the "name" field in the examples.yaml file.
tests := []struct {
lookup string
want string
wantErr bool
}{
{
lookup: "default",
want: "true",
},
{
lookup: "Check image registry",
want: "true",
},
{
lookup: "Disallow HostPorts",
want: "false",
},
{
lookup: "Require non-root containers",
want: "false",
},
{
lookup: "Drop ALL capabilities",
want: "true",
},
{
lookup: "Semantic version check for image tags (Regex)",
want: "false",
},
{
lookup: "URLs",
wantErr: true,
},
{
lookup: "Check JWT custom claims",
want: "true",
},
{
lookup: "Optional",
want: "fallback",
},
{
lookup: "Duration and timestamp",
want: "true",
},
{
lookup: "Quantity",
wantErr: true,
},
{
lookup: "Access Log Filtering",
want: "true",
},
{
lookup: "Custom Metrics",
want: "echo",
},
{
lookup: "Blank",
wantErr: true,
},
}
for _, tc := range tests {
t.Run(tc.lookup, func(t *testing.T) {
var exp Example
for _, e := range examples {
if e.Name == tc.lookup {
exp = e
break
}
}
if exp.Name == "" {
t.Fatalf("failed to find example %q", tc.lookup)
}

got, err := eval.Eval(exp.Expr, marshal(t, exp.Data))
if (err != nil) != tc.wantErr {
t.Errorf("Eval() got error %v, expected error %v", err, tc.wantErr)
}
if tc.wantErr {
return
}

var obj map[string]AlwaysString
if err := json.Unmarshal([]byte(got), &obj); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if s := obj["result"].Value; s != tc.want {
t.Errorf("Eval() got %q, expected %q", s, tc.want)
}
})
}
// Ensure these tests are updated when the examples are updated.
// Not a perfect solution, but it's better than nothing.
if len(examples) != len(tests) {
t.Errorf("Regression test counts got %d, expected %d", len(tests), len(examples))
}
}

type Example struct {
Name string `yaml:"name"`
Expr string `yaml:"expr"`
Data string `yaml:"data"`
}

func setup(t *testing.T) []Example {
t.Helper()

out, err := os.ReadFile("../examples.yaml")
if err != nil {
t.Fatalf("failed to read examples.yaml: %v", err)
}
var examples struct {
Examples []Example `yaml:"examples"`
}
if err := yaml.Unmarshal(out, &examples); err != nil {
t.Fatalf("failed to unmarshal examples.yaml: %v", err)
}
return examples.Examples
}

// Attempt to get the data into either yaml or json format.
func marshal(t *testing.T, s string) map[string]any {
t.Helper()

var v map[string]any
if yamlErr := yaml.Unmarshal([]byte(s), &v); yamlErr != nil {
if err := json.Unmarshal([]byte(s), &v); err != nil {
t.Errorf("failed to unmarshal %q as yaml: %v", s, yamlErr)
t.Fatalf("failed to unmarshal %q as json: %v", s, err)
}
}
return v
}

// AlwaysString attempts to unmarshal the value as a string.
type AlwaysString struct {
Value string
}

func (c *AlwaysString) UnmarshalJSON(b []byte) error {
var raw any
if err := json.Unmarshal(b, &raw); err != nil {
return err
}

switch v := raw.(type) {
case bool:
c.Value = strconv.FormatBool(v)
case string:
c.Value = v
default:
return fmt.Errorf("unsupported type %T", v)
}
return nil
}