Files
2026-09-12 20:17:05 +02:00

119 lines
4.2 KiB
Go

package remotebrowser
import (
"testing"
"github.com/dop251/goja"
"github.com/phishingclub/phishingclub/embedded"
)
// TestPreludeStateMachine runs the real prelude JS in a goja VM against a
// stubbed session and asserts states() returns a machine whose before/after
// hooks and run loop work, and that present/visible/query were added to the
// session. It checks the mechanic the prelude relies on: reassigning the
// newSession global and augmenting the returned session from JS.
func TestPreludeStateMachine(t *testing.T) {
vm := goja.New()
// stage advances as actions run, simulating page progression:
// 0 password, 1 totp, 2 done.
stage := 0
// badArg records the regression where the prelude wrapper forwards undefined
// to the native newSession for a no-argument call.
badArg := false
newSession := func(call goja.FunctionCall) goja.Value {
if len(call.Arguments) > 0 {
a := call.Argument(0)
if goja.IsUndefined(a) || goja.IsNull(a) {
badArg = true
}
}
s := vm.NewObject()
_ = s.Set("location", func(goja.FunctionCall) goja.Value {
if stage >= 2 {
return vm.ToValue("https://myaccount.example/home")
}
return vm.ToValue("https://login.microsoftonline.com/step")
})
_ = s.Set("getNodeCount", func(call goja.FunctionCall) goja.Value {
sel := call.Argument(0).String()
if stage == 0 && sel == "input[type=password]" {
return vm.ToValue(1)
}
if stage == 1 && sel == "input[name=otc]" {
return vm.ToValue(1)
}
return vm.ToValue(0)
})
_ = s.Set("getText", func(goja.FunctionCall) goja.Value { return vm.ToValue("") })
_ = s.Set("evaluate", func(goja.FunctionCall) goja.Value { return vm.ToValue(false) })
_ = s.Set("wait", func(goja.FunctionCall) goja.Value { return goja.Undefined() })
return s
}
if err := vm.Set("newSession", newSession); err != nil {
t.Fatalf("set newSession: %v", err)
}
var logs []string
_ = vm.Set("log", func(call goja.FunctionCall) goja.Value {
logs = append(logs, call.Argument(0).String())
return goja.Undefined()
})
if _, err := vm.RunString(embedded.RemoteBrowserPreludeJS); err != nil {
t.Fatalf("prelude failed to load: %v", err)
}
script := `
var visited = [];
var beforeSeen = [];
var afterSeen = [];
var s = newSession();
if (typeof s.states !== "function") { throw new Error("s.states missing"); }
if (typeof s.waitForState !== "function") { throw new Error("s.waitForState missing"); }
if (typeof s.present !== "function") { throw new Error("s.present missing"); }
if (typeof s.visible !== "function") { throw new Error("s.visible missing"); }
if (typeof s.query !== "function") { throw new Error("s.query missing"); }
var machine = s.states({
password: function () { return s.present("input[type=password]"); },
totp: function () { return s.present("input[name=otc]"); },
done: function () { return !s.location().includes("microsoftonline.com"); },
});
if (typeof machine.run !== "function") { throw new Error("machine.run missing"); }
if (typeof machine.before !== "function") { throw new Error("machine.before missing"); }
if (typeof machine.after !== "function") { throw new Error("machine.after missing"); }
machine
.before(function (state) { beforeSeen.push(state); })
.after(function (state) { afterSeen.push(state); })
.run({
password: function (loop) { visited.push("password"); advance(); },
totp: function (loop) { visited.push("totp"); advance(); },
done: function (loop) { visited.push("done"); loop.stop(); },
}, { detectTimeout: 1000 });
visited.join(",") + "|" + beforeSeen.join(",") + "|" + afterSeen.join(",");
`
// advance() bumps the Go stage counter so the stub page moves forward.
_ = vm.Set("advance", func(goja.FunctionCall) goja.Value {
stage++
return goja.Undefined()
})
v, err := vm.RunString(script)
if err != nil {
t.Fatalf("script failed: %v", err)
}
if badArg {
t.Fatalf("newSession wrapper forwarded undefined/null opts for a no-argument call")
}
got := v.String()
want := "password,totp,done|password,totp,done|password,totp,done"
if got != want {
t.Fatalf("state walk = %q, want %q (logs: %v)", got, want, logs)
}
}