package jsplugin import ( "bytes" "context" "encoding/json" "errors" "io" "math" "net/http" "net/url" "os" "os/exec" "runtime" "strings" "sync/atomic" "testing" "time" "unicode/utf8" "unsafe" "weak" "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" "github.com/gin-gonic/gin" jsoniter "github.com/json-iterator/go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestEngineCallsESMExportWithInjectedUtils(t *testing.T) { t.Parallel() logs := make([]string, 0, 1) engine, err := Compile(` export function sign(ctx) { console.log("called", ctx.name); return { now: utils.unixNow(), digest: utils.hmacSHA256(ctx.message, ctx.secret), encoded: utils.base64(ctx.message), }; } export const meta = { apiVersion: 1, key: "mock" }; `, Options{ Key: "mock", Version: "1.0.0", Now: func() time.Time { return time.Unix(1234, 0) }, Log: func(message string) { logs = append(logs, message) }, }) require.NoError(t, err) result, err := engine.Call(context.Background(), "sign", map[string]any{ "name": "fixture", "message": "hello", "secret": "secret", }) require.NoError(t, err) assert.Equal(t, map[string]any{ "now": int64(1234), "digest": "88aab3ede8d3adf94d26ab90d3bafd4a2083070c3bcce9c014ee04a443847c0b", "encoded": "aGVsbG8=", }, result) assert.Equal(t, []string{"[plugin:mock@1.0.0] called fixture"}, logs) meta, err := engine.Export(context.Background(), "meta") require.NoError(t, err) assert.Equal(t, map[string]any{"apiVersion": int64(1), "key": "mock"}, meta) } func TestEngineJSONCloneProducesIndependentNativeContainers(t *testing.T) { engine, err := Compile(` export function clone(input) { const result = utils.json.clone(input); result.items.push({text:"appended"}); result.items[0].text = "changed"; return {result, array:Array.isArray(result.items), prototype:Object.getPrototypeOf(result) === Object.prototype, ownProto:Object.prototype.hasOwnProperty.call(result,"__proto__"), available:utils.hasCapability("json-clone@1"), unavailable:utils.hasCapability("json-clone@2")}; } export function invalid(kind) { let value; switch(kind) { case "cycle": value={}; value.self=value; break; case "function": value={run:function(){}}; break; case "undefined": value={missing:undefined}; break; case "nan": value=NaN; break; case "bigint": value=1n; break; case "date": value=new Date(); break; case "sparse": value=[,1]; break; case "size": value="<".repeat(200000); break; case "nodes": value=new Array(32768).fill(0); break; } return utils.json.clone(value); }`, Options{}) require.NoError(t, err) input := map[string]any{"items": []any{map[string]any{"text": "original"}}, "__proto__": map[string]any{"marker": "data"}, "zero": math.Copysign(0, -1)} value, err := engine.Call(t.Context(), "clone", input) require.NoError(t, err) result := value.(map[string]any) assert.Equal(t, true, result["array"]) assert.Equal(t, true, result["prototype"]) assert.Equal(t, true, result["ownProto"]) assert.Equal(t, true, result["available"]) assert.Equal(t, false, result["unavailable"]) cloned := result["result"].(map[string]any) assert.Equal(t, []any{map[string]any{"text": "changed"}, map[string]any{"text": "appended"}}, cloned["items"]) assert.Equal(t, map[string]any{"marker": "data"}, cloned["__proto__"]) assert.True(t, math.Signbit(cloned["zero"].(float64))) assert.Equal(t, []any{map[string]any{"text": "original"}}, input["items"]) for _, kind := range []string{"cycle", "function", "undefined", "nan", "bigint", "date", "sparse", "size", "nodes"} { t.Run(kind, func(t *testing.T) { _, err := engine.Call(t.Context(), "invalid", kind) require.Error(t, err) }) } } func TestEngineRawJSONArgumentsMatchDecodedArguments(t *testing.T) { engine, err := Compile(` export function describe(...values) { const seen = []; const stack = values.reverse().map((v) => ({v})); while (stack.length > 0) { const {v, key, close} = stack.pop(); if (close) { seen.push(close); continue; } if (key !== undefined) seen.push(key); if (v === null || typeof v !== "object") { seen.push(typeof v + ":" + (Object.is(v, -0) ? "-0" : JSON.stringify(v))); continue; } const array = Array.isArray(v); seen.push(array ? "[" : Object.getPrototypeOf(v) === Object.prototype ? "{" : "{?"); stack.push({close: array ? "]" : "}"}); const keys = Object.keys(v); for (let i = keys.length - 1; i >= 0; i--) { stack.push({v: v[keys[i]], key: array ? undefined : JSON.stringify(keys[i])}); } } return seen.join(" "); }`, Options{}) require.NoError(t, err) // Hosts pass text written by common.Marshal, where both paths agree on // key order, numbers and strings. values := map[string]any{ "object": map[string]any{ "__proto__": map[string]any{"polluted": true}, "10": "ten", "2": "two", "a": []any{}, "b": map[string]any{}, "numbers": []any{0, math.Copysign(0, -1), 1e21, int64(123456789012345678), 1.5e-7, -12, math.MaxInt64, math.SmallestNonzeroFloat64}, "strings": []any{"", "<>&

", "Γ©δΈ­πŸ˜€", "\u0000\t\"\\", "a\xffb"}, "nested": map[string]any{"z": nil, "y": []any{true, false, map[string]any{"x": []any{[]any{1}}}}}, }, "array": []any{map[string]any{"id": "x", "b": 1, "a": 2}, nil, "s"}, "string": "plain", "number": 42.5, "null": nil, "deep": json.RawMessage(strings.Repeat("[", 600) + strings.Repeat("]", 600)), } for name, value := range values { t.Run(name, func(t *testing.T) { text, err := common.Marshal(value) require.NoError(t, err) var decoded any require.NoError(t, common.Unmarshal(text, &decoded)) want, err := engine.Call(t.Context(), "describe", decoded) require.NoError(t, err) got, err := engine.Call(t.Context(), "describe", RawJSON(text)) require.NoError(t, err) assert.Equal(t, want, got) nested, err := engine.Call(t.Context(), "describe", map[string]any{"value": RawJSON(text)}) require.NoError(t, err) wantNested, err := engine.Call(t.Context(), "describe", map[string]any{"value": decoded}) require.NoError(t, err) assert.Equal(t, wantNested, nested) }) } _, err = engine.Call(t.Context(), "describe", "first", RawJSON(`{"a":`)) require.EqualError(t, err, "plugin @ hook describe argument 2: unexpected end of JSON input") } func TestJSONStateChangesAndEncodedLimits(t *testing.T) { const changes = `[ [{"op":"set","path":[],"value":{"text":"<","parts":[],"nested":{"value":null}}}], [{"op":"appendText","path":["text"],"value":"&\n\"ε›ΎεƒπŸ˜€\u2028"}, {"op":"append","path":["parts"],"value":{"text":"a","enabled":false}}, {"op":"set","path":["nested","value"],"value":[1,2]}], [{"op":"appendText","path":["parts",0,"text"],"value":"b"}, {"op":"set","path":["nested","value",0],"value":0}, {"op":"set","path":["nested","a 0 { assert.Equal(t, testCase.wantLen, utf8.RuneCountInString(hookErr.Message)) } assert.Contains(t, hookErr.Error(), "plugin diag@1.0.0") assert.NotContains(t, hookErr.Message, "Error:") assert.NotContains(t, hookErr.Message, "plugin diag@") }) } } func TestEngineReportsProtocolAdmissionTimeoutSeparately(t *testing.T) { engine, err := Compile(` export const protocols = { responses: {renderEvents: function() { return {events: [], done: false}; }}, }; `, Options{Key: "admission", Version: "1", Concurrency: 1}) require.NoError(t, err) engine.semaphore <- struct{}{} _, err = engine.CallPathWithAdmissionTimeout( context.Background(), time.Nanosecond, "protocols", []string{"responses", "renderEvents"}, ) <-engine.semaphore require.ErrorIs(t, err, ErrCallAdmissionTimeout) result, err := engine.CallPathWithAdmissionTimeout( context.Background(), time.Second, "protocols", []string{"responses", "renderEvents"}, ) require.NoError(t, err) assert.Equal(t, map[string]any{"events": []any{}, "done": false}, result) } func TestEngineExportInterruptsLongRunningGetter(t *testing.T) { t.Parallel() engine, err := Compile(` export const meta = { apiVersion: 1, get name() { while (true) {} }, }; `, Options{Key: "meta-loop", Version: "1.0.0", Timeout: 20 * time.Millisecond}) require.NoError(t, err) _, err = engine.Export(context.Background(), "meta") require.ErrorContains(t, err, "export meta interrupted") } func TestEngineExportReturnsThrownGetterError(t *testing.T) { t.Parallel() engine, err := Compile(` export const meta = { apiVersion: 1, get name() { throw new Error("getter failed"); }, }; `, Options{Key: "meta-throw", Version: "1.0.0"}) require.NoError(t, err) _, err = engine.Export(context.Background(), "meta") require.ErrorContains(t, err, "export meta failed") assert.Contains(t, err.Error(), "getter failed") } func TestEngineNestedHooksRequireOwnProperties(t *testing.T) { t.Parallel() engine, err := Compile(` const inheritedRenderers = { inherited: function(value) { return value; }, constructor: function(value) { return value; }, toString: function(value) { return value; }, ["__proto__"]: function(value) { return value; }, }; export const renderers = Object.create(inheritedRenderers); renderers.own = function(value) { return {id: value.id}; }; const inheritedProtocol = { renderFinal: function(value) { return value; }, }; export const protocols = { responses: Object.create(inheritedProtocol), }; `, Options{Key: "own-hooks", Version: "1.0.0"}) require.NoError(t, err) found, err := engine.HasCallablePath(context.Background(), "renderers", "own") require.NoError(t, err) assert.True(t, found) result, err := engine.CallMember(context.Background(), "renderers", "own", map[string]any{"id": "task-1"}) require.NoError(t, err) assert.Equal(t, map[string]any{"id": "task-1"}, result) for _, member := range []string{"inherited", "constructor", "toString", "__proto__"} { t.Run(member, func(t *testing.T) { found, err := engine.HasCallablePath(context.Background(), "renderers", member) require.NoError(t, err) assert.False(t, found) _, err = engine.CallMember(context.Background(), "renderers", member) require.ErrorContains(t, err, "not found") }) } found, err = engine.HasCallablePath(context.Background(), "protocols", "responses", "renderFinal") require.NoError(t, err) assert.False(t, found) _, err = engine.CallPath(context.Background(), "protocols", []string{"responses", "renderFinal"}) require.ErrorContains(t, err, "not found") } func TestCompileInterruptsLongRunningInitialization(t *testing.T) { t.Parallel() _, err := Compile(`while (true) {}; export function run() {}`, Options{ Key: "loop", Version: "1", Timeout: 20 * time.Millisecond, }) require.Error(t, err) assert.Contains(t, err.Error(), "initialization timed out") } func TestValidateRequestURL(t *testing.T) { t.Parallel() tests := []struct { name string requestURL string baseURL string allowedHosts []string wantError string }{ {name: "same host", requestURL: "https://api.example.com/v1/task", baseURL: "https://api.example.com/v1"}, {name: "default port", requestURL: "https://api.example.com:443/v1/task", baseURL: "https://api.example.com"}, {name: "approved host", requestURL: "https://upload.example.com/task", baseURL: "https://api.example.com", allowedHosts: []string{"upload.example.com"}}, {name: "approved host with port", requestURL: "http://192.168.1.10:8080/task", baseURL: "http://192.168.1.10:8000", allowedHosts: []string{"192.168.1.10:8080"}}, {name: "approved host with explicit default port", requestURL: "https://upload.example.com/task", baseURL: "https://api.example.com", allowedHosts: []string{"upload.example.com:443"}}, {name: "approved host port mismatch", requestURL: "http://192.168.1.10:9000/task", baseURL: "http://192.168.1.10:8000", allowedHosts: []string{"192.168.1.10:8080"}, wantError: "not allowed"}, {name: "subdomain is not implicit", requestURL: "https://evil.api.example.com/task", baseURL: "https://api.example.com", wantError: "not allowed"}, {name: "userinfo trick", requestURL: "https://api.example.com@evil.example/task", baseURL: "https://api.example.com", wantError: "not allowed"}, {name: "relative URL", requestURL: "/v1/task", baseURL: "https://api.example.com", wantError: "absolute"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { err := ValidateRequestURL(test.requestURL, test.baseURL, test.allowedHosts) if test.wantError == "" { require.NoError(t, err) return } require.Error(t, err) assert.True(t, strings.Contains(err.Error(), test.wantError), err.Error()) }) } }