@x47base/pocketbase-addon 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/.dockerignore +9 -0
  2. package/Dockerfile +26 -0
  3. package/FEATURES.md +32 -0
  4. package/LICENSE.md +17 -0
  5. package/MIGRATION.md +49 -0
  6. package/NOTICE.md +5 -0
  7. package/README.md +26 -0
  8. package/adapter.go +31 -0
  9. package/admin/register.go +60 -0
  10. package/admin/register_test.go +37 -0
  11. package/backups/backup_encryption_test.go +82 -0
  12. package/backups/encryption.go +138 -0
  13. package/backups/integration_test.go +51 -0
  14. package/backups/register.go +120 -0
  15. package/backups/restore.go +119 -0
  16. package/backups/s3_test.go +52 -0
  17. package/backups/swap.go +53 -0
  18. package/backups/swap_test.go +75 -0
  19. package/backups/upload.go +52 -0
  20. package/bin/pocketbase-extension.mjs +25 -0
  21. package/cmd/edge/main.go +123 -0
  22. package/cmd/import-fork/main.go +37 -0
  23. package/cmd/loadtest/main.go +61 -0
  24. package/cmd/loadtest/sandbox.go +73 -0
  25. package/cmd/loadtest/sandbox_test.go +20 -0
  26. package/cmd/pocketbase/main.go +78 -0
  27. package/deploy/README.md +148 -0
  28. package/deploy/app/hooks/README.md +2 -0
  29. package/deploy/app/migrations/1789000000_notes.js +16 -0
  30. package/deploy/app/public/README.md +2 -0
  31. package/deploy/compose.secrets.yaml +8 -0
  32. package/deploy/compose.yaml +68 -0
  33. package/deploy/edge.json +13 -0
  34. package/edge/gateway.go +295 -0
  35. package/edge/gateway_test.go +296 -0
  36. package/edge/openapi.json +1 -0
  37. package/edge/policy.go +150 -0
  38. package/features/collection_singleton.go +13 -0
  39. package/features/collection_singleton_test.go +46 -0
  40. package/features/dimensions_test.go +65 -0
  41. package/features/duplicate.go +178 -0
  42. package/features/duplicate_test.go +128 -0
  43. package/features/field_color.go +46 -0
  44. package/features/field_date_only.go +39 -0
  45. package/features/field_json_schema.go +92 -0
  46. package/features/field_scalar_extensions_test.go +66 -0
  47. package/features/files.go +36 -0
  48. package/features/filter_has_any_test.go +81 -0
  49. package/features/generate_test.go +72 -0
  50. package/features/has_any_visibility_test.go +62 -0
  51. package/features/json.go +50 -0
  52. package/features/membership.go +64 -0
  53. package/features/register.go +45 -0
  54. package/features/schema_test.go +58 -0
  55. package/features/ui/main.js +133 -0
  56. package/features/ui/settings.js +31 -0
  57. package/go.mod +54 -0
  58. package/go.sum +159 -0
  59. package/internal/archive/create.go +91 -0
  60. package/internal/archive/create_test.go +125 -0
  61. package/internal/archive/extract.go +99 -0
  62. package/internal/archive/extract_test.go +88 -0
  63. package/jsvm/binds.go +1273 -0
  64. package/jsvm/binds_app_reset_test.go +314 -0
  65. package/jsvm/binds_test.go +1870 -0
  66. package/jsvm/form_data.go +149 -0
  67. package/jsvm/form_data_test.go +225 -0
  68. package/jsvm/internal/types/generated/embed.go +6 -0
  69. package/jsvm/internal/types/generated/types.d.ts +24820 -0
  70. package/jsvm/internal/types/types.go +1408 -0
  71. package/jsvm/jsvm.go +587 -0
  72. package/jsvm/mapper.go +67 -0
  73. package/jsvm/mapper_test.go +42 -0
  74. package/jsvm/pool.go +73 -0
  75. package/jsvm/program_source_test.go +24 -0
  76. package/loadtest/loadtest.go +202 -0
  77. package/loadtest/loadtest_test.go +84 -0
  78. package/localization/README.md +23 -0
  79. package/localization/catalogue.json +483 -0
  80. package/localization/localization.go +94 -0
  81. package/localization/localization_test.go +21 -0
  82. package/mail/register.go +80 -0
  83. package/mail/register_test.go +49 -0
  84. package/mail/resolve.go +91 -0
  85. package/migration/import.go +96 -0
  86. package/migration/import_test.go +58 -0
  87. package/otp/otp.go +56 -0
  88. package/otp/otp_test.go +56 -0
  89. package/package.json +51 -0
  90. package/scripts/check-edge.py +42 -0
  91. package/scripts/check.sh +11 -0
  92. package/scripts/sync-jsvm-types.sh +10 -0
  93. package/security/README.md +94 -0
  94. package/security/assurance_test.go +149 -0
  95. package/security/compatibility_test.go +128 -0
  96. package/security/config.go +78 -0
  97. package/security/dashboard_test.go +103 -0
  98. package/security/management.go +169 -0
  99. package/security/openapi.json +508 -0
  100. package/security/review.go +34 -0
  101. package/security/security.go +503 -0
  102. package/security/security_test.go +146 -0
  103. package/security/state.go +116 -0
  104. package/security/ui/dashboard.css +4 -0
  105. package/security/ui/dashboard.js +83 -0
  106. package/security/ui/main.js +15 -0
  107. package/security/ui/model.js +32 -0
  108. package/security/ui/model.test.mjs +25 -0
  109. package/security/ui/registration.test.mjs +10 -0
  110. package/settings/env_test.go +41 -0
  111. package/settings/openapi.json +193 -0
  112. package/settings/settings.go +155 -0
  113. package/settings/settings_test.go +31 -0
  114. package/watcher/watcher.go +192 -0
  115. package/watcher/watcher_test.go +200 -0
  116. package/web/static.go +99 -0
  117. package/web/static_test.go +48 -0
