Files
OpenFlux/node_wizard_test.go
T
p1neappleXpressandClaude Opus 4.8 38867720ac integrate: fold openfluxfork features onto the encrypted-logging hotfix
Base is hotfix/oflx-encrypted-logging-context; in every place both trees
touched the same code, the hotfix version wins (3-level logger, heard-first
routing + MarkStalled, replay window, boards JSON API, robust SOCKS5 auth).

Brought over from meepo161/openfluxfork@fork-main (the mobile/ Android bridge
is intentionally left in the Android fork):
- provision/ + node_wizard.go + --node-wizard: SSH VDS exit-node install.
- deploy/node-install.sh + .github/workflows/node-release.yml: pinned,
  hash-verified node release.
- netbind/ + tun_windows.go + --inbound=tun on Windows (Wintun): full tunnel
  with the core's own sockets bound to the real interface; dial sites in
  direct/boards/vyandex/cupsonline/mailru/yandex now dial bound.
- transport/yandex/cookies.go + --yandex-cookies-file: Netscape cookie import
  into the shared vyandex jar (sessions and single-transport alike).
- yandex: CheckVolgaDocument (doc suitability for the wizard) and the PoW
  captcha fix (a rejected challenge is a captcha, not a bad document).
- --http-proxy: HTTP CONNECT proxy over the same tunnel as SOCKS5.
- ipcStatusLoop: per-second IPC Status frame with the active carrier
  (Session.LiveTransports / ActiveTransport).

Builds for darwin, windows and linux; go vet and the full test suite pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-27 02:47:49 +03:00

143 lines
5.0 KiB
Go

