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) } }