Files
OpenFlux/share_cli_test.go
T
2784b04240 Links are read and made in the core: one answer and one export for every client (#127)
* share: links are read and made in the core, with codes for the apps

share.Read and share.Make answer every entry point with the same JSON
(--parse-link, new --make-link, mobile.ReadShareLink / MakeShareLink, the
iOS OpenFluxShareDecode / OpenFluxShareEncode): the configuration and its
context, the link, or an error code and param. Every link problem now has
a code (share.Code*); the apps word it for their users, the core only
decides which one it is. Error() keeps the English detail for the CLI.

share.Make normalizes what apps spell differently: surrounding spaces, the
default codec, a carrier named after its type, and the context of an
encrypted link, filled in by transport.KDFContexts when not given. One
configuration gives one link whichever client exports it. The node
wizard's link (desktop and Android) comes from one share.NodeConfig, and
--share and ExitShareLink go through share.Make too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* share: a lone carrier's link carries no priority

Apps that keep a priority for a single carrier and apps that do not now
export the same link for the same profile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: p1neappleXpress <a@a.a>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:53:31 +03:00

95 lines
4.0 KiB
Go

package main
import (
"strings"
"testing"
"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())
}
link, err := nodeShareLink("node", "https://docs.yandex.ru/edit/d/AbC", "a shared secret of 32 characters", "203.0.113.7", 9443)
want := share.Make(share.NodeConfig("node", "https://docs.yandex.ru/edit/d/AbC", "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)
}
}