Files
OpenFlux/share_cli_test.go
T
Meepo 647b856b03 Своя нода: выбор транспортов канала и автообновление ноды (#125)
* Node wizard: choose the channel's transports; self-updating nodes

A new channel is no longer Yandex-only: it takes any mix of a Yandex
document, a Mail.ru public document and cups.online rooms, with direct
always as the backup. The app creates the cups.online rooms
(cupsonline.CreateRoomList) so the node starts with them in node.conf and
keeps the same rooms, and link, across restarts. provision.ShareLink
builds the link with the priorities and encryption context node-install.sh
writes; TestNodeConfMatchesShareLink runs the script's write_node_conf and
checks the core derives the same.

node-install.sh update, run every 6 hours by the optional
openflux-node-update.timer, moves the server to the newest node-v* release
of p1neappleXpress/OpenFlux (or repo= in /etc/openflux-node/update.conf):
the core must match both the release's node-install.sh at its tag and its
SHA256SUMS. If a channel does not stay up on it the previous core comes
back and that release is skipped. An older app's pinned script no longer
downgrades a server the updater has moved on.

TestInstallOnVDS covers the install with every transport, the updater,
an update, a rollback from a crashing core and the cleanup.

* provision: pin the node-install.sh with transport choice and the updater

* node-install.sh: keep only cores it installed; autoupdate on|off for existing nodes

A core some other installer left behind, such as an old fork's
node-v1.4.0, has a version number that says nothing about this
repository's releases. The no-downgrade rule and the updater now trust
only the versions this script or the updater installed (bin/.managed), so
such a server is moved to the pinned core by the wizard and to the newest
release by the updater instead of being stuck on it.

node-install.sh autoupdate on|off turns the updater on for channels
installed before the wizard offered it.

* provision: pin node-install.sh 1f7bd2a

* cupsonline: say why new rooms were refused

The wizard showed "cups: транспорт остановлен" when cups.online turned
the address away with 403 until its deadline. createRooms now keeps the
last error, and CreateRoomList names a 403/429 refusal for what it is.

* node-install.sh: remember which repository a core came from

A core is now the release of one repository: .managed lists "repo tag",
the plan says where the core comes from, and a script following another
repository (a fork's, say) moves the server to that repository's core
instead of keeping a release whose number only happens to be higher. The
updater's script copy is replaced by a script from another repository in
the same way. CORE_BASE now follows RELEASE_REPO.

* provision: pin node-install.sh 122ab88

* provision: pin node-install.sh fe9dc8b

* mobile: the upstream link test on the transports-JSON NodeShareLink

* mobile: OfferExitCookies hands a sign-in to the exit over the Session
2026-09-29 20:46:03 +03:00

99 lines
4.2 KiB
Go

package main
import (
"strings"
"testing"
"openflux/provision"
"openflux/share"
)
// The link an exit prints must decode to what a client needs: the exit's
// transports, key and context, with direct dialing the shared host.
func TestShareConfigFromExitSpecs(t *testing.T) {
specs := []transportSpec{
{Name: "direct", Type: "direct", Priority: 100, Params: map[string]interface{}{"listen": "0.0.0.0:8445", "is_exit": true}},
{Name: "yandex", Type: "yandex", Priority: 50, URL: "https://disk.yandex.ru/i/abc"},
{Name: "oneme", Type: "oneme", Priority: 10},
}
c, skipped := shareConfig(specs, true, codecBatched, "a shared secret of 32 characters", "https://disk.yandex.ru/i/abc", "203.0.113.7", nil)
if len(skipped) != 1 || !strings.HasPrefix(skipped[0], "oneme") {
t.Fatalf("skipped = %v, want the MAX transport only", skipped)
}
link, err := share.Encode(c)
if err != nil {
t.Fatal(err)
}
got, err := share.Decode(link)
if err != nil {
t.Fatal(err)
}
if !got.Negotiate || got.Context != "https://disk.yandex.ru/i/abc" || len(got.Transports) != 2 ||
got.Transports[0].Dial != "203.0.113.7:8445" || got.Transports[1].URL != "https://disk.yandex.ru/i/abc" {
t.Fatalf("decoded %+v", got)
}
_, skipped = shareConfig(specs[:1], true, codecBatched, "a shared secret of 32 characters", "", "", nil)
if len(skipped) != 1 {
t.Fatal("direct without a host to share must be left out")
}
}
type fakeRooms struct{ list string }
func (f *fakeRooms) RoomList() string { return f.list }
func (f *fakeRooms) OnRoomList(func(string)) {}
// A cupsonline exit started without --url creates its rooms at start; the
// link must carry them, or the client has nothing to join.
func TestShareConfigCarriesCreatedCupsRooms(t *testing.T) {
specs := []transportSpec{{Name: "cupsonline", Type: "cupsonline", Priority: 50}}
rooms := map[string]roomLister{"cupsonline": &fakeRooms{}}
_, skipped := shareConfig(specs, true, codecBatched, "a shared secret of 32 characters", "http://#", "", rooms)
if len(skipped) != 1 || !strings.HasPrefix(skipped[0], "cupsonline") {
t.Fatalf("skipped = %v, want cupsonline left out before its rooms exist", skipped)
}
rooms["cupsonline"].(*fakeRooms).list = "WyJyb29tLTEiXQ"
c, skipped := shareConfig(specs, true, codecBatched, "a shared secret of 32 characters", "http://#", "", rooms)
if len(skipped) != 0 || len(c.Transports) != 1 || c.Transports[0].URL != "WyJyb29tLTEiXQ" {
t.Fatalf("transports %+v, skipped %v", c.Transports, skipped)
}
}
// --parse-link and --make-link answer with share.Result, byte for byte
// what package mobile and the iOS library answer, and the wizard's link is
// the one share.Make builds for its configuration.
func TestLinkCommandsAnswerLikeShare(t *testing.T) {
cfg := `{"name":"node","negotiate":true,"secret":"a shared secret of 32 characters",` +
`"transports":[{"type":"vyandex","url":"https://docs.yandex.ru/edit/d/AbC","priority":100}]}`
var out strings.Builder
if code := runMakeLink("-", strings.NewReader(cfg), &out); code != 0 {
t.Fatalf("--make-link exit %d: %s", code, out.String())
}
made := share.MakeJSON(cfg)
if out.String() != made.JSON()+"\n" {
t.Fatalf("--make-link %s, share.MakeJSON %s", out.String(), made.JSON())
}
wrapped := made.Link[:40] + "\r\n" + made.Link[40:]
out.Reset()
if code := runParseLink("-", strings.NewReader(wrapped), &out); code != 0 || out.String() != share.Read(made.Link).JSON()+"\n" {
t.Fatalf("--parse-link exit %d: %s", code, out.String())
}
out.Reset()
if code := runParseLink("https://example.com", nil, &out); code != 1 || !strings.Contains(out.String(), `"code":"not_link"`) {
t.Fatalf("--parse-link on garbage: exit %d %s", code, out.String())
}
// The wizard's link for a Yandex-document channel is share.NodeConfig's.
doc := "https://docs.yandex.ru/edit/d/AbCdEfGhIjKlMnOpQrStUv"
link, err := provision.ShareLink("node", "a shared secret of 32 characters", "203.0.113.7", 9443,
[]provision.ChannelTransport{{Type: "vyandex", URL: doc}})
want := share.Make(share.NodeConfig("node", doc, "a shared secret of 32 characters", "203.0.113.7:9443"))
if err != nil || link != want.Link {
t.Fatalf("wizard link %q (%v), share.Make %q", link, err, want.Link)
}
}