@@ -0,0 +1,1870 @@
1
+ package jsvm
2
+
3
+ import (
4
+ "bytes"
5
+ "encoding/json/v2"
6
+ "errors"
7
+ "fmt"
8
+ "io"
9
+ "mime/multipart"
10
+ "net/http"
11
+ "net/http/httptest"
12
+ "os"
13
+ "path/filepath"
14
+ "strconv"
15
+ "strings"
16
+ "testing"
17
+ "time"
18
+
19
+ "github.com/dop251/goja"
20
+ validation "github.com/pocketbase/ozzo-validation/v4"
21
+ "github.com/pocketbase/pocketbase/apis"
22
+ "github.com/pocketbase/pocketbase/core"
23
+ "github.com/pocketbase/pocketbase/tests"
24
+ "github.com/pocketbase/pocketbase/tools/filesystem"
25
+ "github.com/pocketbase/pocketbase/tools/mailer"
26
+ "github.com/pocketbase/pocketbase/tools/router"
27
+ "github.com/pocketbase/pocketbase/tools/types"
28
+ "github.com/spf13/cast"
29
+ )
30
+
31
+ func testBindsCount(vm *goja.Runtime, namespace string, count int, t *testing.T) {
32
+ v, err := vm.RunString(`Object.keys(` + namespace + `).length`)
33
+ if err != nil {
34
+ t.Fatal(err)
35
+ }
36
+
37
+ total, _ := v.Export().(int64)
38
+
39
+ if int(total) != count {
40
+ t.Fatalf("Expected %d %s binds, got %d", count, namespace, total)
41
+ }
42
+ }
43
+
44
+ // note: this test is useful as a reminder to update the tests in case
45
+ // a new base binding is added.
46
+ func TestBindCoreCount(t *testing.T) {
47
+ vm := goja.New()
48
+ BindCore(vm)
49
+
50
+ testBindsCount(vm, "this", 41, t)
51
+ }
52
+
53
+ func TestBindCoreSleep(t *testing.T) {
54
+ vm := goja.New()
55
+ BindCore(vm)
56
+ vm.Set("reader", strings.NewReader("test"))
57
+
58
+ start := time.Now()
59
+ _, err := vm.RunString(`
60
+ sleep(100);
61
+ `)
62
+ if err != nil {
63
+ t.Fatal(err)
64
+ }
65
+
66
+ lasted := time.Since(start).Milliseconds()
67
+ if lasted < 100 || lasted > 150 {
68
+ t.Fatalf("Expected to sleep for ~100ms, got %d", lasted)
69
+ }
70
+ }
71
+
72
+ func TestBindCoreReaderToString(t *testing.T) {
73
+ vm := goja.New()
74
+ BindCore(vm)
75
+ vm.Set("reader", strings.NewReader("test"))
76
+
77
+ _, err := vm.RunString(`
78
+ let result = readerToString(reader)
79
+
80
+ if (result != "test") {
81
+ throw new Error('Expected "test", got ' + result);
82
+ }
83
+ `)
84
+ if err != nil {
85
+ t.Fatal(err)
86
+ }
87
+ }
88
+
89
+ func TestBindCoreToString(t *testing.T) {
90
+ vm := goja.New()
91
+ BindCore(vm)
92
+ vm.Set("scenarios", []struct {
93
+ Name string
94
+ Value any
95
+ Expected string
96
+ }{
97
+ {"null", nil, ""},
98
+ {"string", "test", "test"},
99
+ {"number", -12.4, "-12.4"},
100
+ {"bool", true, "true"},
101
+ {"arr", []int{1, 2, 3}, `[1,2,3]`},
102
+ {"obj", map[string]any{"test": 123}, `{"test":123}`},
103
+ {"reader", strings.NewReader("test"), "test"},
104
+ {"struct", struct {
105
+ Name string
106
+ private string
107
+ }{Name: "123", private: "456"}, `{"Name":"123"}`},
108
+ })
109
+
110
+ _, err := vm.RunString(`
111
+ for (let s of scenarios) {
112
+ let str = toString(s.value)
113
+ if (str != s.expected) {
114
+ throw new Error('[' + s.name + '] Expected string ' + s.expected + ', got ' + str);
115
+ }
116
+ }
117
+ `)
118
+ if err != nil {
119
+ t.Fatal(err)
120
+ }
121
+ }
122
+
123
+ func TestBindCoreToBytes(t *testing.T) {
124
+ vm := goja.New()
125
+ BindCore(vm)
126
+ vm.Set("bytesEqual", bytes.Equal)
127
+ vm.Set("scenarios", []struct {
128
+ Name string
129
+ Value any
130
+ Expected []byte
131
+ }{
132
+ {"null", nil, []byte{}},
133
+ {"string", "test", []byte("test")},
134
+ {"number", -12.4, []byte("-12.4")},
135
+ {"bool", true, []byte("true")},
136
+ {"arr", []int{1, 2, 3}, []byte{1, 2, 3}},
137
+ {"jsonraw", types.JSONRaw{1, 2, 3}, []byte{1, 2, 3}},
138
+ {"reader", strings.NewReader("test"), []byte("test")},
139
+ {"obj", map[string]any{"test": 123}, []byte(`{"test":123}`)},
140
+ {"struct", struct {
141
+ Name string
142
+ private string
143
+ }{Name: "123", private: "456"}, []byte(`{"Name":"123"}`)},
144
+ })
145
+
146
+ _, err := vm.RunString(`
147
+ for (let s of scenarios) {
148
+ let b = toBytes(s.value)
149
+ if (!Array.isArray(b)) {
150
+ throw new Error('[' + s.name + '] Expected toBytes to return an array');
151
+ }
152
+
153
+ if (!bytesEqual(b, s.expected)) {
154
+ throw new Error('[' + s.name + '] Expected bytes ' + s.expected + ', got ' + b);
155
+ }
156
+ }
157
+ `)
158
+ if err != nil {
159
+ t.Fatal(err)
160
+ }
161
+ }
162
+
163
+ func TestBindCoreUnmarshal(t *testing.T) {
164
+ vm := goja.New()
165
+ BindCore(vm)
166
+ vm.Set("data", &map[string]any{"a": 123})
167
+
168
+ _, err := vm.RunString(`
169
+ unmarshal({"b": 456}, data)
170
+
171
+ if (data.a != 123) {
172
+ throw new Error('Expected data.a 123, got ' + data.a);
173
+ }
174
+
175
+ if (data.b != 456) {
176
+ throw new Error('Expected data.b 456, got ' + data.b);
177
+ }
178
+ `)
179
+ if err != nil {
180
+ t.Fatal(err)
181
+ }
182
+ }
183
+
184
+ func TestBindCoreContext(t *testing.T) {
185
+ vm := goja.New()
186
+ BindCore(vm)
187
+
188
+ _, err := vm.RunString(`
189
+ const base = new Context(null, "a", 123);
190
+ const sub = new Context(base, "b", 456);
191
+
192
+ const scenarios = [
193
+ {key: "a", expected: 123},
194
+ {key: "b", expected: 456},
195
+ ];
196
+
197
+ for (let s of scenarios) {
198
+ if (sub.value(s.key) != s.expected) {
199
+ throw new("Expected " +s.key + " value " + s.expected + ", got " + sub.value(s.key));
200
+ }
201
+ }
202
+ `)
203
+ if err != nil {
204
+ t.Fatal(err)
205
+ }
206
+ }
207
+
208
+ func TestBindCoreCookie(t *testing.T) {
209
+ vm := goja.New()
210
+ BindCore(vm)
211
+
212
+ _, err := vm.RunString(`
213
+ const cookie = new Cookie({
214
+ name: "example_name",
215
+ value: "example_value",
216
+ path: "/example_path",
217
+ domain: "example.com",
218
+ maxAge: 10,
219
+ secure: true,
220
+ httpOnly: true,
221
+ sameSite: 3,
222
+ });
223
+
224
+ const result = cookie.string();
225
+
226
+ const expected = "example_name=example_value; Path=/example_path; Domain=example.com; Max-Age=10; HttpOnly; Secure; SameSite=Strict";
227
+
228
+ if (expected != result) {
229
+ throw new("Expected \n" + expected + "\ngot\n" + result);
230
+ }
231
+ `)
232
+ if err != nil {
233
+ t.Fatal(err)
234
+ }
235
+ }
236
+
237
+ func TestBindCoreSubscriptionMessage(t *testing.T) {
238
+ vm := goja.New()
239
+ BindCore(vm)
240
+ vm.Set("bytesToString", func(b []byte) string {
241
+ return string(b)
242
+ })
243
+
244
+ _, err := vm.RunString(`
245
+ const payload = {
246
+ name: "test",
247
+ data: '{"test":123}'
248
+ }
249
+
250
+ const result = new SubscriptionMessage(payload);
251
+
252
+ if (result.name != payload.name) {
253
+ throw new("Expected name " + payload.name + ", got " + result.name);
254
+ }
255
+
256
+ if (bytesToString(result.data) != payload.data) {
257
+ throw new("Expected data '" + payload.data + "', got '" + bytesToString(result.data) + "'");
258
+ }
259
+ `)
260
+ if err != nil {
261
+ t.Fatal(err)
262
+ }
263
+ }
264
+
265
+ func TestBindCoreRecord(t *testing.T) {
266
+ app, _ := tests.NewTestApp()
267
+ defer app.Cleanup()
268
+
269
+ collection, err := app.FindCachedCollectionByNameOrId("users")
270
+ if err != nil {
271
+ t.Fatal(err)
272
+ }
273
+
274
+ vm := goja.New()
275
+ BindCore(vm)
276
+ vm.Set("collection", collection)
277
+
278
+ // without record data
279
+ // ---
280
+ v1, err := vm.RunString(`new Record(collection)`)
281
+ if err != nil {
282
+ t.Fatal(err)
283
+ }
284
+
285
+ m1, ok := v1.Export().(*core.Record)
286
+ if !ok {
287
+ t.Fatalf("Expected m1 to be models.Record, got \n%v", m1)
288
+ }
289
+
290
+ // with record data
291
+ // ---
292
+ v2, err := vm.RunString(`new Record(collection, { email: "test@example.com" })`)
293
+ if err != nil {
294
+ t.Fatal(err)
295
+ }
296
+
297
+ m2, ok := v2.Export().(*core.Record)
298
+ if !ok {
299
+ t.Fatalf("Expected m2 to be core.Record, got \n%v", m2)
300
+ }
301
+
302
+ if m2.Collection().Name != "users" {
303
+ t.Fatalf("Expected record with collection %q, got \n%v", "users", m2.Collection())
304
+ }
305
+
306
+ if m2.Email() != "test@example.com" {
307
+ t.Fatalf("Expected record with email field set to %q, got \n%v", "test@example.com", m2)
308
+ }
309
+ }
310
+
311
+ func TestBindCoreCollection(t *testing.T) {
312
+ vm := goja.New()
313
+ BindCore(vm)
314
+
315
+ v, err := vm.RunString(`new Collection({ name: "test", createRule: "@request.auth.id != ''", fields: [{name: "title", "type": "text"}] })`)
316
+ if err != nil {
317
+ t.Fatal(err)
318
+ }
319
+
320
+ m, ok := v.Export().(*core.Collection)
321
+ if !ok {
322
+ t.Fatalf("Expected core.Collection, got %v", m)
323
+ }
324
+
325
+ if m.Name != "test" {
326
+ t.Fatalf("Expected collection with name %q, got %q", "test", m.Name)
327
+ }
328
+
329
+ expectedRule := "@request.auth.id != ''"
330
+ if m.CreateRule == nil || *m.CreateRule != expectedRule {
331
+ t.Fatalf("Expected create rule %q, got %v", "@request.auth.id != ''", m.CreateRule)
332
+ }
333
+
334
+ if f := m.Fields.GetByName("title"); f == nil {
335
+ t.Fatalf("Expected fields to be set, got %v", m.Fields)
336
+ }
337
+ }
338
+
339
+ func TestBindCoreFieldsList(t *testing.T) {
340
+ vm := goja.New()
341
+ BindCore(vm)
342
+
343
+ v, err := vm.RunString(`new FieldsList([{name: "title", "type": "text"}])`)
344
+ if err != nil {
345
+ t.Fatal(err)
346
+ }
347
+
348
+ m, ok := v.Export().(*core.FieldsList)
349
+ if !ok {
350
+ t.Fatalf("Expected core.FieldsList, got %v", m)
351
+ }
352
+
353
+ if f := m.GetByName("title"); f == nil {
354
+ t.Fatalf("Expected fields list to be loaded, got %v", m)
355
+ }
356
+ }
357
+
358
+ func TestBindCoreField(t *testing.T) {
359
+ vm := goja.New()
360
+ BindCore(vm)
361
+
362
+ v, err := vm.RunString(`new Field({name: "test", "type": "bool"})`)
363
+ if err != nil {
364
+ t.Fatal(err)
365
+ }
366
+
367
+ f, ok := v.Export().(*core.BoolField)
368
+ if !ok {
369
+ t.Fatalf("Expected *core.BoolField, got %v", f)
370
+ }
371
+
372
+ if f.Name != "test" {
373
+ t.Fatalf("Expected field %q, got %v", "test", f)
374
+ }
375
+ }
376
+
377
+ func isType[T any](v any) bool {
378
+ _, ok := v.(T)
379
+ return ok
380
+ }
381
+
382
+ func TestBindCoreNamedFields(t *testing.T) {
383
+ t.Parallel()
384
+
385
+ vm := goja.New()
386
+ BindCore(vm)
387
+
388
+ scenarios := []struct {
389
+ js string
390
+ typeFunc func(v any) bool
391
+ }{
392
+ {
393
+ "new NumberField({name: 'test'})",
394
+ isType[*core.NumberField],
395
+ },
396
+ {
397
+ "new BoolField({name: 'test'})",
398
+ isType[*core.BoolField],
399
+ },
400
+ {
401
+ "new TextField({name: 'test'})",
402
+ isType[*core.TextField],
403
+ },
404
+ {
405
+ "new URLField({name: 'test'})",
406
+ isType[*core.URLField],
407
+ },
408
+ {
409
+ "new EmailField({name: 'test'})",
410
+ isType[*core.EmailField],
411
+ },
412
+ {
413
+ "new EditorField({name: 'test'})",
414
+ isType[*core.EditorField],
415
+ },
416
+ {
417
+ "new PasswordField({name: 'test'})",
418
+ isType[*core.PasswordField],
419
+ },
420
+ {
421
+ "new DateField({name: 'test'})",
422
+ isType[*core.DateField],
423
+ },
424
+ {
425
+ "new AutodateField({name: 'test'})",
426
+ isType[*core.AutodateField],
427
+ },
428
+ {
429
+ "new JSONField({name: 'test'})",
430
+ isType[*core.JSONField],
431
+ },
432
+ {
433
+ "new RelationField({name: 'test'})",
434
+ isType[*core.RelationField],
435
+ },
436
+ {
437
+ "new SelectField({name: 'test'})",
438
+ isType[*core.SelectField],
439
+ },
440
+ {
441
+ "new FileField({name: 'test'})",
442
+ isType[*core.FileField],
443
+ },
444
+ {
445
+ "new GeoPointField({name: 'test'})",
446
+ isType[*core.GeoPointField],
447
+ },
448
+ }
449
+
450
+ for _, s := range scenarios {
451
+ t.Run(s.js, func(t *testing.T) {
452
+ v, err := vm.RunString(s.js)
453
+ if err != nil {
454
+ t.Fatal(err)
455
+ }
456
+
457
+ f, ok := v.Export().(core.Field)
458
+ if !ok {
459
+ t.Fatalf("Expected core.Field instance, got %T (%v)", f, f)
460
+ }
461
+
462
+ if !s.typeFunc(f) {
463
+ t.Fatalf("Unexpected field type %T (%v)", f, f)
464
+ }
465
+
466
+ if f.GetName() != "test" {
467
+ t.Fatalf("Expected field %q, got %v", "test", f)
468
+ }
469
+ })
470
+ }
471
+ }
472
+
473
+ func TestBindCoreMailerMessage(t *testing.T) {
474
+ vm := goja.New()
475
+ BindCore(vm)
476
+
477
+ v, err := vm.RunString(`new MailerMessage({
478
+ from: {name: "test_from", address: "test_from@example.com"},
479
+ to: [
480
+ {name: "test_to1", address: "test_to1@example.com"},
481
+ {name: "test_to2", address: "test_to2@example.com"},
482
+ ],
483
+ bcc: [
484
+ {name: "test_bcc1", address: "test_bcc1@example.com"},
485
+ {name: "test_bcc2", address: "test_bcc2@example.com"},
486
+ ],
487
+ cc: [
488
+ {name: "test_cc1", address: "test_cc1@example.com"},
489
+ {name: "test_cc2", address: "test_cc2@example.com"},
490
+ ],
491
+ subject: "test_subject",
492
+ html: "test_html",
493
+ text: "test_text",
494
+ headers: {
495
+ header1: "a",
496
+ header2: "b",
497
+ }
498
+ })`)
499
+ if err != nil {
500
+ t.Fatal(err)
501
+ }
502
+
503
+ m, ok := v.Export().(*mailer.Message)
504
+ if !ok {
505
+ t.Fatalf("Expected mailer.Message, got %v", m)
506
+ }
507
+
508
+ raw, err := json.Marshal(m, json.Deterministic(true))
509
+ if err != nil {
510
+ t.Fatal(err)
511
+ }
512
+
513
+ expected := `{"from":{"Name":"test_from","Address":"test_from@example.com"},"to":[{"Name":"test_to1","Address":"test_to1@example.com"},{"Name":"test_to2","Address":"test_to2@example.com"}],"bcc":[{"Name":"test_bcc1","Address":"test_bcc1@example.com"},{"Name":"test_bcc2","Address":"test_bcc2@example.com"}],"cc":[{"Name":"test_cc1","Address":"test_cc1@example.com"},{"Name":"test_cc2","Address":"test_cc2@example.com"}],"subject":"test_subject","html":"test_html","text":"test_text","headers":{"header1":"a","header2":"b"},"attachments":{},"inlineAttachments":{}}`
514
+
515
+ if string(raw) != expected {
516
+ t.Fatalf("Expected \n%s, \ngot \n%s", expected, raw)
517
+ }
518
+ }
519
+
520
+ func TestBindCoreCommand(t *testing.T) {
521
+ vm := goja.New()
522
+ BindCore(vm)
523
+
524
+ _, err := vm.RunString(`
525
+ let runCalls = 0;
526
+
527
+ let cmd = new Command({
528
+ use: "test",
529
+ run: (c, args) => {
530
+ runCalls++;
531
+ }
532
+ });
533
+
534
+ cmd.run(null, []);
535
+
536
+ if (cmd.use != "test") {
537
+ throw new Error('Expected cmd.use "test", got: ' + cmd.use);
538
+ }
539
+
540
+ if (runCalls != 1) {
541
+ throw new Error('Expected runCalls 1, got: ' + runCalls);
542
+ }
543
+ `)
544
+ if err != nil {
545
+ t.Fatal(err)
546
+ }
547
+ }
548
+
549
+ func TestBindCoreRequestInfo(t *testing.T) {
550
+ vm := goja.New()
551
+ BindCore(vm)
552
+
553
+ _, err := vm.RunString(`
554
+ const info = new RequestInfo({
555
+ body: {"name": "test2"}
556
+ });
557
+
558
+ if (info.body?.name != "test2") {
559
+ throw new Error('Expected info.body.name to be test2, got: ' + info.body?.name);
560
+ }
561
+ `)
562
+ if err != nil {
563
+ t.Fatal(err)
564
+ }
565
+ }
566
+
567
+ func TestBindCoreMiddleware(t *testing.T) {
568
+ vm := goja.New()
569
+ BindCore(vm)
570
+
571
+ _, err := vm.RunString(`
572
+ const m = new Middleware(
573
+ (e) => {},
574
+ 10,
575
+ "test"
576
+ );
577
+
578
+ if (!m) {
579
+ throw new Error('Expected non-empty Middleware instance');
580
+ }
581
+ `)
582
+ if err != nil {
583
+ t.Fatal(err)
584
+ }
585
+ }
586
+
587
+ func TestBindCoreTimezone(t *testing.T) {
588
+ vm := goja.New()
589
+ BindCore(vm)
590
+
591
+ _, err := vm.RunString(`
592
+ const v0 = (new Timezone()).string();
593
+ if (v0 != "UTC") {
594
+ throw new Error("(v0) Expected UTC got " + v0)
595
+ }
596
+
597
+ const v1 = (new Timezone("invalid")).string();
598
+ if (v1 != "UTC") {
599
+ throw new Error("(v1) Expected UTC got " + v1)
600
+ }
601
+
602
+ const v2 = (new Timezone("EET")).string();
603
+ if (v2 != "EET") {
604
+ throw new Error("(v2) Expected EET got " + v2)
605
+ }
606
+ `)
607
+ if err != nil {
608
+ t.Fatal(err)
609
+ }
610
+ }
611
+
612
+ func TestBindCoreDateTime(t *testing.T) {
613
+ vm := goja.New()
614
+ BindCore(vm)
615
+
616
+ _, err := vm.RunString(`
617
+ const now = new DateTime();
618
+ if (now.isZero()) {
619
+ throw new Error('(now) Expected to fallback to now, got zero value');
620
+ }
621
+
622
+ const nowPart = now.string().substring(0, 19)
623
+
624
+ const scenarios = [
625
+ // empty datetime string and no custom location
626
+ {date: new DateTime(''), expected: nowPart},
627
+ // empty datetime string and custom default location (should be ignored)
628
+ {date: new DateTime('', 'Asia/Tokyo'), expected: nowPart},
629
+ // full datetime string and no custom default location
630
+ {date: new DateTime('2023-01-01 00:00:00.000Z'), expected: "2023-01-01 00:00:00.000Z"},
631
+ // invalid location (fallback to UTC)
632
+ {date: new DateTime('2025-10-26 03:00:00', 'invalid'), expected: "2025-10-26 03:00:00.000Z"},
633
+ // CET
634
+ {date: new DateTime('2025-10-26 03:00:00', 'Europe/Amsterdam'), expected: "2025-10-26 02:00:00.000Z"},
635
+ // CEST
636
+ {date: new DateTime('2025-10-26 01:00:00', 'Europe/Amsterdam'), expected: "2025-10-25 23:00:00.000Z"},
637
+ // with timezone/offset in the date string (aka. should ignore the custom default location)
638
+ {date: new DateTime('2025-10-26 01:00:00 +0200', 'Asia/Tokyo'), expected: "2025-10-25 23:00:00.000Z"},
639
+ ];
640
+
641
+ for (let i = 0; i < scenarios.length; i++) {
642
+ const s = scenarios[i];
643
+ if (!s.date.string().includes(s.expected)) {
644
+ throw new Error('(' + i + ') ' + s.date.string() + ' does not contain expected ' + s.expected);
645
+ }
646
+ }
647
+ `)
648
+ if err != nil {
649
+ t.Fatal(err)
650
+ }
651
+ }
652
+
653
+ func TestBindCoreValidationError(t *testing.T) {
654
+ vm := goja.New()
655
+ BindCore(vm)
656
+
657
+ scenarios := []struct {
658
+ js string
659
+ expectCode string
660
+ expectMessage string
661
+ }{
662
+ {
663
+ `new ValidationError()`,
664
+ "",
665
+ "",
666
+ },
667
+ {
668
+ `new ValidationError("test_code")`,
669
+ "test_code",
670
+ "",
671
+ },
672
+ {
673
+ `new ValidationError("test_code", "test_message")`,
674
+ "test_code",
675
+ "test_message",
676
+ },
677
+ }
678
+
679
+ for _, s := range scenarios {
680
+ v, err := vm.RunString(s.js)
681
+ if err != nil {
682
+ t.Fatal(err)
683
+ }
684
+
685
+ m, ok := v.Export().(validation.Error)
686
+ if !ok {
687
+ t.Fatalf("[%s] Expected validation.Error, got %v", s.js, m)
688
+ }
689
+
690
+ if m.Code() != s.expectCode {
691
+ t.Fatalf("[%s] Expected code %q, got %q", s.js, s.expectCode, m.Code())
692
+ }
693
+
694
+ if m.Message() != s.expectMessage {
695
+ t.Fatalf("[%s] Expected message %q, got %q", s.js, s.expectMessage, m.Message())
696
+ }
697
+ }
698
+ }
699
+
700
+ func TestBindDbx(t *testing.T) {
701
+ app, _ := tests.NewTestApp()
702
+ defer app.Cleanup()
703
+
704
+ vm := goja.New()
705
+ vm.Set("db", app.DB())
706
+ BindCore(vm)
707
+ BindDbx(vm)
708
+
709
+ testBindsCount(vm, "$dbx", 15, t)
710
+
711
+ sceneraios := []struct {
712
+ js string
713
+ expected string
714
+ }{
715
+ {
716
+ `$dbx.exp("a = 1").build(db, {})`,
717
+ "a = 1",
718
+ },
719
+ {
720
+ `$dbx.hashExp({
721
+ "a": 1,
722
+ b: null,
723
+ c: [1, 2, 3],
724
+ }).build(db, {})`,
725
+ "`a`={:p0} AND `b` IS NULL AND `c` IN ({:p1}, {:p2}, {:p3})",
726
+ },
727
+ {
728
+ `$dbx.not($dbx.exp("a = 1")).build(db, {})`,
729
+ "NOT (a = 1)",
730
+ },
731
+ {
732
+ `$dbx.and($dbx.exp("a = 1"), $dbx.exp("b = 2")).build(db, {})`,
733
+ "(a = 1) AND (b = 2)",
734
+ },
735
+ {
736
+ `$dbx.or($dbx.exp("a = 1"), $dbx.exp("b = 2")).build(db, {})`,
737
+ "(a = 1) OR (b = 2)",
738
+ },
739
+ {
740
+ `$dbx.in("a", 1, 2, 3).build(db, {})`,
741
+ "`a` IN ({:p0}, {:p1}, {:p2})",
742
+ },
743
+ {
744
+ `$dbx.notIn("a", 1, 2, 3).build(db, {})`,
745
+ "`a` NOT IN ({:p0}, {:p1}, {:p2})",
746
+ },
747
+ {
748
+ `$dbx.like("a", "test1", "test2").match(true, false).build(db, {})`,
749
+ "`a` LIKE {:p0} AND `a` LIKE {:p1}",
750
+ },
751
+ {
752
+ `$dbx.orLike("a", "test1", "test2").match(false, true).build(db, {})`,
753
+ "`a` LIKE {:p0} OR `a` LIKE {:p1}",
754
+ },
755
+ {
756
+ `$dbx.notLike("a", "test1", "test2").match(true, false).build(db, {})`,
757
+ "`a` NOT LIKE {:p0} AND `a` NOT LIKE {:p1}",
758
+ },
759
+ {
760
+ `$dbx.orNotLike("a", "test1", "test2").match(false, false).build(db, {})`,
761
+ "`a` NOT LIKE {:p0} OR `a` NOT LIKE {:p1}",
762
+ },
763
+ {
764
+ `$dbx.exists($dbx.exp("a = 1")).build(db, {})`,
765
+ "EXISTS (a = 1)",
766
+ },
767
+ {
768
+ `$dbx.notExists($dbx.exp("a = 1")).build(db, {})`,
769
+ "NOT EXISTS (a = 1)",
770
+ },
771
+ {
772
+ `$dbx.between("a", 1, 2).build(db, {})`,
773
+ "`a` BETWEEN {:p0} AND {:p1}",
774
+ },
775
+ {
776
+ `$dbx.notBetween("a", 1, 2).build(db, {})`,
777
+ "`a` NOT BETWEEN {:p0} AND {:p1}",
778
+ },
779
+ }
780
+
781
+ for _, s := range sceneraios {
782
+ result, err := vm.RunString(s.js)
783
+ if err != nil {
784
+ t.Fatalf("[%s] Failed to execute js script, got %v", s.js, err)
785
+ }
786
+
787
+ v, _ := result.Export().(string)
788
+
789
+ if v != s.expected {
790
+ t.Fatalf("[%s] Expected \n%s, \ngot \n%s", s.js, s.expected, v)
791
+ }
792
+ }
793
+ }
794
+
795
+ func TestBindMailsCount(t *testing.T) {
796
+ vm := goja.New()
797
+ BindMails(vm)
798
+
799
+ testBindsCount(vm, "$mails", 5, t)
800
+ }
801
+
802
+ func TestBindMails(t *testing.T) {
803
+ app, _ := tests.NewTestApp()
804
+ defer app.Cleanup()
805
+
806
+ record, err := app.FindAuthRecordByEmail("users", "test@example.com")
807
+ if err != nil {
808
+ t.Fatal(err)
809
+ }
810
+
811
+ vm := goja.New()
812
+ BindCore(vm)
813
+ BindMails(vm)
814
+ vm.Set("$app", app)
815
+ vm.Set("record", record)
816
+
817
+ _, vmErr := vm.RunString(`
818
+ $mails.sendRecordPasswordReset($app, record);
819
+ if (!$app.testMailer.lastMessage().html.includes("/_/#/auth/confirm-password-reset/")) {
820
+ throw new Error("Expected record password reset email, got:" + JSON.stringify($app.testMailer.lastMessage()))
821
+ }
822
+
823
+ $mails.sendRecordVerification($app, record);
824
+ if (!$app.testMailer.lastMessage().html.includes("/_/#/auth/confirm-verification/")) {
825
+ throw new Error("Expected record verification email, got:" + JSON.stringify($app.testMailer.lastMessage()))
826
+ }
827
+
828
+ $mails.sendRecordChangeEmail($app, record, "new@example.com");
829
+ if (!$app.testMailer.lastMessage().html.includes("/_/#/auth/confirm-email-change/")) {
830
+ throw new Error("Expected record email change email, got:" + JSON.stringify($app.testMailer.lastMessage()))
831
+ }
832
+
833
+ $mails.sendRecordOTP($app, record, "test_otp_id", "test_otp_pass");
834
+ if (!$app.testMailer.lastMessage().html.includes("test_otp_pass")) {
835
+ throw new Error("Expected record OTP email, got:" + JSON.stringify($app.testMailer.lastMessage()))
836
+ }
837
+
838
+ $mails.sendRecordAuthAlert($app, record, "test_alert_info");
839
+ if (!$app.testMailer.lastMessage().html.includes("test_alert_info")) {
840
+ throw new Error("Expected record OTP email, got:" + JSON.stringify($app.testMailer.lastMessage()))
841
+ }
842
+ `)
843
+ if vmErr != nil {
844
+ t.Fatal(vmErr)
845
+ }
846
+ }
847
+
848
+ func TestBindSecurityCount(t *testing.T) {
849
+ vm := goja.New()
850
+ BindSecurity(vm)
851
+
852
+ testBindsCount(vm, "$security", 16, t)
853
+ }
854
+
855
+ func TestSecurityCryptoBinds(t *testing.T) {
856
+ vm := goja.New()
857
+ BindCore(vm)
858
+ BindSecurity(vm)
859
+
860
+ sceneraios := []struct {
861
+ js string
862
+ expected string
863
+ }{
864
+ {`$security.md5("123")`, "202cb962ac59075b964b07152d234b70"},
865
+ {`$security.sha256("123")`, "a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3"},
866
+ {`$security.sha512("123")`, "3c9909afec25354d551dae21590bb26e38d53f2173b8d3dc3eee4c047e7ab1c1eb8b85103e3be7ba613b31bb5c9c36214dc9f14a42fd7a2fdb84856bca5c44c2"},
867
+ {`$security.hs256("hello", "test")`, "f151ea24bda91a18e89b8bb5793ef324b2a02133cce15a28a719acbd2e58a986"},
868
+ {`$security.hs512("hello", "test")`, "44f280e11103e295c26cd61dd1cdd8178b531b860466867c13b1c37a26b6389f8af110efbe0bb0717b9d9c87f6fe1c97b3b1690936578890e5669abf279fe7fd"},
869
+ {`$security.equal("abc", "abc")`, "true"},
870
+ {`$security.equal("abc", "abcd")`, "false"},
871
+ }
872
+
873
+ for _, s := range sceneraios {
874
+ t.Run(s.js, func(t *testing.T) {
875
+ result, err := vm.RunString(s.js)
876
+ if err != nil {
877
+ t.Fatalf("Failed to execute js script, got %v", err)
878
+ }
879
+
880
+ v := cast.ToString(result.Export())
881
+
882
+ if v != s.expected {
883
+ t.Fatalf("Expected %v \ngot \n%v", s.expected, v)
884
+ }
885
+ })
886
+ }
887
+ }
888
+
889
+ func TestSecurityRandomStringBinds(t *testing.T) {
890
+ vm := goja.New()
891
+ BindCore(vm)
892
+ BindSecurity(vm)
893
+
894
+ sceneraios := []struct {
895
+ js string
896
+ length int
897
+ }{
898
+ {`$security.randomString(6)`, 6},
899
+ {`$security.randomStringWithAlphabet(7, "abc")`, 7},
900
+ {`$security.pseudorandomString(8)`, 8},
901
+ {`$security.pseudorandomStringWithAlphabet(9, "abc")`, 9},
902
+ {`$security.randomStringByRegex("abc")`, 3},
903
+ }
904
+
905
+ for _, s := range sceneraios {
906
+ t.Run(s.js, func(t *testing.T) {
907
+ result, err := vm.RunString(s.js)
908
+ if err != nil {
909
+ t.Fatalf("Failed to execute js script, got %v", err)
910
+ }
911
+
912
+ v, _ := result.Export().(string)
913
+
914
+ if len(v) != s.length {
915
+ t.Fatalf("Expected %d length string, \ngot \n%v", s.length, v)
916
+ }
917
+ })
918
+ }
919
+ }
920
+
921
+ func TestSecurityJWTBinds(t *testing.T) {
922
+ sceneraios := []struct {
923
+ name string
924
+ js string
925
+ }{
926
+ {
927
+ "$security.parseUnverifiedJWT",
928
+ `
929
+ const result = $security.parseUnverifiedJWT("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.aXzC7q7z1lX_hxk5P0R368xEU7H1xRwnBQQcLAmG0EY")
930
+ if (result.name != "John Doe") {
931
+ throw new Error("Expected result.name 'John Doe', got " + result.name)
932
+ }
933
+ if (result.sub != "1234567890") {
934
+ throw new Error("Expected result.sub '1234567890', got " + result.sub)
935
+ }
936
+ `,
937
+ },
938
+ {
939
+ "$security.parseJWT",
940
+ `
941
+ const result = $security.parseJWT("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.aXzC7q7z1lX_hxk5P0R368xEU7H1xRwnBQQcLAmG0EY", "test")
942
+ if (result.name != "John Doe") {
943
+ throw new Error("Expected result.name 'John Doe', got " + result.name)
944
+ }
945
+ if (result.sub != "1234567890") {
946
+ throw new Error("Expected result.sub '1234567890', got " + result.sub)
947
+ }
948
+ `,
949
+ },
950
+ {
951
+ "$security.createJWT",
952
+ `
953
+ // overwrite the exp claim for static token
954
+ const result = $security.createJWT({"exp": 123}, "test", 0)
955
+
956
+ const expected = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjEyM30.7gbv7w672gApdBRASI6OniCtKwkKjhieSxsr6vxSrtw";
957
+ if (result != expected) {
958
+ throw new Error("Expected token \n" + expected + ", got \n" + result)
959
+ }
960
+ `,
961
+ },
962
+ }
963
+
964
+ for _, s := range sceneraios {
965
+ t.Run(s.name, func(t *testing.T) {
966
+ vm := goja.New()
967
+ BindCore(vm)
968
+ BindSecurity(vm)
969
+
970
+ _, err := vm.RunString(s.js)
971
+ if err != nil {
972
+ t.Fatalf("Failed to execute js script, got %v", err)
973
+ }
974
+ })
975
+ }
976
+ }
977
+
978
+ func TestSecurityEncryptAndDecryptBinds(t *testing.T) {
979
+ vm := goja.New()
980
+ BindCore(vm)
981
+ BindSecurity(vm)
982
+
983
+ _, err := vm.RunString(`
984
+ const key = "abcdabcdabcdabcdabcdabcdabcdabcd"
985
+
986
+ const encrypted = $security.encrypt("123", key)
987
+
988
+ const decrypted = $security.decrypt(encrypted, key)
989
+
990
+ if (decrypted != "123") {
991
+ throw new Error("Expected decrypted '123', got " + decrypted)
992
+ }
993
+ `)
994
+ if err != nil {
995
+ t.Fatal(err)
996
+ }
997
+ }
998
+
999
+ func TestBindFilesystem(t *testing.T) {
1000
+ app, _ := tests.NewTestApp()
1001
+ defer app.Cleanup()
1002
+
1003
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1004
+ if r.URL.Path == "/error" {
1005
+ w.WriteHeader(http.StatusInternalServerError)
1006
+ }
1007
+
1008
+ fmt.Fprintf(w, "test")
1009
+ }))
1010
+ defer srv.Close()
1011
+
1012
+ tmpDir, err := os.MkdirTemp("", "jsvm")
1013
+ if err != nil {
1014
+ t.Fatal(err)
1015
+ }
1016
+ defer os.RemoveAll(tmpDir)
1017
+
1018
+ vm := goja.New()
1019
+ vm.Set("mh", &multipart.FileHeader{Filename: "test"})
1020
+ vm.Set("tmpDir", tmpDir)
1021
+ vm.Set("testFile", filepath.Join(app.DataDir(), "data.db"))
1022
+ vm.Set("baseURL", srv.URL)
1023
+ BindCore(vm)
1024
+ BindFilesystem(vm)
1025
+
1026
+ testBindsCount(vm, "$filesystem", 6, t)
1027
+
1028
+ // s3
1029
+ {
1030
+ v, err := vm.RunString(`$filesystem.s3("bucketName", "region", "endpoint", "accessKey", "secretKey", true)`)
1031
+ if err != nil {
1032
+ t.Fatal(err)
1033
+ }
1034
+
1035
+ fsys, ok := v.Export().(*filesystem.System)
1036
+ if !ok {
1037
+ t.Fatalf("[s3] Expected System instance got %v", fsys)
1038
+ }
1039
+ }
1040
+
1041
+ // local
1042
+ {
1043
+ v, err := vm.RunString(`$filesystem.local(tmpDir)`)
1044
+ if err != nil {
1045
+ t.Fatal(err)
1046
+ }
1047
+
1048
+ fsys, ok := v.Export().(*filesystem.System)
1049
+ if !ok {
1050
+ t.Fatalf("[s3] Expected System instance got %v", fsys)
1051
+ }
1052
+ }
1053
+
1054
+ // fileFromPath
1055
+ {
1056
+ v, err := vm.RunString(`$filesystem.fileFromPath(testFile)`)
1057
+ if err != nil {
1058
+ t.Fatal(err)
1059
+ }
1060
+
1061
+ file, _ := v.Export().(*filesystem.File)
1062
+
1063
+ if file == nil || file.OriginalName != "data.db" {
1064
+ t.Fatalf("[fileFromPath] Expected file with name %q, got %v", file.OriginalName, file)
1065
+ }
1066
+ }
1067
+
1068
+ // fileFromBytes
1069
+ {
1070
+ v, err := vm.RunString(`$filesystem.fileFromBytes([1, 2, 3], "test")`)
1071
+ if err != nil {
1072
+ t.Fatal(err)
1073
+ }
1074
+
1075
+ file, _ := v.Export().(*filesystem.File)
1076
+
1077
+ if file == nil || file.OriginalName != "test" {
1078
+ t.Fatalf("[fileFromBytes] Expected file with name %q, got %v", file.OriginalName, file)
1079
+ }
1080
+ }
1081
+
1082
+ // fileFromMultipart
1083
+ {
1084
+ v, err := vm.RunString(`$filesystem.fileFromMultipart(mh)`)
1085
+ if err != nil {
1086
+ t.Fatal(err)
1087
+ }
1088
+
1089
+ file, _ := v.Export().(*filesystem.File)
1090
+
1091
+ if file == nil || file.OriginalName != "test" {
1092
+ t.Fatalf("[fileFromMultipart] Expected file with name %q, got %v", file.OriginalName, file)
1093
+ }
1094
+ }
1095
+
1096
+ // fileFromURL (success)
1097
+ {
1098
+ v, err := vm.RunString(`$filesystem.fileFromURL(baseURL + "/test")`)
1099
+ if err != nil {
1100
+ t.Fatal(err)
1101
+ }
1102
+
1103
+ file, _ := v.Export().(*filesystem.File)
1104
+
1105
+ if file == nil || file.OriginalName != "test" {
1106
+ t.Fatalf("[fileFromURL] Expected file with name %q, got %v", file.OriginalName, file)
1107
+ }
1108
+ }
1109
+
1110
+ // fileFromURL (failure)
1111
+ {
1112
+ _, err := vm.RunString(`$filesystem.fileFromURL(baseURL + "/error")`)
1113
+ if err == nil {
1114
+ t.Fatal("Expected url fetch error")
1115
+ }
1116
+ }
1117
+ }
1118
+
1119
+ func TestBindForms(t *testing.T) {
1120
+ vm := goja.New()
1121
+ BindForms(vm)
1122
+
1123
+ testBindsCount(vm, "this", 4, t)
1124
+ }
1125
+
1126
+ func TestBindApisCount(t *testing.T) {
1127
+ vm := goja.New()
1128
+ BindApis(vm)
1129
+
1130
+ testBindsCount(vm, "this", 8, t)
1131
+ testBindsCount(vm, "$apis", 11, t)
1132
+ }
1133
+
1134
+ func TestBindApisErrors(t *testing.T) {
1135
+ vm := goja.New()
1136
+ BindApis(vm)
1137
+
1138
+ scenarios := []struct {
1139
+ js string
1140
+ expectStatus int
1141
+ expectMessage string
1142
+ expectData string
1143
+ }{
1144
+ {"new ApiError()", 0, "", "null"},
1145
+ {"new ApiError(100, 'test', {'test': 1})", 100, "Test.", `{"test":1}`},
1146
+ {"new NotFoundError()", 404, "The requested resource wasn't found.", "null"},
1147
+ {"new NotFoundError('test', {'test': 1})", 404, "Test.", `{"test":1}`},
1148
+ {"new BadRequestError()", 400, "Something went wrong while processing your request.", "null"},
1149
+ {"new BadRequestError('test', {'test': 1})", 400, "Test.", `{"test":1}`},
1150
+ {"new ForbiddenError()", 403, "You are not allowed to perform this request.", "null"},
1151
+ {"new ForbiddenError('test', {'test': 1})", 403, "Test.", `{"test":1}`},
1152
+ {"new UnauthorizedError()", 401, "Missing or invalid authentication.", "null"},
1153
+ {"new UnauthorizedError('test', {'test': 1})", 401, "Test.", `{"test":1}`},
1154
+ {"new TooManyRequestsError()", 429, "Too Many Requests.", "null"},
1155
+ {"new TooManyRequestsError('test', {'test': 1})", 429, "Test.", `{"test":1}`},
1156
+ {"new InternalServerError()", 500, "Something went wrong while processing your request.", "null"},
1157
+ {"new InternalServerError('test', {'test': 1})", 500, "Test.", `{"test":1}`},
1158
+ }
1159
+
1160
+ for _, s := range scenarios {
1161
+ v, err := vm.RunString(s.js)
1162
+ if err != nil {
1163
+ t.Errorf("[%s] %v", s.js, err)
1164
+ continue
1165
+ }
1166
+
1167
+ apiErr, ok := v.Export().(*router.ApiError)
1168
+ if !ok {
1169
+ t.Errorf("[%s] Expected ApiError, got %v", s.js, v)
1170
+ continue
1171
+ }
1172
+
1173
+ if apiErr.Status != s.expectStatus {
1174
+ t.Errorf("[%s] Expected Status %d, got %d", s.js, s.expectStatus, apiErr.Status)
1175
+ }
1176
+
1177
+ if apiErr.Message != s.expectMessage {
1178
+ t.Errorf("[%s] Expected Message %q, got %q", s.js, s.expectMessage, apiErr.Message)
1179
+ }
1180
+
1181
+ dataRaw, _ := json.Marshal(apiErr.RawData(), json.Deterministic(true))
1182
+ if string(dataRaw) != s.expectData {
1183
+ t.Errorf("[%s] Expected Data %q, got %q", s.js, s.expectData, dataRaw)
1184
+ }
1185
+ }
1186
+ }
1187
+
1188
+ func TestLoadingDynamicModel(t *testing.T) {
1189
+ app, _ := tests.NewTestApp()
1190
+ defer app.Cleanup()
1191
+
1192
+ vm := goja.New()
1193
+ BindCore(vm)
1194
+ BindDbx(vm)
1195
+ vm.Set("$app", app)
1196
+
1197
+ _, err := vm.RunString(`
1198
+ let result = new DynamicModel({
1199
+ string: "",
1200
+ nullString: nullString(),
1201
+ nullStringEmpty: nullString(),
1202
+
1203
+ bool: false,
1204
+ nullBool: nullBool(),
1205
+ nullBoolEmpty: nullBool(),
1206
+
1207
+ int: 0,
1208
+ nullInt: nullInt(),
1209
+ nullIntEmpty: nullInt(),
1210
+
1211
+ float: -0,
1212
+ nullFloat: nullFloat(),
1213
+ nullFloatEmpty: nullFloat(),
1214
+
1215
+ array: [],
1216
+ nullArray: nullArray(),
1217
+ nullArrayEmpty: nullArray(),
1218
+
1219
+ object: {},
1220
+ nullObject: nullObject(),
1221
+ nullObjectEmpty: nullObject(),
1222
+ })
1223
+
1224
+ const expectations = {
1225
+ "string": "a",
1226
+ "nullString": "b",
1227
+ "nullStringEmpty": null,
1228
+
1229
+ "bool": false,
1230
+ "nullBool": true,
1231
+ "nullBoolEmpty": null,
1232
+
1233
+ "int": 1,
1234
+ "nullInt": 2,
1235
+ "nullIntEmpty": null,
1236
+
1237
+ "float": 1.1,
1238
+ "nullFloat": 1.2,
1239
+ "nullFloatEmpty": null,
1240
+
1241
+ "array": [1,2],
1242
+ "nullArray": [3,4],
1243
+ "nullArrayEmpty": null,
1244
+
1245
+ "object": {a:1},
1246
+ "nullObject": {a:2},
1247
+ "nullObjectEmpty": null,
1248
+ };
1249
+
1250
+ // construct dummy SELECT column value literals based on the expectations
1251
+ const selectColumns = [];
1252
+ for (const col in expectations) {
1253
+ const val = expectations[col]
1254
+
1255
+ if (val === null) {
1256
+ selectColumns.push("null as [[" + col + "]]")
1257
+ } else if (typeof val === "string") {
1258
+ selectColumns.push("'" + val + "' as [[" + col + "]]")
1259
+ } else if (typeof val === "object") {
1260
+ selectColumns.push("'" + JSON.stringify(val) + "' as [[" + col + "]]")
1261
+ } else {
1262
+ selectColumns.push(val + " as [[" + col + "]]")
1263
+ }
1264
+ }
1265
+
1266
+ $app.db()
1267
+ .newQuery("SELECT " + selectColumns.join(", "))
1268
+ .one(result)
1269
+
1270
+ for (const col in expectations) {
1271
+ let expVal = expectations[col];
1272
+ let resVal = result[col];
1273
+
1274
+ if (expVal !== null && typeof expVal === "object") {
1275
+ expVal = JSON.stringify(expVal)
1276
+ resVal = JSON.stringify(resVal)
1277
+ }
1278
+
1279
+ if (expVal != resVal) {
1280
+ throw new Error("Expected '" + col + "' value " + expVal + ", got " + resVal);
1281
+ }
1282
+ }
1283
+ `)
1284
+ if err != nil {
1285
+ t.Fatal(err)
1286
+ }
1287
+ }
1288
+
1289
+ func TestDynamicModelMapFieldCaching(t *testing.T) {
1290
+ app, _ := tests.NewTestApp()
1291
+ defer app.Cleanup()
1292
+
1293
+ vm := goja.New()
1294
+ BindCore(vm)
1295
+ BindDbx(vm)
1296
+ vm.Set("$app", app)
1297
+
1298
+ _, err := vm.RunString(`
1299
+ let m1 = new DynamicModel({
1300
+ int: 0,
1301
+ float: -0,
1302
+ text: "",
1303
+ bool: false,
1304
+ obj: {},
1305
+ arr: [],
1306
+ })
1307
+
1308
+ let m2 = new DynamicModel({
1309
+ int: 0,
1310
+ float: -0,
1311
+ text: "",
1312
+ bool: false,
1313
+ obj: {},
1314
+ arr: [],
1315
+ })
1316
+
1317
+ m1.int = 1
1318
+ m1.float = 1.5
1319
+ m1.text = "a"
1320
+ m1.bool = true
1321
+ m1.obj.set("a", 1)
1322
+ m1.arr.push(1)
1323
+
1324
+ m2.int = 2
1325
+ m2.float = 2.5
1326
+ m2.text = "b"
1327
+ m2.bool = false
1328
+ m2.obj.set("b", 1)
1329
+ m2.arr.push(2)
1330
+
1331
+ let m1Expected = '{"arr":[1],"bool":true,"float":1.5,"int":1,"obj":{"a":1},"text":"a"}';
1332
+ let m1Serialized = JSON.stringify(m1);
1333
+ if (m1Serialized != m1Expected) {
1334
+ throw new Error("Expected m1 \n" + m1Expected + "\ngot\n" + m1Serialized);
1335
+ }
1336
+
1337
+ let m2Expected = '{"arr":[2],"bool":false,"float":2.5,"int":2,"obj":{"b":1},"text":"b"}';
1338
+ let m2Serialized = JSON.stringify(m2);
1339
+ if (m2Serialized != m2Expected) {
1340
+ throw new Error("Expected m2 \n" + m2Expected + "\ngot\n" + m2Serialized);
1341
+ }
1342
+ `)
1343
+ if err != nil {
1344
+ t.Fatal(err)
1345
+ }
1346
+ }
1347
+
1348
+ func TestLoadingArrayOf(t *testing.T) {
1349
+ app, _ := tests.NewTestApp()
1350
+ defer app.Cleanup()
1351
+
1352
+ vm := goja.New()
1353
+ BindCore(vm)
1354
+ BindDbx(vm)
1355
+ vm.Set("$app", app)
1356
+
1357
+ _, err := vm.RunString(`
1358
+ let result = arrayOf(new DynamicModel({
1359
+ id: "",
1360
+ text: "",
1361
+ }))
1362
+
1363
+ $app.db()
1364
+ .select("id", "text")
1365
+ .from("demo1")
1366
+ .where($dbx.exp("id='84nmscqy84lsi1t' OR id='al1h9ijdeojtsjy'"))
1367
+ .limit(2)
1368
+ .orderBy("text ASC")
1369
+ .all(result)
1370
+
1371
+ if (result.length != 2) {
1372
+ throw new Error('Expected 2 list items, got ' + result.length);
1373
+ }
1374
+
1375
+ if (result[0].id != "84nmscqy84lsi1t") {
1376
+ throw new Error('Expected 0.id "84nmscqy84lsi1t", got ' + result[0].id);
1377
+ }
1378
+ if (result[0].text != "test") {
1379
+ throw new Error('Expected 0.text "test", got ' + result[0].text);
1380
+ }
1381
+
1382
+ if (result[1].id != "al1h9ijdeojtsjy") {
1383
+ throw new Error('Expected 1.id "al1h9ijdeojtsjy", got ' + result[1].id);
1384
+ }
1385
+ if (result[1].text != "test2") {
1386
+ throw new Error('Expected 1.text "test2", got ' + result[1].text);
1387
+ }
1388
+ `)
1389
+ if err != nil {
1390
+ t.Fatal(err)
1391
+ }
1392
+ }
1393
+
1394
+ func TestBindHTTPCount(t *testing.T) {
1395
+ app, _ := tests.NewTestApp()
1396
+ defer app.Cleanup()
1397
+
1398
+ vm := goja.New()
1399
+ BindHTTP(vm)
1400
+
1401
+ testBindsCount(vm, "this", 2, t) // + FormData
1402
+ testBindsCount(vm, "$http", 1, t)
1403
+ }
1404
+
1405
+ func TestBindHTTPSend(t *testing.T) {
1406
+ t.Parallel()
1407
+
1408
+ // start a test server
1409
+ server := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
1410
+ if req.URL.Query().Get("testError") != "" {
1411
+ res.WriteHeader(400)
1412
+ return
1413
+ }
1414
+
1415
+ timeoutStr := req.URL.Query().Get("testTimeout")
1416
+ timeout, _ := strconv.Atoi(timeoutStr)
1417
+ if timeout > 0 {
1418
+ time.Sleep(time.Duration(timeout) * time.Second)
1419
+ }
1420
+
1421
+ bodyRaw, _ := io.ReadAll(req.Body)
1422
+ defer req.Body.Close()
1423
+
1424
+ // normalize headers
1425
+ headers := make(map[string]string, len(req.Header))
1426
+ for k, v := range req.Header {
1427
+ if len(v) > 0 {
1428
+ headers[strings.ToLower(strings.ReplaceAll(k, "-", "_"))] = v[0]
1429
+ }
1430
+ }
1431
+
1432
+ info := map[string]any{
1433
+ "method": req.Method,
1434
+ "headers": headers,
1435
+ "body": string(bodyRaw),
1436
+ }
1437
+
1438
+ // add custom headers and cookies
1439
+ res.Header().Add("X-Custom", "custom_header")
1440
+ res.Header().Add("Set-Cookie", "sessionId=123456")
1441
+
1442
+ infoRaw, _ := json.Marshal(info, json.Deterministic(true))
1443
+
1444
+ // write back the submitted request
1445
+ res.Write(infoRaw)
1446
+ }))
1447
+ defer server.Close()
1448
+
1449
+ vm := goja.New()
1450
+ BindCore(vm)
1451
+ BindHTTP(vm)
1452
+ vm.Set("testURL", server.URL)
1453
+
1454
+ _, err := vm.RunString(`
1455
+ function getNestedVal(data, path) {
1456
+ let result = data || {};
1457
+ let parts = path.split(".");
1458
+
1459
+ for (const part of parts) {
1460
+ if (
1461
+ result == null ||
1462
+ typeof result !== "object" ||
1463
+ typeof result[part] === "undefined"
1464
+ ) {
1465
+ return null;
1466
+ }
1467
+
1468
+ result = result[part];
1469
+ }
1470
+
1471
+ return result;
1472
+ }
1473
+
1474
+ let testTimeout;
1475
+ try {
1476
+ $http.send({
1477
+ url: testURL + "?testTimeout=3",
1478
+ timeout: 1
1479
+ })
1480
+ } catch (err) {
1481
+ testTimeout = err
1482
+ }
1483
+ if (!testTimeout) {
1484
+ throw new Error("Expected timeout error")
1485
+ }
1486
+
1487
+ // error response check
1488
+ const test0 = $http.send({
1489
+ url: testURL + "?testError=1",
1490
+ })
1491
+
1492
+ // basic fields check
1493
+ const test1 = $http.send({
1494
+ method: "post",
1495
+ url: testURL,
1496
+ headers: {"header1": "123", "header2": "456"},
1497
+ body: '789',
1498
+ })
1499
+
1500
+ // with custom content-type header
1501
+ const test2 = $http.send({
1502
+ url: testURL,
1503
+ headers: {"content-type": "text/plain"},
1504
+ })
1505
+
1506
+ // with FormData
1507
+ const formData = new FormData()
1508
+ formData.append("title", "123")
1509
+ const test3 = $http.send({
1510
+ url: testURL,
1511
+ body: formData,
1512
+ headers: {"content-type": "text/plain"}, // should be ignored
1513
+ })
1514
+
1515
+ // raw body response field check
1516
+ const test4 = $http.send({
1517
+ method: "post",
1518
+ url: testURL,
1519
+ body: 'test',
1520
+ })
1521
+
1522
+ const scenarios = [
1523
+ [test0, {
1524
+ "statusCode": "400",
1525
+ }],
1526
+ [test1, {
1527
+ "statusCode": "200",
1528
+ "headers.X-Custom.0": "custom_header",
1529
+ "cookies.sessionId.value": "123456",
1530
+ "json.method": "POST",
1531
+ "json.headers.header1": "123",
1532
+ "json.headers.header2": "456",
1533
+ "json.body": "789",
1534
+ }],
1535
+ [test2, {
1536
+ "statusCode": "200",
1537
+ "headers.X-Custom.0": "custom_header",
1538
+ "cookies.sessionId.value": "123456",
1539
+ "json.method": "GET",
1540
+ "json.headers.content_type": "text/plain",
1541
+ }],
1542
+ [test3, {
1543
+ "statusCode": "200",
1544
+ "headers.X-Custom.0": "custom_header",
1545
+ "cookies.sessionId.value": "123456",
1546
+ "json.method": "GET",
1547
+ "json.body": [
1548
+ "\r\nContent-Disposition: form-data; name=\"title\"\r\n\r\n123\r\n--",
1549
+ ],
1550
+ "json.headers.content_type": [
1551
+ "multipart/form-data; boundary="
1552
+ ],
1553
+ }],
1554
+ [test4, {
1555
+ "statusCode": "200",
1556
+ "headers.X-Custom.0": "custom_header",
1557
+ "cookies.sessionId.value": "123456",
1558
+ // {"body":"test","headers":{"accept_encoding":"gzip","content_length":"4","user_agent":"Go-http-client/1.1"},"method":"POST"}
1559
+ "body": [123,34,98,111,100,121,34,58,34,116,101,115,116,34,44,34,104,101,97,100,101,114,115,34,58,123,34,97,99,99,101,112,116,95,101,110,99,111,100,105,110,103,34,58,34,103,122,105,112,34,44,34,99,111,110,116,101,110,116,95,108,101,110,103,116,104,34,58,34,52,34,44,34,117,115,101,114,95,97,103,101,110,116,34,58,34,71,111,45,104,116,116,112,45,99,108,105,101,110,116,47,49,46,49,34,125,44,34,109,101,116,104,111,100,34,58,34,80,79,83,84,34,125],
1560
+ }],
1561
+ ]
1562
+
1563
+ for (let scenario of scenarios) {
1564
+ const result = scenario[0];
1565
+ const expectations = scenario[1];
1566
+
1567
+ for (let key in expectations) {
1568
+ const value = getNestedVal(result, key);
1569
+ const expectation = expectations[key]
1570
+ if (Array.isArray(expectation)) {
1571
+ // check for partial match(es)
1572
+ for (let exp of expectation) {
1573
+ if (!value.includes(exp)) {
1574
+ throw new Error('Expected ' + key + ' to contain ' + exp + ', got: ' + toString(result.body));
1575
+ }
1576
+ }
1577
+ } else {
1578
+ // check for direct match
1579
+ if (value != expectation) {
1580
+ throw new Error('Expected ' + key + ' ' + expectation + ', got: ' + toString(result.body));
1581
+ }
1582
+ }
1583
+ }
1584
+ }
1585
+ `)
1586
+ if err != nil {
1587
+ t.Fatal(err)
1588
+ }
1589
+ }
1590
+
1591
+ func TestCronBindsCount(t *testing.T) {
1592
+ app, _ := tests.NewTestApp()
1593
+ defer app.Cleanup()
1594
+
1595
+ vm := goja.New()
1596
+
1597
+ pool := newPool(1, func() *goja.Runtime { return goja.New() })
1598
+
1599
+ cronBinds(app, vm, pool)
1600
+
1601
+ testBindsCount(vm, "this", 2, t)
1602
+
1603
+ pool.run(func(poolVM *goja.Runtime) error {
1604
+ testBindsCount(poolVM, "this", 2, t)
1605
+ return nil
1606
+ })
1607
+ }
1608
+
1609
+ func TestHooksBindsCount(t *testing.T) {
1610
+ app, _ := tests.NewTestApp()
1611
+ defer app.Cleanup()
1612
+
1613
+ vm := goja.New()
1614
+ hooksBinds(app, vm, nil)
1615
+
1616
+ testBindsCount(vm, "this", 82, t)
1617
+ }
1618
+
1619
+ func TestHooksBinds(t *testing.T) {
1620
+ app, _ := tests.NewTestApp()
1621
+ defer app.Cleanup()
1622
+
1623
+ result := &struct {
1624
+ Called int
1625
+ }{}
1626
+
1627
+ vmFactory := func() *goja.Runtime {
1628
+ vm := goja.New()
1629
+ BindCore(vm)
1630
+ vm.Set("$app", app)
1631
+ vm.Set("result", result)
1632
+ return vm
1633
+ }
1634
+
1635
+ pool := newPool(1, vmFactory)
1636
+
1637
+ vm := vmFactory()
1638
+ hooksBinds(app, vm, pool)
1639
+
1640
+ _, err := vm.RunString(`
1641
+ onModelUpdate((e) => {
1642
+ result.called++;
1643
+ e.next()
1644
+ }, "demo1")
1645
+
1646
+ onModelUpdate((e) => {
1647
+ throw new Error("example");
1648
+ }, "demo1")
1649
+
1650
+ onModelUpdate((e) => {
1651
+ result.called++;
1652
+ e.next();
1653
+ }, "demo2")
1654
+
1655
+ onModelUpdate((e) => {
1656
+ result.called++;
1657
+ e.next()
1658
+ }, "demo2")
1659
+
1660
+ onModelUpdate((e) => {
1661
+ // stop propagation
1662
+ }, "demo2")
1663
+
1664
+ onModelUpdate((e) => {
1665
+ result.called++;
1666
+ e.next();
1667
+ }, "demo2")
1668
+
1669
+ onBootstrap((e) => {
1670
+ e.next()
1671
+
1672
+ // check hooks propagation and tags filtering
1673
+ const recordA = $app.findFirstRecordByFilter("demo2", "1=1")
1674
+ recordA.set("title", "update")
1675
+ $app.save(recordA)
1676
+ if (result.called != 2) {
1677
+ throw new Error("Expected result.called to be 2, got " + result.called)
1678
+ }
1679
+
1680
+ // reset
1681
+ result.called = 0;
1682
+
1683
+ // check error handling
1684
+ let hasErr = false
1685
+ try {
1686
+ const recordB = $app.findFirstRecordByFilter("demo1", "1=1")
1687
+ recordB.set("text", "update")
1688
+ $app.save(recordB)
1689
+ } catch (err) {
1690
+ hasErr = true
1691
+ }
1692
+ if (!hasErr) {
1693
+ throw new Error("Expected an error to be thrown")
1694
+ }
1695
+ if (result.called != 1) {
1696
+ throw new Error("Expected result.called to be 1, got " + result.called)
1697
+ }
1698
+ })
1699
+
1700
+ $app.bootstrap();
1701
+ `)
1702
+ if err != nil {
1703
+ t.Fatal(err)
1704
+ }
1705
+ }
1706
+
1707
+ func TestHooksExceptionUnwrapping(t *testing.T) {
1708
+ app, _ := tests.NewTestApp()
1709
+ defer app.Cleanup()
1710
+
1711
+ goErr := errors.New("test")
1712
+
1713
+ vmFactory := func() *goja.Runtime {
1714
+ vm := goja.New()
1715
+ BindCore(vm)
1716
+ vm.Set("$app", app)
1717
+ vm.Set("goErr", goErr)
1718
+ return vm
1719
+ }
1720
+
1721
+ pool := newPool(1, vmFactory)
1722
+
1723
+ vm := vmFactory()
1724
+ hooksBinds(app, vm, pool)
1725
+
1726
+ _, err := vm.RunString(`
1727
+ onModelUpdate((e) => {
1728
+ throw goErr
1729
+ }, "demo1")
1730
+ `)
1731
+ if err != nil {
1732
+ t.Fatal(err)
1733
+ }
1734
+
1735
+ record, err := app.FindFirstRecordByFilter("demo1", "1=1")
1736
+ if err != nil {
1737
+ t.Fatal(err)
1738
+ }
1739
+
1740
+ record.Set("text", "update")
1741
+
1742
+ err = app.Save(record)
1743
+ if !errors.Is(err, goErr) {
1744
+ t.Fatalf("Expected goError, got %v", err)
1745
+ }
1746
+ }
1747
+
1748
+ func TestRouterBindsCount(t *testing.T) {
1749
+ app, _ := tests.NewTestApp()
1750
+ defer app.Cleanup()
1751
+
1752
+ vm := goja.New()
1753
+ routerBinds(app, vm, nil)
1754
+
1755
+ testBindsCount(vm, "this", 2, t)
1756
+ }
1757
+
1758
+ func TestRouterBinds(t *testing.T) {
1759
+ app, _ := tests.NewTestApp()
1760
+ defer app.Cleanup()
1761
+
1762
+ result := &struct {
1763
+ RouteMiddlewareCalls int
1764
+ GlobalMiddlewareCalls int
1765
+ }{}
1766
+
1767
+ vmFactory := func() *goja.Runtime {
1768
+ vm := goja.New()
1769
+ BindCore(vm)
1770
+ BindApis(vm)
1771
+ vm.Set("$app", app)
1772
+ vm.Set("result", result)
1773
+ return vm
1774
+ }
1775
+
1776
+ pool := newPool(1, vmFactory)
1777
+
1778
+ vm := vmFactory()
1779
+ routerBinds(app, vm, pool)
1780
+
1781
+ _, err := vm.RunString(`
1782
+ routerAdd("GET", "/test", (e) => {
1783
+ result.routeMiddlewareCalls++;
1784
+ }, (e) => {
1785
+ result.routeMiddlewareCalls++;
1786
+ return e.next();
1787
+ })
1788
+
1789
+ // Promise is not technically supported as return result
1790
+ // but we try to resolve it at least for thrown errors
1791
+ routerAdd("GET", "/error", async (e) => {
1792
+ throw new ApiError(456, 'test', null)
1793
+ })
1794
+
1795
+ routerUse((e) => {
1796
+ result.globalMiddlewareCalls++;
1797
+
1798
+ return e.next();
1799
+ })
1800
+ `)
1801
+ if err != nil {
1802
+ t.Fatal(err)
1803
+ }
1804
+
1805
+ pbRouter, err := apis.NewRouter(app)
1806
+ if err != nil {
1807
+ t.Fatal(err)
1808
+ }
1809
+
1810
+ serveEvent := new(core.ServeEvent)
1811
+ serveEvent.App = app
1812
+ serveEvent.Router = pbRouter
1813
+ if err = app.OnServe().Trigger(serveEvent); err != nil {
1814
+ t.Fatal(err)
1815
+ }
1816
+
1817
+ mux, err := serveEvent.Router.BuildMux()
1818
+ if err != nil {
1819
+ t.Fatalf("Failed to build router mux: %v", err)
1820
+ }
1821
+
1822
+ scenarios := []struct {
1823
+ method string
1824
+ path string
1825
+ expectedRouteMiddlewareCalls int
1826
+ expectedGlobalMiddlewareCalls int
1827
+ expectedCode int
1828
+ }{
1829
+ {"GET", "/test", 2, 1, 200},
1830
+ {"GET", "/error", 0, 1, 456},
1831
+ }
1832
+
1833
+ for _, s := range scenarios {
1834
+ t.Run(s.method+" "+s.path, func(t *testing.T) {
1835
+ // reset
1836
+ result.RouteMiddlewareCalls = 0
1837
+ result.GlobalMiddlewareCalls = 0
1838
+
1839
+ rec := httptest.NewRecorder()
1840
+ req := httptest.NewRequest(s.method, s.path, nil)
1841
+ mux.ServeHTTP(rec, req)
1842
+
1843
+ if result.RouteMiddlewareCalls != s.expectedRouteMiddlewareCalls {
1844
+ t.Fatalf("Expected RouteMiddlewareCalls %d, got %d", s.expectedRouteMiddlewareCalls, result.RouteMiddlewareCalls)
1845
+ }
1846
+
1847
+ if result.GlobalMiddlewareCalls != s.expectedGlobalMiddlewareCalls {
1848
+ t.Fatalf("Expected GlobalMiddlewareCalls %d, got %d", s.expectedGlobalMiddlewareCalls, result.GlobalMiddlewareCalls)
1849
+ }
1850
+
1851
+ if rec.Code != s.expectedCode {
1852
+ t.Fatalf("Expected status code %d, got %d", s.expectedCode, rec.Code)
1853
+ }
1854
+ })
1855
+ }
1856
+ }
1857
+
1858
+ func TestBindFilepathCount(t *testing.T) {
1859
+ vm := goja.New()
1860
+ BindFilepath(vm)
1861
+
1862
+ testBindsCount(vm, "$filepath", 15, t)
1863
+ }
1864
+
1865
+ func TestBindOSCount(t *testing.T) {
1866
+ vm := goja.New()
1867
+ BindOS(vm)
1868
+
1869
+ testBindsCount(vm, "$os", 20, t)
1870
+ }