restructered some of the internals and added basic js app hooks support

This commit is contained in:
Gani Georgiev
2023-06-08 17:59:08 +03:00
parent ff5508cb79
commit 3cf3e04866
24 changed files with 1218 additions and 422 deletions

View File

@@ -1,18 +1,35 @@
package jsvm_test
package jsvm
import (
"reflect"
"encoding/json"
"mime/multipart"
"path/filepath"
"testing"
"github.com/dop251/goja"
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/models/schema"
"github.com/pocketbase/pocketbase/plugins/jsvm"
"github.com/pocketbase/pocketbase/tests"
"github.com/pocketbase/pocketbase/tools/filesystem"
"github.com/pocketbase/pocketbase/tools/mailer"
"github.com/pocketbase/pocketbase/tools/security"
)
func TestBaseVMUnmarshal(t *testing.T) {
vm := jsvm.NewBaseVM()
// note: this test is useful as a reminder to update the tests in case
// a new base binding is added.
func TestBaseBindsCount(t *testing.T) {
vm := goja.New()
baseBinds(vm)
testBindsCount(vm, "this", 9, t)
}
func TestBaseBindsUnmarshal(t *testing.T) {
vm := goja.New()
baseBinds(vm)
v, err := vm.RunString(`unmarshal({ name: "test" }, new Collection())`)
if err != nil {
@@ -29,7 +46,7 @@ func TestBaseVMUnmarshal(t *testing.T) {
}
}
func TestBaseVMRecordBind(t *testing.T) {
func TestBaseBindsRecord(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
@@ -38,7 +55,8 @@ func TestBaseVMRecordBind(t *testing.T) {
t.Fatal(err)
}
vm := jsvm.NewBaseVM()
vm := goja.New()
baseBinds(vm)
vm.Set("collection", collection)
// without record data
@@ -74,75 +92,9 @@ func TestBaseVMRecordBind(t *testing.T) {
}
}
// @todo enable after https://github.com/dop251/goja/issues/426
// func TestBaseVMRecordGetAndSetBind(t *testing.T) {
// app, _ := tests.NewTestApp()
// defer app.Cleanup()
// collection, err := app.Dao().FindCollectionByNameOrId("users")
// if err != nil {
// t.Fatal(err)
// }
// vm := jsvm.NewBaseVM()
// vm.Set("collection", collection)
// vm.Set("getRecord", func() *models.Record {
// return models.NewRecord(collection)
// })
// _, runErr := vm.RunString(`
// const jsRecord = new Record(collection);
// jsRecord.email = "test@example.com"; // test js record setter
// const email = jsRecord.email; // test js record getter
// const goRecord = getRecord()
// goRecord.name = "test" // test go record setter
// const name = goRecord.name; // test go record getter
// `)
// if runErr != nil {
// t.Fatal(runErr)
// }
// expectedEmail := "test@example.com"
// expectedName := "test"
// jsRecord, ok := vm.Get("jsRecord").Export().(*models.Record)
// if !ok {
// t.Fatalf("Failed to export jsRecord")
// }
// if v := jsRecord.Email(); v != expectedEmail {
// t.Fatalf("Expected the js created record to have email %q, got %q", expectedEmail, v)
// }
// email := vm.Get("email").Export().(string)
// if email != expectedEmail {
// t.Fatalf("Expected exported email %q, got %q", expectedEmail, email)
// }
// goRecord, ok := vm.Get("goRecord").Export().(*models.Record)
// if !ok {
// t.Fatalf("Failed to export goRecord")
// }
// if v := goRecord.GetString("name"); v != expectedName {
// t.Fatalf("Expected the go created record to have name %q, got %q", expectedName, v)
// }
// name := vm.Get("name").Export().(string)
// if name != expectedName {
// t.Fatalf("Expected exported name %q, got %q", expectedName, name)
// }
// // ensure that the two record instances are not mixed
// if v := goRecord.Email(); v != "" {
// t.Fatalf("Expected the go created record to not have an email, got %q", v)
// }
// if v := jsRecord.GetString("name"); v != "" {
// t.Fatalf("Expected the js created record to not have a name, got %q", v)
// }
// }
func TestBaseVMCollectionBind(t *testing.T) {
vm := jsvm.NewBaseVM()
func TestBaseBindsCollection(t *testing.T) {
vm := goja.New()
baseBinds(vm)
v, err := vm.RunString(`new Collection({ name: "test", schema: [{name: "title", "type": "text"}] })`)
if err != nil {
@@ -164,7 +116,8 @@ func TestBaseVMCollectionBind(t *testing.T) {
}
func TestBaseVMAdminBind(t *testing.T) {
vm := jsvm.NewBaseVM()
vm := goja.New()
baseBinds(vm)
v, err := vm.RunString(`new Admin({ email: "test@example.com" })`)
if err != nil {
@@ -177,8 +130,9 @@ func TestBaseVMAdminBind(t *testing.T) {
}
}
func TestBaseVMSchemaBind(t *testing.T) {
vm := jsvm.NewBaseVM()
func TestBaseBindsSchema(t *testing.T) {
vm := goja.New()
baseBinds(vm)
v, err := vm.RunString(`new Schema([{name: "title", "type": "text"}])`)
if err != nil {
@@ -195,8 +149,9 @@ func TestBaseVMSchemaBind(t *testing.T) {
}
}
func TestBaseVMSchemaFieldBind(t *testing.T) {
vm := jsvm.NewBaseVM()
func TestBaseBindsSchemaField(t *testing.T) {
vm := goja.New()
baseBinds(vm)
v, err := vm.RunString(`new SchemaField({name: "title", "type": "text"})`)
if err != nil {
@@ -213,56 +168,461 @@ func TestBaseVMSchemaFieldBind(t *testing.T) {
}
}
func TestBaseVMDaoBind(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
func TestBaseBindsMail(t *testing.T) {
vm := goja.New()
baseBinds(vm)
vm := jsvm.NewBaseVM()
vm.Set("db", app.DB())
v, err := vm.RunString(`new Dao(db)`)
v, err := vm.RunString(`new Mail({
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",
}
})`)
if err != nil {
t.Fatal(err)
}
d, ok := v.Export().(*daos.Dao)
m, ok := v.Export().(*mailer.Message)
if !ok {
t.Fatalf("Expected daos.Dao, got %v", d)
t.Fatalf("Expected mailer.Message, got %v", m)
}
if d.DB() != app.DB() {
t.Fatalf("The db instances doesn't match")
raw, err := json.Marshal(m)
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":null}`
if string(raw) != expected {
t.Fatalf("Expected \n%s, \ngot \n%s", expected, raw)
}
}
func TestFieldMapper(t *testing.T) {
mapper := jsvm.FieldMapper{}
func TestBaseBindsValidationError(t *testing.T) {
vm := goja.New()
baseBinds(vm)
scenarios := []struct {
name string
expected string
js string
expectCode string
expectMessage string
}{
{"", ""},
{"test", "test"},
{"Test", "test"},
{"miXeD", "miXeD"},
{"MiXeD", "miXeD"},
{"ResolveRequestAsJSON", "resolveRequestAsJSON"},
{"Variable_with_underscore", "variable_with_underscore"},
{"ALLCAPS", "allcaps"},
{"NOTALLCAPs", "nOTALLCAPs"},
{"ALL_CAPS_WITH_UNDERSCORE", "all_caps_with_underscore"},
{
`new ValidationError()`,
"",
"",
},
{
`new ValidationError("test_code")`,
"test_code",
"",
},
{
`new ValidationError("test_code", "test_message")`,
"test_code",
"test_message",
},
}
for i, s := range scenarios {
field := reflect.StructField{Name: s.name}
if v := mapper.FieldName(nil, field); v != s.expected {
t.Fatalf("[%d] Expected FieldName %q, got %q", i, s.expected, v)
for _, s := range scenarios {
v, err := vm.RunString(s.js)
if err != nil {
t.Fatal(err)
}
method := reflect.Method{Name: s.name}
if v := mapper.MethodName(nil, method); v != s.expected {
t.Fatalf("[%d] Expected MethodName %q, got %q", i, s.expected, v)
m, ok := v.Export().(validation.Error)
if !ok {
t.Fatalf("[%s] Expected validation.Error, got %v", s.js, m)
}
if m.Code() != s.expectCode {
t.Fatalf("[%s] Expected code %q, got %q", s.js, s.expectCode, m.Code())
}
if m.Message() != s.expectMessage {
t.Fatalf("[%s] Expected message %q, got %q", s.js, s.expectMessage, m.Message())
}
}
}
func TestBaseBindsDao(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
vm := goja.New()
baseBinds(vm)
vm.Set("db", app.Dao().ConcurrentDB())
vm.Set("db2", app.Dao().NonconcurrentDB())
scenarios := []struct {
js string
concurrentDB dbx.Builder
nonconcurrentDB dbx.Builder
}{
{
js: "new Dao(db)",
concurrentDB: app.Dao().ConcurrentDB(),
nonconcurrentDB: app.Dao().ConcurrentDB(),
},
{
js: "new Dao(db, db2)",
concurrentDB: app.Dao().ConcurrentDB(),
nonconcurrentDB: app.Dao().NonconcurrentDB(),
},
}
for _, s := range scenarios {
v, err := vm.RunString(s.js)
if err != nil {
t.Fatalf("[%s] Failed to execute js script, got %v", s.js, err)
}
d, ok := v.Export().(*daos.Dao)
if !ok {
t.Fatalf("[%s] Expected daos.Dao, got %v", s.js, d)
}
if d.ConcurrentDB() != s.concurrentDB {
t.Fatalf("[%s] The ConcurrentDB instances doesn't match", s.js)
}
if d.NonconcurrentDB() != s.nonconcurrentDB {
t.Fatalf("[%s] The NonconcurrentDB instances doesn't match", s.js)
}
}
}
func TestDbxBinds(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
vm := goja.New()
vm.Set("db", app.Dao().DB())
baseBinds(vm)
dbxBinds(vm)
testBindsCount(vm, "$dbx", 15, t)
sceneraios := []struct {
js string
expected string
}{
{
`$dbx.exp("a = 1").build(db, {})`,
"a = 1",
},
{
`$dbx.hashExp({
"a": 1,
b: null,
c: [1, 2, 3],
}).build(db, {})`,
"`a`={:p0} AND `b` IS NULL AND `c` IN ({:p1}, {:p2}, {:p3})",
},
{
`$dbx.not($dbx.exp("a = 1")).build(db, {})`,
"NOT (a = 1)",
},
{
`$dbx.and($dbx.exp("a = 1"), $dbx.exp("b = 2")).build(db, {})`,
"(a = 1) AND (b = 2)",
},
{
`$dbx.or($dbx.exp("a = 1"), $dbx.exp("b = 2")).build(db, {})`,
"(a = 1) OR (b = 2)",
},
{
`$dbx.in("a", 1, 2, 3).build(db, {})`,
"`a` IN ({:p0}, {:p1}, {:p2})",
},
{
`$dbx.notIn("a", 1, 2, 3).build(db, {})`,
"`a` NOT IN ({:p0}, {:p1}, {:p2})",
},
{
`$dbx.like("a", "test1", "test2").match(true, false).build(db, {})`,
"`a` LIKE {:p0} AND `a` LIKE {:p1}",
},
{
`$dbx.orLike("a", "test1", "test2").match(false, true).build(db, {})`,
"`a` LIKE {:p0} OR `a` LIKE {:p1}",
},
{
`$dbx.notLike("a", "test1", "test2").match(true, false).build(db, {})`,
"`a` NOT LIKE {:p0} AND `a` NOT LIKE {:p1}",
},
{
`$dbx.orNotLike("a", "test1", "test2").match(false, false).build(db, {})`,
"`a` NOT LIKE {:p0} OR `a` NOT LIKE {:p1}",
},
{
`$dbx.exists($dbx.exp("a = 1")).build(db, {})`,
"EXISTS (a = 1)",
},
{
`$dbx.notExists($dbx.exp("a = 1")).build(db, {})`,
"NOT EXISTS (a = 1)",
},
{
`$dbx.between("a", 1, 2).build(db, {})`,
"`a` BETWEEN {:p0} AND {:p1}",
},
{
`$dbx.notBetween("a", 1, 2).build(db, {})`,
"`a` NOT BETWEEN {:p0} AND {:p1}",
},
}
for _, s := range sceneraios {
result, err := vm.RunString(s.js)
if err != nil {
t.Fatalf("[%s] Failed to execute js script, got %v", s.js, err)
}
v, _ := result.Export().(string)
if v != s.expected {
t.Fatalf("[%s] Expected \n%s, \ngot \n%s", s.js, s.expected, v)
}
}
}
func TestTokensBinds(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
admin, err := app.Dao().FindAdminByEmail("test@example.com")
if err != nil {
t.Fatal(err)
}
record, err := app.Dao().FindAuthRecordByEmail("users", "test@example.com")
if err != nil {
t.Fatal(err)
}
vm := goja.New()
vm.Set("$app", app)
vm.Set("admin", admin)
vm.Set("record", record)
baseBinds(vm)
tokensBinds(vm)
testBindsCount(vm, "$tokens", 8, t)
sceneraios := []struct {
js string
key string
}{
{
`$tokens.adminAuthToken($app, admin)`,
admin.TokenKey + app.Settings().AdminAuthToken.Secret,
},
{
`$tokens.adminResetPasswordToken($app, admin)`,
admin.TokenKey + app.Settings().AdminPasswordResetToken.Secret,
},
{
`$tokens.adminFileToken($app, admin)`,
admin.TokenKey + app.Settings().AdminFileToken.Secret,
},
{
`$tokens.recordAuthToken($app, record)`,
record.TokenKey() + app.Settings().RecordAuthToken.Secret,
},
{
`$tokens.recordVerifyToken($app, record)`,
record.TokenKey() + app.Settings().RecordVerificationToken.Secret,
},
{
`$tokens.recordResetPasswordToken($app, record)`,
record.TokenKey() + app.Settings().RecordPasswordResetToken.Secret,
},
{
`$tokens.recordChangeEmailToken($app, record)`,
record.TokenKey() + app.Settings().RecordEmailChangeToken.Secret,
},
{
`$tokens.recordFileToken($app, record)`,
record.TokenKey() + app.Settings().RecordFileToken.Secret,
},
}
for _, s := range sceneraios {
result, err := vm.RunString(s.js)
if err != nil {
t.Fatalf("[%s] Failed to execute js script, got %v", s.js, err)
}
v, _ := result.Export().(string)
if _, err := security.ParseJWT(v, s.key); err != nil {
t.Fatalf("[%s] Failed to parse JWT %v, got %v", s.js, v, err)
}
}
}
func TestSecurityRandomStringBinds(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
vm := goja.New()
baseBinds(vm)
securityBinds(vm)
testBindsCount(vm, "$security", 7, t)
sceneraios := []struct {
js string
length int
}{
{`$security.randomString(6)`, 6},
{`$security.randomStringWithAlphabet(7, "abc")`, 7},
{`$security.pseudorandomString(8)`, 8},
{`$security.pseudorandomStringWithAlphabet(9, "abc")`, 9},
}
for _, s := range sceneraios {
result, err := vm.RunString(s.js)
if err != nil {
t.Fatalf("[%s] Failed to execute js script, got %v", s.js, err)
}
v, _ := result.Export().(string)
if len(v) != s.length {
t.Fatalf("[%s] Expected %d length string, \ngot \n%v", s.js, s.length, v)
}
}
}
func TestSecurityTokenBinds(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
vm := goja.New()
baseBinds(vm)
securityBinds(vm)
testBindsCount(vm, "$security", 7, t)
sceneraios := []struct {
js string
expected string
}{
{
`$security.parseUnverifiedToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.aXzC7q7z1lX_hxk5P0R368xEU7H1xRwnBQQcLAmG0EY")`,
`{"name":"John Doe","sub":"1234567890"}`,
},
{
`$security.parseToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.aXzC7q7z1lX_hxk5P0R368xEU7H1xRwnBQQcLAmG0EY", "test")`,
`{"name":"John Doe","sub":"1234567890"}`,
},
{
`$security.createToken({"exp": 123}, "test", 0)`, // overwrite the exp claim for static token
`"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjEyM30.7gbv7w672gApdBRASI6OniCtKwkKjhieSxsr6vxSrtw"`,
},
}
for _, s := range sceneraios {
result, err := vm.RunString(s.js)
if err != nil {
t.Fatalf("[%s] Failed to execute js script, got %v", s.js, err)
}
raw, _ := json.Marshal(result.Export())
if string(raw) != s.expected {
t.Fatalf("[%s] Expected \n%s, \ngot \n%s", s.js, s.expected, raw)
}
}
}
func TestFilesystemBinds(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
vm := goja.New()
vm.Set("mh", &multipart.FileHeader{Filename: "test"})
vm.Set("testFile", filepath.Join(app.DataDir(), "data.db"))
baseBinds(vm)
filesystemBinds(vm)
testBindsCount(vm, "$filesystem", 3, t)
// fileFromPath
{
v, err := vm.RunString(`$filesystem.fileFromPath(testFile)`)
if err != nil {
t.Fatal(err)
}
file, _ := v.Export().(*filesystem.File)
if file == nil || file.OriginalName != "data.db" {
t.Fatalf("[fileFromPath] Expected file with name %q, got %v", file.OriginalName, file)
}
}
// fileFromBytes
{
v, err := vm.RunString(`$filesystem.fileFromBytes([1, 2, 3], "test")`)
if err != nil {
t.Fatal(err)
}
file, _ := v.Export().(*filesystem.File)
if file == nil || file.OriginalName != "test" {
t.Fatalf("[fileFromBytes] Expected file with name %q, got %v", file.OriginalName, file)
}
}
// fileFromMultipart
{
v, err := vm.RunString(`$filesystem.fileFromMultipart(mh)`)
if err != nil {
t.Fatal(err)
}
file, _ := v.Export().(*filesystem.File)
if file == nil || file.OriginalName != "test" {
t.Fatalf("[fileFromMultipart] Expected file with name %q, got %v", file.OriginalName, file)
}
}
}
func TestFormsBinds(t *testing.T) {
vm := goja.New()
formsBinds(vm)
testBindsCount(vm, "this", 20, t)
}
func testBindsCount(vm *goja.Runtime, namespace string, count int, t *testing.T) {
v, err := vm.RunString(`Object.keys(` + namespace + `).length`)
if err != nil {
t.Fatal(err)
}
total, _ := v.Export().(int64)
if int(total) != count {
t.Fatalf("Expected %d %s binds, got %d", count, namespace, total)
}
}