//go:build !exitnode
package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"testing"
"openflux/provision"
"openflux/share"
"openflux/transport/yandex"
)
func wizardCall(t *testing.T, w *nodeWizard, method string, params interface{}) map[string]interface{} {
t.Helper()
raw, _ := json.Marshal(params)
return w.handle(wizardRequest{ID: 1, Method: method, Params: raw})
}
func TestNodeWizardProtocolLines(t *testing.T) {
in := strings.NewReader("{\"id\":7,\"method\":\"newChannel\"}\n\nnot json\n{\"id\":9,\"method\":\"nope\"}\n")
var out bytes.Buffer
if code := runNodeWizard(in, &out); code != 0 {
t.Fatalf("exit code %d", code)
}
lines := strings.Split(strings.TrimSpace(out.String()), "\n")
if len(lines) != 3 {
t.Fatalf("want 3 responses, got %d: %q", len(lines), out.String())
}
var first, bad, unknown map[string]interface{}
for i, into := range []*map[string]interface{}{&first, &bad, &unknown} {
if err := json.Unmarshal([]byte(lines[i]), into); err != nil {
t.Fatal(err)
}
}
if first["id"] != float64(7) || first["ok"] != true || len(first["key"].(string)) != 64 || first["channel"] == "" {
t.Fatalf("newChannel: %v", first)
}
if bad["ok"] != false {
t.Fatalf("bad line: %v", bad)
}
if unknown["id"] != float64(9) || unknown["ok"] != false {
t.Fatalf("unknown method: %v", unknown)
}
}
func TestNodeWizardNeedsConnection(t *testing.T) {
w := newNodeWizard()
for _, m := range []string{"plan", "apply", "setCookies", "remove"} {
r := wizardCall(t, w, m, map[string]string{"channel": "c1"})
if r["ok"] != false || r["error"] != "нет подключения к серверу" {
t.Fatalf("%s: %v", m, r)
}
}
}
func TestNodeWizardHostKeyIsReportedNotTrusted(t *testing.T) {
w := newNodeWizard()
var got provision.Target
w.dial = func(_ context.Context, target provision.Target) (*provision.Conn, error) {
got = target
if target.HostKey == "" {
return nil, &provision.HostKeyError{Fingerprint: "SHA256:abc"}
}
return nil, &provision.HostKeyError{Fingerprint: "SHA256:new", Mismatch: true}
}
r := wizardCall(t, w, "connect", wizardParams{Host: " vds.example ", Port: 2222, User: " root ", Password: "pw"})
if r["ok"] != false || r["hostKey"] != "SHA256:abc" || r["trust"] != true || r["mismatch"] != false {
t.Fatalf("new server: %v", r)
}
if got.Host != "vds.example" || got.User != "root" || got.Port != 2222 || got.Password != "pw" {
t.Fatalf("target: %+v", got)
}
r = wizardCall(t, w, "connect", wizardParams{Host: "vds.example", User: "root", Password: "pw", HostKey: "SHA256:abc"})
if r["ok"] != false || r["mismatch"] != true || r["trust"] != false {
t.Fatalf("changed key: %v", r)
}
if strings.Contains(fmt.Sprint(r), "pw") {
t.Fatalf("password leaked into the reply: %v", r)
}
}
func TestNodeWizardCheckDocument(t *testing.T) {
w := newNodeWizard()
w.checkDoc = func(u string) (yandex.VolgaDocument, error) {
switch u {
case "edit":
return yandex.VolgaDocument{Editable: true}, nil
case "view":
return yandex.VolgaDocument{}, nil
case "pow":
return yandex.VolgaDocument{}, fmt.Errorf("captcha solve: %w", errors.New("captcha POST unexpected status 400"))
default:
return yandex.VolgaDocument{}, fmt.Errorf("wrapped: %w", yandex.ErrCaptchaRequired)
}
}
if r := wizardCall(t, w, "checkDocument", wizardParams{DocumentURL: "edit"}); r["ok"] != true || r["editable"] != true {
t.Fatalf("editable: %v", r)
}
if r := wizardCall(t, w, "checkDocument", wizardParams{DocumentURL: "view"}); r["ok"] != false || r["captcha"] == true {
t.Fatalf("view only: %v", r)
}
if r := wizardCall(t, w, "checkDocument", wizardParams{DocumentURL: "captcha"}); r["ok"] != false || r["captcha"] != true {
t.Fatalf("captcha: %v", r)
}
// A PoW captcha Yandex rejected says nothing about the document either.
if r := wizardCall(t, w, "checkDocument", wizardParams{DocumentURL: "pow"}); r["ok"] != false || r["captcha"] != true {
t.Fatalf("rejected PoW captcha: %v", r)
}
}
func TestNodeWizardShareLinkAndSignIn(t *testing.T) {
w := newNodeWizard()
key := strings.Repeat("ab", 32)
doc := "https://docs.yandex.ru/edit/d/AbCdEfGhIjKlMnOpQrStUv"
r := wizardCall(t, w, "shareLink", wizardParams{Name: "Нода", DocumentURL: doc, Key: key, Host: "203.0.113.5", ChannelPort: 8445})
if r["ok"] != true {
t.Fatalf("shareLink: %v", r)
}
c, err := share.Decode(r["link"].(string))
if err != nil {
t.Fatal(err)
}
if !c.Negotiate || c.Secret != key || c.Context != doc || len(c.Transports) != 2 ||
c.Transports[0].Type != "vyandex" || c.Transports[1].Dial != "203.0.113.5:8445" {
t.Fatalf("link config: %+v", c)
}
if r := wizardCall(t, w, "shareLink", wizardParams{DocumentURL: doc, Key: key}); r["ok"] != false {
t.Fatalf("no host: %v", r)
}
if r := wizardCall(t, w, "signedIn", wizardParams{Cookies: "yandexuid=1; Session_id=s"}); r["signedIn"] != true {
t.Fatalf("signed in: %v", r)
}
if r := wizardCall(t, w, "signedIn", wizardParams{Cookies: "yandexuid=1"}); r["signedIn"] != false {
t.Fatalf("anonymous: %v", r)
}
}