package jscriptx_test import ( "context" "errors" "strings" "testing" "git.fsdpf.net/go/jscriptx" ) func TestNew_全局名校验(t *testing.T) { for _, name := range []string{"db.From", "2fast", "有中文", "", "a-b"} { if _, err := jscriptx.New(jscriptx.WithGlobal(name, 1)); !errors.Is(err, jscriptx.ErrBadGlobal) { t.Errorf("全局名 %q 应该被拒绝,got %v", name, err) } } for _, name := range []string{"db", "req", "_x", "$", "a1"} { if _, err := jscriptx.New(jscriptx.WithGlobal(name, 1)); err != nil { t.Errorf("全局名 %q 应该被接受: %v", name, err) } } } // 白名单之外的东西脚本一律看不见。 func TestGlobals_白名单之外看不见(t *testing.T) { e := newEngine(t, jscriptx.WithGlobals(map[string]any{ "allowed": func() string { return "ok" }, })) s := mustCompile(t, e, "wl", ` export function probe(name) { return typeof globalThis[name] } `) for _, tc := range []struct{ name, want string }{ {"allowed", "function"}, {"require", "undefined"}, {"process", "undefined"}, {"setTimeout", "undefined"}, {"db", "undefined"}, } { got, err := s.Call(context.Background(), "probe", tc.name) if err != nil { t.Fatal(err) } if got != tc.want { t.Errorf("typeof %s = %v, want %v", tc.name, got, tc.want) } } } // 注入的命名空间对象必须是只读的:否则同一个 Go map 被所有 VM 共享, // 脚本改一下既污染别的 VM 又是数据竞争。 func TestGlobals_命名空间只读(t *testing.T) { ns := map[string]any{ "helper": map[string]any{"double": func(n int) int { return n * 2 }}, } e := newEngine(t, jscriptx.WithGlobals(ns)) s := mustCompile(t, e, "readonly", ` export function tamper() { try { helper.double = null } catch (err) { /* 严格模式才会抛,这里吞掉 */ } try { helper = null } catch (err) {} return typeof helper.double } `) for i := 0; i < 3; i++ { got, err := s.Call(context.Background(), "tamper") if err != nil { t.Fatal(err) } if got != "function" { t.Fatalf("第 %d 次: 白名单被脚本改掉了: %v", i, got) } } // Go 侧的原始 map 也不能被动过 if _, ok := ns["helper"].(map[string]any)["double"].(func(int) int); !ok { t.Error("Go 侧的白名单 map 被脚本改掉了") } } // 多返回值约定:这是脚本作者唯一需要额外理解的规则,用测试把它钉死。 func TestMultiReturn_转换约定(t *testing.T) { e := newEngine(t, jscriptx.WithGlobals(map[string]any{ "one": func() string { return "hello" }, "okPair": func() (string, error) { return "hello", nil }, "errPair": func() (string, error) { return "", errors.New("boom") }, "twoVals": func() (string, int) { return "a", 1 }, "threeVal": func() (string, int, error) { return "a", 1, nil }, "threeErr": func() (string, int, error) { return "", 0, errors.New("boom") }, })) s := mustCompile(t, e, "multi", ` export function run() { var out = {} out.one = one() out.okPair = okPair() // error 为 nil:拿到裸值 out.twoVals = twoVals().join(",") // 无 error:拿到数组 out.threeVal = threeVal().join(",") // 末位 error 为 nil:只剩前两个 try { errPair(); out.errPair = "没抛" } catch (err) { out.errPair = "抛了: " + err.message } try { threeErr(); out.threeErr = "没抛" } catch (err) { out.threeErr = "抛了" } return out } `) got, err := s.Call(context.Background(), "run") if err != nil { t.Fatal(err) } out, _ := got.(map[string]any) want := map[string]any{ "one": "hello", "okPair": "hello", "twoVals": "a,1", "threeVal": "a,1", "threeErr": "抛了", } for k, v := range want { if out[k] != v { t.Errorf("%s = %#v, want %#v", k, out[k], v) } } if s, _ := out["errPair"].(string); !strings.HasPrefix(s, "抛了") { t.Errorf("errPair = %#v,error 非 nil 时应该变成 JS 异常", out["errPair"]) } t.Logf("多返回值转换结果: %v", out) } func TestEngine_Reload与Invalidate(t *testing.T) { loader := newTestLoader() loader.put("x", `export function hi() { return "v1" }`, "v1") e := newEngine(t, jscriptx.WithLoader(loader)) s1, _ := e.Script("x") loader.put("x", `export function hi() { return "v2" }`, "v2") // 没开自动重载,普通取还是旧的 s2, _ := e.Script("x") if s1 != s2 { t.Error("没开自动重载不该自己更新") } s3, err := e.Reload("x") if err != nil { t.Fatal(err) } if got, _ := s3.Call(context.Background(), "hi"); got != "v2" { t.Errorf("Reload 后 got %#v", got) } // 旧 Script 被顶替后会被关掉,正在进行的调用不受影响、之后的调用报 ErrClosed if _, err := s1.Call(context.Background(), "hi"); !errors.Is(err, jscriptx.ErrClosed) { t.Errorf("被顶替的旧脚本应该已关闭,got %v", err) } e.Invalidate("x") if len(e.Names()) != 0 { t.Errorf("Invalidate 之后缓存该空了: %v", e.Names()) } } func TestEngine_Close(t *testing.T) { e, err := jscriptx.New(jscriptx.WithLogger(nil)) if err != nil { t.Fatal(err) } s := mustCompile(t, e, "x", `export function f() { return 1 }`) e.Close() if _, err := e.Compile("y", `export function g() {}`); !errors.Is(err, jscriptx.ErrClosed) { t.Errorf("want ErrClosed, got %v", err) } if _, err := s.Call(context.Background(), "f"); !errors.Is(err, jscriptx.ErrClosed) { t.Errorf("want ErrClosed, got %v", err) } } // 同名脚本重新 Compile 会顶替掉旧的。 func TestEngine_重复Compile顶替(t *testing.T) { e := newEngine(t) mustCompile(t, e, "x", `export function f() { return 1 }`) s2 := mustCompile(t, e, "x", `export function f() { return 2 }`) cached, err := e.Script("x") if err != nil { t.Fatal(err) } if cached != s2 { t.Error("缓存里应该是最后一次编译的版本") } if got, _ := cached.Call(context.Background(), "f"); got.(int64) != 2 { t.Errorf("got %#v", got) } }