114 lines
3.3 KiB
Go
114 lines
3.3 KiB
Go
package basic
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestCalculator_Evaluate_Normal covers the arithmetic grammar.
|
|
func TestCalculator_Evaluate_Normal(t *testing.T) {
|
|
t.Parallel()
|
|
c := calculator{}
|
|
cases := []struct {
|
|
expr string
|
|
want float64
|
|
}{
|
|
{expr: "1+2*3", want: 7},
|
|
{expr: "(1+2)*3", want: 9},
|
|
{expr: "2^10", want: 1024},
|
|
{expr: "2^3^2", want: 512}, // right-associative
|
|
{expr: "-2^2", want: -4}, // power binds tighter than unary minus
|
|
{expr: "2^-3", want: 0.125}, // negative exponent
|
|
{expr: "sqrt(16)", want: 4},
|
|
{expr: "sqrt(2)+1", want: 2.414213562373095},
|
|
{expr: "10/4", want: 2.5},
|
|
{expr: " ( 5 + 3 ) * 2 ", want: 16},
|
|
}
|
|
for _, tc := range cases {
|
|
tc := tc
|
|
t.Run(tc.expr, func(t *testing.T) {
|
|
t.Parallel()
|
|
got, err := run(t, c, map[string]any{"expression": tc.expr})
|
|
assertNoErr(t, err)
|
|
assertApprox(t, fieldFloat(t, mapResult(t, got), "result"), tc.want)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestCalculator_Evaluate_Errors covers invalid expressions and missing/typed args.
|
|
func TestCalculator_Evaluate_Errors(t *testing.T) {
|
|
t.Parallel()
|
|
c := calculator{}
|
|
byExpression := []struct {
|
|
name string
|
|
args map[string]any
|
|
}{
|
|
{name: "missing expression", args: map[string]any{}},
|
|
{name: "non-string expression", args: map[string]any{"expression": 42}},
|
|
{name: "division by zero", args: map[string]any{"expression": "1/0"}},
|
|
{name: "trailing operator", args: map[string]any{"expression": "1+"}},
|
|
{name: "bare identifier", args: map[string]any{"expression": "x+1"}},
|
|
{name: "unknown function", args: map[string]any{"expression": "sqrt(16"}},
|
|
{name: "negative sqrt", args: map[string]any{"expression": "sqrt(-1)"}},
|
|
{name: "empty string", args: map[string]any{"expression": ""}},
|
|
{name: "non-ASCII injection", args: map[string]any{"expression": "1+2;DROP"}},
|
|
}
|
|
for _, tc := range byExpression {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
_, err := run(t, c, tc.args)
|
|
if err == nil {
|
|
t.Fatalf("want error, got nil")
|
|
}
|
|
if !errors.Is(err, ErrInvalidArgument) && !errors.Is(err, ErrEvaluation) {
|
|
t.Fatalf("err %v wraps neither ErrInvalidArgument nor ErrEvaluation", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestCalculator_InjectionRejected ensures expression injection is refused.
|
|
func TestCalculator_InjectionRejected(t *testing.T) {
|
|
t.Parallel()
|
|
c := calculator{}
|
|
payloads := []string{
|
|
"os.system('id')",
|
|
"process.env.HOME",
|
|
`__import__("os")`,
|
|
"1+2*3;rm -rf /",
|
|
"`id`",
|
|
"eval(1)",
|
|
"1 && 2",
|
|
"len(open('x'))",
|
|
}
|
|
for _, p := range payloads {
|
|
p := p
|
|
t.Run(p, func(t *testing.T) {
|
|
t.Parallel()
|
|
_, err := run(t, c, map[string]any{"expression": p})
|
|
assertIs(t, err, ErrEvaluation)
|
|
})
|
|
}
|
|
// The expression length bound rejects a long payload.
|
|
large := strings.Repeat("1+", 200) + "1"
|
|
_, err := run(t, c, map[string]any{"expression": large})
|
|
assertIs(t, err, ErrEvaluation)
|
|
}
|
|
|
|
// TestCalculator_Schema verifies the JSON Schema contract.
|
|
func TestCalculator_Schema(t *testing.T) {
|
|
t.Parallel()
|
|
p, err := calculator{}.Parameters()
|
|
assertNoErr(t, err)
|
|
reqs := p["required"].([]any)
|
|
if len(reqs) != 1 || reqs[0] != "expression" {
|
|
t.Fatalf("required = %v, want [expression]", reqs)
|
|
}
|
|
props := p["properties"].(map[string]any)
|
|
if got := props["expression"].(map[string]any)["type"]; got != "string" {
|
|
t.Fatalf("expression type = %v, want string", got)
|
|
}
|
|
}
|