environment_test.go 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752
  1. package expressions
  2. import (
  3. "context"
  4. "errors"
  5. "net"
  6. "strings"
  7. "testing"
  8. "github.com/TecharoHQ/anubis/internal/dns"
  9. "github.com/TecharoHQ/anubis/lib/store/memory"
  10. "github.com/google/cel-go/common/types"
  11. "github.com/google/cel-go/common/types/ref"
  12. )
  13. // newTestDNS is a helper function to create a new Dns object with an in-memory cache for testing.
  14. func newTestDNS(forwardTTL int, reverseTTL int) *dns.Dns {
  15. ctx := context.Background()
  16. memStore := memory.New(ctx)
  17. cache := dns.NewDNSCache(forwardTTL, reverseTTL, memStore)
  18. return dns.New(ctx, cache)
  19. }
  20. func TestBotEnvironment(t *testing.T) {
  21. dnsObj := newTestDNS(300, 300)
  22. env, err := BotEnvironment(dnsObj)
  23. if err != nil {
  24. t.Fatalf("failed to create bot environment: %v", err)
  25. }
  26. t.Run("missingHeader", func(t *testing.T) {
  27. tests := []struct {
  28. headers map[string]string
  29. name string
  30. expression string
  31. description string
  32. expected types.Bool
  33. }{
  34. {
  35. name: "missing-header",
  36. expression: `missingHeader(headers, "Missing-Header")`,
  37. headers: map[string]string{
  38. "User-Agent": "test-agent",
  39. "Content-Type": "application/json",
  40. },
  41. expected: types.Bool(true),
  42. description: "should return true when header is missing",
  43. },
  44. {
  45. name: "existing-header",
  46. expression: `missingHeader(headers, "User-Agent")`,
  47. headers: map[string]string{
  48. "User-Agent": "test-agent",
  49. "Content-Type": "application/json",
  50. },
  51. expected: types.Bool(false),
  52. description: "should return false when header exists",
  53. },
  54. {
  55. name: "case-sensitive",
  56. expression: `missingHeader(headers, "user-agent")`,
  57. headers: map[string]string{
  58. "User-Agent": "test-agent",
  59. },
  60. expected: types.Bool(true),
  61. description: "should be case-sensitive (user-agent != User-Agent)",
  62. },
  63. {
  64. name: "empty-headers",
  65. expression: `missingHeader(headers, "Any-Header")`,
  66. headers: map[string]string{},
  67. expected: types.Bool(true),
  68. description: "should return true for any header when map is empty",
  69. },
  70. {
  71. name: "real-world-sec-ch-ua",
  72. expression: `missingHeader(headers, "Sec-Ch-Ua")`,
  73. headers: map[string]string{
  74. "User-Agent": "curl/7.68.0",
  75. "Accept": "*/*",
  76. "Host": "example.com",
  77. },
  78. expected: types.Bool(true),
  79. description: "should detect missing browser-specific headers from bots",
  80. },
  81. {
  82. name: "browser-with-sec-ch-ua",
  83. expression: `missingHeader(headers, "Sec-Ch-Ua")`,
  84. headers: map[string]string{
  85. "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
  86. "Sec-Ch-Ua": `"Chrome"; v="91", "Not A Brand"; v="99"`,
  87. "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
  88. },
  89. expected: types.Bool(false),
  90. description: "should return false when browser sends Sec-Ch-Ua header",
  91. },
  92. }
  93. for _, tt := range tests {
  94. t.Run(tt.name, func(t *testing.T) {
  95. prog, err := Compile(env, tt.expression)
  96. if err != nil {
  97. t.Fatalf("failed to compile expression %q: %v", tt.expression, err)
  98. }
  99. result, _, err := prog.Eval(map[string]interface{}{
  100. "headers": tt.headers,
  101. })
  102. if err != nil {
  103. t.Fatalf("failed to evaluate expression %q: %v", tt.expression, err)
  104. }
  105. if result != tt.expected {
  106. t.Errorf("%s: expected %v, got %v", tt.description, tt.expected, result)
  107. }
  108. })
  109. }
  110. t.Run("function-compilation", func(t *testing.T) {
  111. src := `missingHeader(headers, "Test-Header")`
  112. _, err := Compile(env, src)
  113. if err != nil {
  114. t.Fatalf("failed to compile missingHeader expression: %v", err)
  115. }
  116. })
  117. })
  118. t.Run("segments", func(t *testing.T) {
  119. for _, tt := range []struct {
  120. name string
  121. description string
  122. expression string
  123. path string
  124. expected types.Bool
  125. }{
  126. {
  127. name: "simple",
  128. description: "/ should have one path segment",
  129. expression: `size(segments(path)) == 1`,
  130. path: "/",
  131. expected: types.Bool(true),
  132. },
  133. {
  134. name: "two segments without trailing slash",
  135. description: "/user/foo should have two segments",
  136. expression: `size(segments(path)) == 2`,
  137. path: "/user/foo",
  138. expected: types.Bool(true),
  139. },
  140. {
  141. name: "at least two segments",
  142. description: "/foo/bar/ should have at least two path segments",
  143. expression: `size(segments(path)) >= 2`,
  144. path: "/foo/bar/",
  145. expected: types.Bool(true),
  146. },
  147. {
  148. name: "at most two segments",
  149. description: "/foo/bar/ does not have less than two path segments",
  150. expression: `size(segments(path)) < 2`,
  151. path: "/foo/bar/",
  152. expected: types.Bool(false),
  153. },
  154. } {
  155. t.Run(tt.name, func(t *testing.T) {
  156. prog, err := Compile(env, tt.expression)
  157. if err != nil {
  158. t.Fatalf("failed to compile expression %q: %v", tt.expression, err)
  159. }
  160. result, _, err := prog.Eval(map[string]interface{}{
  161. "path": tt.path,
  162. })
  163. if err != nil {
  164. t.Fatalf("failed to evaluate expression %q: %v", tt.expression, err)
  165. }
  166. if result != tt.expected {
  167. t.Errorf("%s: expected %v, got %v", tt.description, tt.expected, result)
  168. }
  169. })
  170. }
  171. t.Run("invalid", func(t *testing.T) {
  172. for _, tt := range []struct {
  173. env any
  174. name string
  175. description string
  176. expression string
  177. wantFailCompile bool
  178. wantFailEval bool
  179. }{
  180. {
  181. name: "segments of headers",
  182. description: "headers are not a path list",
  183. expression: `segments(headers)`,
  184. env: map[string]any{
  185. "headers": map[string]string{
  186. "foo": "bar",
  187. },
  188. },
  189. wantFailCompile: true,
  190. },
  191. {
  192. name: "invalid path type",
  193. description: "a path should be a sting",
  194. expression: `size(segments(path)) != 0`,
  195. env: map[string]any{
  196. "path": 4,
  197. },
  198. wantFailEval: true,
  199. },
  200. {
  201. name: "invalid path",
  202. description: "a path should start with a leading slash",
  203. expression: `size(segments(path)) != 0`,
  204. env: map[string]any{
  205. "path": "foo",
  206. },
  207. wantFailEval: true,
  208. },
  209. } {
  210. t.Run(tt.name, func(t *testing.T) {
  211. prog, err := Compile(env, tt.expression)
  212. if err != nil {
  213. if !tt.wantFailCompile {
  214. t.Log(tt.description)
  215. t.Fatalf("failed to compile expression %q: %v", tt.expression, err)
  216. } else {
  217. return
  218. }
  219. }
  220. _, _, err = prog.Eval(tt.env)
  221. if err == nil {
  222. t.Log(tt.description)
  223. t.Fatal("wanted an error but got none")
  224. }
  225. t.Log(err)
  226. })
  227. }
  228. })
  229. t.Run("function-compilation", func(t *testing.T) {
  230. src := `size(segments(path)) <= 2`
  231. _, err := Compile(env, src)
  232. if err != nil {
  233. t.Fatalf("failed to compile missingHeader expression: %v", err)
  234. }
  235. })
  236. })
  237. t.Run("regexSafe", func(t *testing.T) {
  238. tests := []struct {
  239. name string
  240. expression string
  241. expected types.String
  242. description string
  243. }{
  244. {
  245. name: "complex-test",
  246. expression: `regexSafe("^(test1|test2|)[a-z]+$")`,
  247. expected: types.String("\\^\\(test1\\|test2\\|\\)\\[a\\-z\\]\\+\\$"),
  248. description: "should escape all reserved regex characters",
  249. },
  250. {
  251. name: "backslash-test",
  252. expression: `regexSafe("use \\\\ for special characters escaping\t, one/\"\\\"/for/cel and one/for/regex")`,
  253. expected: types.String("use \\\\\\\\ for special characters escaping\t, one/\"\\\\\"/for/cel and one/for/regex"),
  254. description: "should escape double-backslashes as double-double-backslashes and ignore cel escaping and forward slashes",
  255. },
  256. }
  257. for _, tt := range tests {
  258. t.Run(tt.name, func(t *testing.T) {
  259. prog, err := Compile(env, tt.expression)
  260. if err != nil {
  261. t.Fatalf("failed to compile expression %q: %v", tt.expression, err)
  262. }
  263. result, _, err := prog.Eval(map[string]interface{}{})
  264. if err != nil {
  265. t.Fatalf("failed to evaluate expression %q: %v", tt.expression, err)
  266. }
  267. if result != tt.expected {
  268. t.Errorf("%s: expected %v, got %v", tt.description, tt.expected, result)
  269. }
  270. })
  271. }
  272. t.Run("function-compilation", func(t *testing.T) {
  273. src := `regexSafe(".*")`
  274. _, err := Compile(env, src)
  275. if err != nil {
  276. t.Fatalf("failed to compile regexSafe expression: %v", err)
  277. }
  278. })
  279. })
  280. t.Run("dnsFunctions", func(t *testing.T) {
  281. originalDNSLookupAddr := dns.DNSLookupAddr
  282. originalDNSLookupHost := dns.DNSLookupHost
  283. defer func() {
  284. dns.DNSLookupAddr = originalDNSLookupAddr
  285. dns.DNSLookupHost = originalDNSLookupHost
  286. }()
  287. t.Run("reverseDNS", func(t *testing.T) {
  288. tests := []struct {
  289. name string
  290. addr string
  291. mockReturn []string
  292. mockError error
  293. expression string
  294. expected ref.Val
  295. description string
  296. }{
  297. {
  298. name: "success",
  299. addr: "8.8.8.8",
  300. mockReturn: []string{"dns.google."},
  301. expression: `reverseDNS("8.8.8.8")`,
  302. expected: types.NewStringList(types.DefaultTypeAdapter, []string{"dns.google"}),
  303. description: "should return domain names for an IP",
  304. },
  305. {
  306. name: "not-found",
  307. addr: "127.0.0.1",
  308. mockReturn: []string{},
  309. mockError: &net.DNSError{IsNotFound: true},
  310. expression: `reverseDNS("127.0.0.1")`,
  311. expected: types.NewStringList(types.DefaultTypeAdapter, []string{}),
  312. description: "should return an empty list when not found",
  313. },
  314. {
  315. name: "error",
  316. addr: "error-addr",
  317. mockError: errors.New("some dns error"),
  318. expression: `reverseDNS("error-addr")`,
  319. expected: types.NewStringList(types.DefaultTypeAdapter, []string{}),
  320. description: "should return empty list on error",
  321. },
  322. }
  323. for _, tt := range tests {
  324. t.Run(tt.name, func(t *testing.T) {
  325. dns.DNSLookupAddr = func(addr string) ([]string, error) {
  326. if addr == tt.addr {
  327. return tt.mockReturn, tt.mockError
  328. }
  329. return nil, errors.New("unexpected address for reverse lookup")
  330. }
  331. prog, err := Compile(env, tt.expression)
  332. if err != nil {
  333. t.Fatalf("failed to compile expression %q: %v", tt.expression, err)
  334. }
  335. result, _, err := prog.Eval(map[string]interface{}{})
  336. if err != nil {
  337. t.Fatalf("failed to evaluate expression %q: %v", tt.expression, err)
  338. }
  339. if result.Equal(tt.expected) != types.True {
  340. t.Errorf("%s: expected %v, got %v", tt.description, tt.expected, result)
  341. }
  342. })
  343. }
  344. })
  345. t.Run("lookupHost", func(t *testing.T) {
  346. tests := []struct {
  347. name string
  348. host string
  349. mockReturn []string
  350. mockError error
  351. expression string
  352. expected ref.Val
  353. description string
  354. }{
  355. {
  356. name: "success",
  357. host: "dns.google",
  358. mockReturn: []string{"8.8.8.8", "8.8.4.4"},
  359. expression: `lookupHost("dns.google")`,
  360. expected: types.NewStringList(types.DefaultTypeAdapter, []string{"8.8.8.8", "8.8.4.4"}),
  361. description: "should return IPs for a domain name",
  362. },
  363. {
  364. name: "not-found",
  365. host: "nonexistent.domain.example.com",
  366. mockReturn: []string{},
  367. mockError: &net.DNSError{IsNotFound: true},
  368. expression: `lookupHost("nonexistent.domain.example.com")`,
  369. expected: types.NewStringList(types.DefaultTypeAdapter, []string{}),
  370. description: "should return an empty list when not found",
  371. },
  372. {
  373. name: "error",
  374. host: "error-host",
  375. mockError: errors.New("some dns error"),
  376. expression: `lookupHost("error-host")`,
  377. expected: types.NewStringList(types.DefaultTypeAdapter, []string{}),
  378. description: "should return empty list on error",
  379. },
  380. }
  381. for _, tt := range tests {
  382. t.Run(tt.name, func(t *testing.T) {
  383. dns.DNSLookupHost = func(host string) ([]string, error) {
  384. if host == tt.host {
  385. return tt.mockReturn, tt.mockError
  386. }
  387. return nil, errors.New("unexpected host for forward lookup")
  388. }
  389. prog, err := Compile(env, tt.expression)
  390. if err != nil {
  391. t.Fatalf("failed to compile expression %q: %v", tt.expression, err)
  392. }
  393. result, _, err := prog.Eval(map[string]interface{}{})
  394. if err != nil {
  395. t.Fatalf("failed to evaluate expression %q: %v", tt.expression, err)
  396. }
  397. if result.Equal(tt.expected) != types.True {
  398. t.Errorf("%s: expected %v, got %v", tt.description, tt.expected, result)
  399. }
  400. })
  401. }
  402. })
  403. t.Run("verifyFCrDNS", func(t *testing.T) {
  404. tests := []struct {
  405. name string
  406. addr string
  407. reverseMockReturn []string
  408. reverseMockError error
  409. forwardMockReturn map[string][]string // name -> ips
  410. forwardMockError map[string]error
  411. expression string
  412. expected types.Bool
  413. description string
  414. }{
  415. {
  416. name: "success",
  417. addr: "8.8.8.8",
  418. reverseMockReturn: []string{"dns.google."},
  419. forwardMockReturn: map[string][]string{"dns.google": {"8.8.8.8", "8.8.4.4"}},
  420. expression: `verifyFCrDNS("8.8.8.8")`,
  421. expected: types.Bool(true),
  422. description: "should return true for valid FCrDNS",
  423. },
  424. {
  425. name: "failure",
  426. addr: "1.2.3.4",
  427. reverseMockReturn: []string{"spoofed.example.com."},
  428. forwardMockReturn: map[string][]string{"spoofed.example.com": {"5.6.7.8"}},
  429. expression: `verifyFCrDNS("1.2.3.4")`,
  430. expected: types.Bool(false),
  431. description: "should return false for invalid FCrDNS",
  432. },
  433. {
  434. name: "reverse-lookup-fails",
  435. addr: "1.1.1.1",
  436. reverseMockError: errors.New("reverse lookup failed"),
  437. expression: `verifyFCrDNS("1.1.1.1")`,
  438. expected: types.Bool(false),
  439. description: "should return false if reverse lookup fails",
  440. },
  441. {
  442. name: "success-with-pattern",
  443. addr: "8.8.8.8",
  444. reverseMockReturn: []string{"dns.google."},
  445. forwardMockReturn: map[string][]string{"dns.google": {"8.8.8.8"}},
  446. expression: `verifyFCrDNS("8.8.8.8", "dns.google")`,
  447. expected: types.Bool(true),
  448. description: "should return true for valid FCrDNS with matching pattern",
  449. },
  450. {
  451. name: "failure-with-pattern",
  452. addr: "8.8.8.8",
  453. reverseMockReturn: []string{"dns.google."},
  454. forwardMockReturn: map[string][]string{"dns.google": {"8.8.8.8"}},
  455. expression: `verifyFCrDNS("8.8.8.8", "wrong.pattern")`,
  456. expected: types.Bool(false),
  457. description: "should return false for FCrDNS with non-matching pattern",
  458. },
  459. }
  460. for _, tt := range tests {
  461. t.Run(tt.name, func(t *testing.T) {
  462. dns.DNSLookupAddr = func(addr string) ([]string, error) {
  463. if addr == tt.addr {
  464. return tt.reverseMockReturn, tt.reverseMockError
  465. }
  466. return nil, errors.New("unexpected address for reverse lookup")
  467. }
  468. dns.DNSLookupHost = func(host string) ([]string, error) {
  469. host = strings.TrimSuffix(host, ".")
  470. if ips, ok := tt.forwardMockReturn[host]; ok {
  471. return ips, nil
  472. }
  473. if err, ok := tt.forwardMockError[host]; ok {
  474. return nil, err
  475. }
  476. return nil, &net.DNSError{IsNotFound: true}
  477. }
  478. prog, err := Compile(env, tt.expression)
  479. if err != nil {
  480. t.Fatalf("failed to compile expression %q: %v", tt.expression, err)
  481. }
  482. result, _, err := prog.Eval(map[string]interface{}{})
  483. if err != nil {
  484. t.Fatalf("failed to evaluate expression %q: %v", tt.expression, err)
  485. }
  486. if result.Equal(tt.expected) != types.True {
  487. t.Errorf("%s: expected %v, got %v", tt.description, tt.expected, result)
  488. }
  489. })
  490. }
  491. })
  492. t.Run("arpaReverseIP", func(t *testing.T) {
  493. tests := []struct {
  494. name string
  495. expression string
  496. expected types.String
  497. description string
  498. evalError bool
  499. }{
  500. {
  501. name: "ipv4",
  502. expression: `arpaReverseIP("1.2.3.4")`,
  503. expected: types.String("4.3.2.1"),
  504. description: "should correctly reverse an IPv4 address",
  505. },
  506. {
  507. name: "ipv6",
  508. expression: `arpaReverseIP("2001:db8::1")`,
  509. expected: types.String("1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2"),
  510. description: "should correctly reverse an IPv6 address",
  511. },
  512. {
  513. name: "ipv6-full",
  514. expression: `arpaReverseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334")`,
  515. expected: types.String("4.3.3.7.0.7.3.0.e.2.a.8.0.0.0.0.0.0.0.0.3.a.5.8.8.b.d.0.1.0.0.2"),
  516. description: "should correctly reverse a fully expanded IPv6 address",
  517. },
  518. {
  519. name: "ipv6-loopback",
  520. expression: `arpaReverseIP("::1")`,
  521. expected: types.String("1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0"),
  522. description: "should correctly reverse the IPv6 loopback address",
  523. },
  524. {
  525. name: "invalid-ip",
  526. expression: `arpaReverseIP("not-an-ip")`,
  527. evalError: true,
  528. description: "should error on an invalid IP",
  529. },
  530. }
  531. for _, tt := range tests {
  532. t.Run(tt.name, func(t *testing.T) {
  533. prog, err := Compile(env, tt.expression)
  534. if err != nil {
  535. t.Fatalf("failed to compile expression %q: %v", tt.expression, err)
  536. }
  537. result, _, err := prog.Eval(map[string]interface{}{})
  538. if tt.evalError {
  539. if err == nil {
  540. t.Errorf("%s: expected an evaluation error, but got none", tt.description)
  541. }
  542. return
  543. }
  544. if err != nil {
  545. t.Fatalf("failed to evaluate expression %q: %v", tt.expression, err)
  546. }
  547. if result.Equal(tt.expected) != types.True {
  548. t.Errorf("%s: expected %v, got %v", tt.description, tt.expected, result)
  549. }
  550. })
  551. }
  552. })
  553. })
  554. }
  555. func TestThresholdEnvironment(t *testing.T) {
  556. env, err := ThresholdEnvironment()
  557. if err != nil {
  558. t.Fatalf("failed to create threshold environment: %v", err)
  559. }
  560. tests := []struct {
  561. variables map[string]interface{}
  562. name string
  563. expression string
  564. description string
  565. expected types.Bool
  566. shouldCompile bool
  567. }{
  568. {
  569. name: "weight-variable-available",
  570. expression: `weight > 100`,
  571. variables: map[string]interface{}{"weight": 150},
  572. expected: types.Bool(true),
  573. description: "should support weight variable in expressions",
  574. shouldCompile: true,
  575. },
  576. {
  577. name: "weight-variable-false-case",
  578. expression: `weight > 100`,
  579. variables: map[string]interface{}{"weight": 50},
  580. expected: types.Bool(false),
  581. description: "should correctly evaluate weight comparisons",
  582. shouldCompile: true,
  583. },
  584. {
  585. name: "missingHeader-not-available",
  586. expression: `missingHeader(headers, "Test")`,
  587. variables: map[string]interface{}{},
  588. expected: types.Bool(false), // not used
  589. description: "should not have missingHeader function available",
  590. shouldCompile: false,
  591. },
  592. }
  593. for _, tt := range tests {
  594. t.Run(tt.name, func(t *testing.T) {
  595. prog, err := Compile(env, tt.expression)
  596. if !tt.shouldCompile {
  597. if err == nil {
  598. t.Fatalf("%s: expected compilation to fail but it succeeded", tt.description)
  599. }
  600. return // Test passed - compilation failed as expected
  601. }
  602. if err != nil {
  603. t.Fatalf("failed to compile expression %q: %v", tt.expression, err)
  604. }
  605. result, _, err := prog.Eval(tt.variables)
  606. if err != nil {
  607. t.Fatalf("failed to evaluate expression %q: %v", tt.expression, err)
  608. }
  609. if result != tt.expected {
  610. t.Errorf("%s: expected %v, got %v", tt.description, tt.expected, result)
  611. }
  612. })
  613. }
  614. }
  615. func TestNewEnvironment(t *testing.T) {
  616. env, err := New()
  617. if err != nil {
  618. t.Fatalf("failed to create new environment: %v", err)
  619. }
  620. tests := []struct {
  621. name string
  622. expression string
  623. variables map[string]interface{}
  624. expectBool *bool // nil if we just want to test compilation or non-bool result
  625. description string
  626. shouldCompile bool
  627. }{
  628. {
  629. name: "randInt-function-compilation",
  630. expression: `randInt(10)`,
  631. variables: map[string]interface{}{},
  632. expectBool: nil, // Don't check result, just compilation
  633. description: "should compile randInt function",
  634. shouldCompile: true,
  635. },
  636. {
  637. name: "randInt-range-validation",
  638. expression: `randInt(10) >= 0 && randInt(10) < 10`,
  639. variables: map[string]interface{}{},
  640. expectBool: boolPtr(true),
  641. description: "should return values in correct range",
  642. shouldCompile: true,
  643. },
  644. {
  645. name: "strings-extension-size",
  646. expression: `"hello".size() == 5`,
  647. variables: map[string]interface{}{},
  648. expectBool: boolPtr(true),
  649. description: "should support string extension functions",
  650. shouldCompile: true,
  651. },
  652. {
  653. name: "strings-extension-contains",
  654. expression: `"hello world".contains("world")`,
  655. variables: map[string]interface{}{},
  656. expectBool: boolPtr(true),
  657. description: "should support string contains function",
  658. shouldCompile: true,
  659. },
  660. {
  661. name: "strings-extension-startsWith",
  662. expression: `"hello world".startsWith("hello")`,
  663. variables: map[string]interface{}{},
  664. expectBool: boolPtr(true),
  665. description: "should support string startsWith function",
  666. shouldCompile: true,
  667. },
  668. }
  669. for _, tt := range tests {
  670. t.Run(tt.name, func(t *testing.T) {
  671. prog, err := Compile(env, tt.expression)
  672. if !tt.shouldCompile {
  673. if err == nil {
  674. t.Fatalf("%s: expected compilation to fail but it succeeded", tt.description)
  675. }
  676. return // Test passed - compilation failed as expected
  677. }
  678. if err != nil {
  679. t.Fatalf("failed to compile expression %q: %v", tt.expression, err)
  680. }
  681. // If we only want to test compilation, skip evaluation
  682. if tt.expectBool == nil {
  683. return
  684. }
  685. result, _, err := prog.Eval(tt.variables)
  686. if err != nil {
  687. t.Fatalf("failed to evaluate expression %q: %v", tt.expression, err)
  688. }
  689. if result != types.Bool(*tt.expectBool) {
  690. t.Errorf("%s: expected %v, got %v", tt.description, *tt.expectBool, result)
  691. }
  692. })
  693. }
  694. }
  695. // Helper function to create bool pointers
  696. func boolPtr(b bool) *bool {
  697. return &b
  698. }