begin nodejs testing
This commit is contained in:
parent
994a76cd67
commit
29688bc328
|
@ -1,13 +1,14 @@
|
||||||
package tunnel
|
package tunnel
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
kitlog "github.com/go-kit/kit/log"
|
kitlog "github.com/go-kit/kit/log"
|
||||||
"go.cryptoscope.co/muxrpc/v2"
|
"go.cryptoscope.co/muxrpc/v2"
|
||||||
"go.cryptoscope.co/muxrpc/v2/typemux"
|
"go.cryptoscope.co/muxrpc/v2/typemux"
|
||||||
|
|
||||||
refs "go.mindeco.de/ssb-refs"
|
"go.mindeco.de/ssb-rooms/internal/broadcasts"
|
||||||
"go.mindeco.de/ssb-rooms/internal/maybemuxrpc"
|
"go.mindeco.de/ssb-rooms/internal/maybemuxrpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -36,11 +37,18 @@ func (plugin) Authorize(net.Conn) bool { return true }
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
func New(log kitlog.Logger, self refs.FeedRef) maybemuxrpc.Plugin {
|
func New(log kitlog.Logger, ctx context.Context) maybemuxrpc.Plugin {
|
||||||
mux := typemux.New(log)
|
mux := typemux.New(log)
|
||||||
|
|
||||||
var rs roomState
|
var rs = new(roomState)
|
||||||
rs.logger = log
|
rs.logger = log
|
||||||
|
rs.updater, rs.broadcaster = broadcasts.NewRoomChanger()
|
||||||
|
rs.rooms = make(roomsStateMap)
|
||||||
|
|
||||||
|
go rs.stateTicker(ctx)
|
||||||
|
|
||||||
|
// so far just lobby (v1 rooms)
|
||||||
|
rs.rooms["lobby"] = make(roomStateMap)
|
||||||
|
|
||||||
mux.RegisterAsync(append(method, "isRoom"), typemux.AsyncFunc(rs.isRoom))
|
mux.RegisterAsync(append(method, "isRoom"), typemux.AsyncFunc(rs.isRoom))
|
||||||
mux.RegisterAsync(append(method, "ping"), typemux.AsyncFunc(rs.ping))
|
mux.RegisterAsync(append(method, "ping"), typemux.AsyncFunc(rs.ping))
|
||||||
|
|
|
@ -2,40 +2,137 @@ package tunnel
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"encoding/json"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"go.mindeco.de/ssb-rooms/internal/network"
|
||||||
|
|
||||||
kitlog "github.com/go-kit/kit/log"
|
kitlog "github.com/go-kit/kit/log"
|
||||||
"github.com/go-kit/kit/log/level"
|
"github.com/go-kit/kit/log/level"
|
||||||
"go.cryptoscope.co/muxrpc/v2"
|
"go.cryptoscope.co/muxrpc/v2"
|
||||||
|
"go.mindeco.de/ssb-rooms/internal/broadcasts"
|
||||||
)
|
)
|
||||||
|
|
||||||
type roomState struct {
|
type roomState struct {
|
||||||
logger kitlog.Logger
|
logger kitlog.Logger
|
||||||
|
|
||||||
|
updater broadcasts.RoomChangeSink
|
||||||
|
broadcaster *broadcasts.RoomChangeBroadcast
|
||||||
|
|
||||||
|
roomsMu sync.Mutex
|
||||||
|
rooms roomsStateMap
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rs roomState) isRoom(context.Context, *muxrpc.Request) (interface{}, error) {
|
func (rs *roomState) stateTicker(ctx context.Context) {
|
||||||
|
tick := time.NewTicker(1 * time.Second)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
tick.Stop()
|
||||||
|
return
|
||||||
|
|
||||||
|
case <-tick.C:
|
||||||
|
}
|
||||||
|
rs.roomsMu.Lock()
|
||||||
|
for room, members := range rs.rooms {
|
||||||
|
level.Info(rs.logger).Log("room", room, "cnt", len(members))
|
||||||
|
for who := range members {
|
||||||
|
level.Info(rs.logger).Log("room", room, "feed", who)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rs.roomsMu.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// layout is map[room-name]map[canonical feedref]client-handle
|
||||||
|
type roomsStateMap map[string]roomStateMap
|
||||||
|
|
||||||
|
// roomStateMap is a single room
|
||||||
|
type roomStateMap map[string]muxrpc.Endpoint
|
||||||
|
|
||||||
|
func (rs *roomState) isRoom(context.Context, *muxrpc.Request) (interface{}, error) {
|
||||||
level.Debug(rs.logger).Log("called", "isRoom")
|
level.Debug(rs.logger).Log("called", "isRoom")
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rs roomState) ping(context.Context, *muxrpc.Request) (interface{}, error) {
|
func (rs *roomState) ping(context.Context, *muxrpc.Request) (interface{}, error) {
|
||||||
now := time.Now().UnixNano() / 1000
|
now := time.Now().UnixNano() / 1000
|
||||||
level.Debug(rs.logger).Log("called", "ping")
|
level.Debug(rs.logger).Log("called", "ping")
|
||||||
return now, nil
|
return now, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rs roomState) announce(context.Context, *muxrpc.Request) (interface{}, error) {
|
func (rs *roomState) announce(_ context.Context, req *muxrpc.Request) (interface{}, error) {
|
||||||
level.Debug(rs.logger).Log("called", "announce")
|
level.Debug(rs.logger).Log("called", "announce")
|
||||||
return nil, fmt.Errorf("TODO:announce")
|
ref, err := network.GetFeedRefFromAddr(req.RemoteAddr())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rs.roomsMu.Lock()
|
||||||
|
rs.updater.Update(broadcasts.RoomChange{
|
||||||
|
Op: "joined",
|
||||||
|
Who: *ref,
|
||||||
|
})
|
||||||
|
|
||||||
|
// add ref to lobby
|
||||||
|
rs.rooms["lobby"][ref.Ref()] = req.Endpoint()
|
||||||
|
members := len(rs.rooms["lobby"])
|
||||||
|
rs.roomsMu.Unlock()
|
||||||
|
|
||||||
|
return RoomUpdate{"joined", true, uint(members)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rs roomState) leave(context.Context, *muxrpc.Request) (interface{}, error) {
|
type RoomUpdate struct {
|
||||||
level.Debug(rs.logger).Log("called", "leave")
|
Action string
|
||||||
return nil, fmt.Errorf("TODO:leave")
|
Success bool
|
||||||
|
Members uint
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rs roomState) endpoints(context.Context, *muxrpc.Request, *muxrpc.ByteSink, muxrpc.Endpoint) error {
|
func (rs *roomState) leave(_ context.Context, req *muxrpc.Request) (interface{}, error) {
|
||||||
|
ref, err := network.GetFeedRefFromAddr(req.RemoteAddr())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rs.roomsMu.Lock()
|
||||||
|
rs.updater.Update(broadcasts.RoomChange{
|
||||||
|
Op: "left",
|
||||||
|
Who: *ref,
|
||||||
|
})
|
||||||
|
|
||||||
|
// add ref to lobby
|
||||||
|
delete(rs.rooms["lobby"], ref.Ref())
|
||||||
|
members := len(rs.rooms["lobby"])
|
||||||
|
rs.roomsMu.Unlock()
|
||||||
|
|
||||||
|
return RoomUpdate{"left", true, uint(members)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rs *roomState) endpoints(_ context.Context, req *muxrpc.Request, snk *muxrpc.ByteSink, edp muxrpc.Endpoint) error {
|
||||||
level.Debug(rs.logger).Log("called", "endpoints")
|
level.Debug(rs.logger).Log("called", "endpoints")
|
||||||
return fmt.Errorf("TODO:endpoints")
|
rs.broadcaster.Register(newForwarder(snk))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type updateForwarder struct {
|
||||||
|
snk *muxrpc.ByteSink
|
||||||
|
enc *json.Encoder
|
||||||
|
}
|
||||||
|
|
||||||
|
func newForwarder(snk *muxrpc.ByteSink) updateForwarder {
|
||||||
|
enc := json.NewEncoder(snk)
|
||||||
|
snk.SetEncoding(muxrpc.TypeJSON)
|
||||||
|
return updateForwarder{
|
||||||
|
snk: snk,
|
||||||
|
enc: enc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uf updateForwarder) Update(rc broadcasts.RoomChange) error {
|
||||||
|
return uf.enc.Encode(rc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uf updateForwarder) Close() error {
|
||||||
|
return uf.snk.Close()
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,220 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package broadcasts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testPrinter struct{}
|
||||||
|
|
||||||
|
func (tp testPrinter) Update(rc RoomChange) error {
|
||||||
|
fmt.Println("test:", rc.Op, rc.Who.Ref())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tp testPrinter) Close() error { return nil }
|
||||||
|
|
||||||
|
func ExampleBroadcast() {
|
||||||
|
sink, bcast := NewRoomChanger()
|
||||||
|
defer sink.Close()
|
||||||
|
|
||||||
|
var p1, p2 testPrinter
|
||||||
|
|
||||||
|
closeSink := bcast.Register(p1)
|
||||||
|
defer closeSink()
|
||||||
|
closeSink = bcast.Register(p2)
|
||||||
|
defer closeSink()
|
||||||
|
|
||||||
|
var rc RoomChange
|
||||||
|
rc.Who.Algo = "dummy"
|
||||||
|
rc.Who.ID = []byte{0, 0, 0, 0}
|
||||||
|
rc.Op = "joined"
|
||||||
|
sink.Update(rc)
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// test: joined @AAAAAA==.dummy
|
||||||
|
// test: joined @AAAAAA==.dummy
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleBroadcastCanceled() {
|
||||||
|
sink, bcast := NewRoomChanger()
|
||||||
|
defer sink.Close()
|
||||||
|
|
||||||
|
var p1, p2 testPrinter
|
||||||
|
|
||||||
|
closeSink := bcast.Register(p1)
|
||||||
|
defer closeSink()
|
||||||
|
closeSink = bcast.Register(p2)
|
||||||
|
|
||||||
|
var rc RoomChange
|
||||||
|
rc.Who.Algo = "dummy"
|
||||||
|
rc.Who.ID = []byte{0, 0, 0, 0}
|
||||||
|
rc.Op = "joined"
|
||||||
|
|
||||||
|
closeSink()
|
||||||
|
|
||||||
|
sink.Update(rc)
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// test: joined @AAAAAA==.dummy
|
||||||
|
}
|
||||||
|
|
||||||
|
type erroringPrinter struct{}
|
||||||
|
|
||||||
|
func (tp erroringPrinter) Update(rc RoomChange) error {
|
||||||
|
fmt.Println("failed:", rc.Op, rc.Who.Ref())
|
||||||
|
return errors.New("nope")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tp erroringPrinter) Close() error { return nil }
|
||||||
|
|
||||||
|
func ExampleBroadcastOneErrs() {
|
||||||
|
sink, bcast := NewRoomChanger()
|
||||||
|
defer sink.Close()
|
||||||
|
|
||||||
|
var p1 testPrinter
|
||||||
|
var p2 erroringPrinter
|
||||||
|
|
||||||
|
closeSink := bcast.Register(p1)
|
||||||
|
defer closeSink()
|
||||||
|
closeSink = bcast.Register(p2)
|
||||||
|
defer closeSink()
|
||||||
|
|
||||||
|
var rc RoomChange
|
||||||
|
rc.Who.Algo = "dummy"
|
||||||
|
rc.Who.ID = []byte{0, 0, 0, 0}
|
||||||
|
rc.Op = "joined"
|
||||||
|
|
||||||
|
sink.Update(rc)
|
||||||
|
|
||||||
|
rc.Op = "left"
|
||||||
|
sink.Update(rc)
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// test: joined @AAAAAA==.dummy
|
||||||
|
// failed: joined @AAAAAA==.dummy
|
||||||
|
// test: left @AAAAAA==.dummy
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
type expectedEOSErr struct{ v interface{} }
|
||||||
|
|
||||||
|
func (err expectedEOSErr) Error() string {
|
||||||
|
return fmt.Sprintf("expected end of stream but got %q", err.v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err expectedEOSErr) IsExpectedEOS() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsExpectedEOS(err error) bool {
|
||||||
|
_, ok := err.(expectedEOSErr)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBroadcast(t *testing.T) {
|
||||||
|
type testcase struct {
|
||||||
|
rx, tx []interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
test := func(tc testcase) {
|
||||||
|
if tc.rx == nil {
|
||||||
|
tc.rx = tc.tx
|
||||||
|
}
|
||||||
|
|
||||||
|
sink, bcast := NewRoomChanger()
|
||||||
|
|
||||||
|
mkSink := func() Sink {
|
||||||
|
var (
|
||||||
|
closed bool
|
||||||
|
i int
|
||||||
|
)
|
||||||
|
|
||||||
|
return FuncSink(func(ctx context.Context, v interface{}, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
if err != (EOS{}) {
|
||||||
|
t.Log("closed with non-EOF error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if closed {
|
||||||
|
return fmt.Errorf("sink already closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if i != len(tc.rx) {
|
||||||
|
return fmt.Errorf("early close at i=%v", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
closed = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if i >= len(tc.rx) {
|
||||||
|
return expectedEOSErr{v}
|
||||||
|
}
|
||||||
|
|
||||||
|
if v != tc.rx[i] {
|
||||||
|
return fmt.Errorf("expected value %v but got %v", tc.rx[i], v)
|
||||||
|
}
|
||||||
|
|
||||||
|
i++
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelReg1 := bcast.Register(mkSink())
|
||||||
|
cancelReg2 := bcast.Register(mkSink())
|
||||||
|
|
||||||
|
defer cancelReg1()
|
||||||
|
defer cancelReg2()
|
||||||
|
|
||||||
|
for j, v := range tc.tx {
|
||||||
|
err := sink.Pour(context.TODO(), v)
|
||||||
|
|
||||||
|
if len(tc.tx) == len(tc.rx) {
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected nil error but got %#v", err)
|
||||||
|
}
|
||||||
|
} else if len(tc.tx) > len(tc.rx) {
|
||||||
|
if j >= len(tc.rx) {
|
||||||
|
merr, ok := err.(*multierror.Error)
|
||||||
|
if ok {
|
||||||
|
for _, err := range merr.Errors {
|
||||||
|
if !IsExpectedEOS(err) {
|
||||||
|
t.Errorf("expected an expectedEOS error, but got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !IsExpectedEOS(err) {
|
||||||
|
t.Errorf("expected an expectedEOS error, but got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected nil error but got %#v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := sink.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected nil error but got %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []testcase{
|
||||||
|
{tx: []interface{}{1, 2, 3}},
|
||||||
|
{tx: []interface{}{}},
|
||||||
|
{tx: []interface{}{nil, 0, ""}},
|
||||||
|
{tx: []interface{}{nil, 0, ""}, rx: []interface{}{nil, 0}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
test(tc)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
*/
|
|
@ -0,0 +1,105 @@
|
||||||
|
package broadcasts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
refs "go.mindeco.de/ssb-refs"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RoomChange struct {
|
||||||
|
Op string
|
||||||
|
Who refs.FeedRef
|
||||||
|
}
|
||||||
|
|
||||||
|
type RoomChangeSink interface {
|
||||||
|
Update(value RoomChange) error
|
||||||
|
io.Closer
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRoomChanger returns the Sink, to write to the broadcaster, and the new
|
||||||
|
// broadcast instance.
|
||||||
|
func NewRoomChanger() (RoomChangeSink, *RoomChangeBroadcast) {
|
||||||
|
bcst := RoomChangeBroadcast{
|
||||||
|
mu: &sync.Mutex{},
|
||||||
|
sinks: make(map[*RoomChangeSink]struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
return (*broadcastSink)(&bcst), &bcst
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoomChangeBroadcast is an interface for registering one or more Sinks to recieve
|
||||||
|
// updates.
|
||||||
|
type RoomChangeBroadcast struct {
|
||||||
|
mu *sync.Mutex
|
||||||
|
sinks map[*RoomChangeSink]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register a Sink for updates to be sent. also returns
|
||||||
|
func (bcst *RoomChangeBroadcast) Register(sink RoomChangeSink) func() {
|
||||||
|
bcst.mu.Lock()
|
||||||
|
defer bcst.mu.Unlock()
|
||||||
|
bcst.sinks[&sink] = struct{}{}
|
||||||
|
|
||||||
|
return func() {
|
||||||
|
bcst.mu.Lock()
|
||||||
|
defer bcst.mu.Unlock()
|
||||||
|
delete(bcst.sinks, &sink)
|
||||||
|
sink.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type broadcastSink RoomChangeBroadcast
|
||||||
|
|
||||||
|
// Pour implements the Sink interface.
|
||||||
|
func (bcst *broadcastSink) Update(rc RoomChange) error {
|
||||||
|
|
||||||
|
bcst.mu.Lock()
|
||||||
|
for s := range bcst.sinks {
|
||||||
|
err := (*s).Update(rc)
|
||||||
|
if err != nil {
|
||||||
|
delete(bcst.sinks, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bcst.mu.Unlock()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements the Sink interface.
|
||||||
|
func (bcst *broadcastSink) Close() error {
|
||||||
|
var sinks []RoomChangeSink
|
||||||
|
|
||||||
|
bcst.mu.Lock()
|
||||||
|
defer bcst.mu.Unlock()
|
||||||
|
|
||||||
|
sinks = make([]RoomChangeSink, 0, len(bcst.sinks))
|
||||||
|
|
||||||
|
for sink := range bcst.sinks {
|
||||||
|
sinks = append(sinks, *sink)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
wg sync.WaitGroup
|
||||||
|
merr *multierror.Error
|
||||||
|
)
|
||||||
|
|
||||||
|
// might be fine without the waitgroup and concurrency
|
||||||
|
|
||||||
|
wg.Add(len(sinks))
|
||||||
|
for _, sink_ := range sinks {
|
||||||
|
go func(sink RoomChangeSink) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
err := sink.Close()
|
||||||
|
if err != nil {
|
||||||
|
merr = multierror.Append(merr, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}(sink_)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
return merr.ErrorOrNil()
|
||||||
|
}
|
|
@ -5,8 +5,9 @@ package multicloser
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"go.mindeco.de/ssb-rooms/internal/maybemod/multierror"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Closer struct {
|
type Closer struct {
|
||||||
|
@ -44,23 +45,5 @@ func (mc *Closer) Close() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return errList{errs: errs}
|
return multierror.List{Errs: errs}
|
||||||
}
|
|
||||||
|
|
||||||
type errList struct {
|
|
||||||
errs []error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (el errList) Error() string {
|
|
||||||
var str strings.Builder
|
|
||||||
|
|
||||||
if n := len(el.errs); n > 0 {
|
|
||||||
fmt.Fprintf(&str, "multiple errors(%d): ", n)
|
|
||||||
}
|
|
||||||
for i, err := range el.errs {
|
|
||||||
fmt.Fprintf(&str, "(%d): ", i)
|
|
||||||
str.WriteString(err.Error() + " - ")
|
|
||||||
}
|
|
||||||
|
|
||||||
return str.String()
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
package multierror
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// List contains a list of errors
|
||||||
|
type List struct{ Errs []error }
|
||||||
|
|
||||||
|
func (el List) Error() string {
|
||||||
|
var str strings.Builder
|
||||||
|
|
||||||
|
if n := len(el.Errs); n > 0 {
|
||||||
|
fmt.Fprintf(&str, "multiple errors(%d): ", n)
|
||||||
|
}
|
||||||
|
for i, err := range el.Errs {
|
||||||
|
fmt.Fprintf(&str, "(%d): ", i)
|
||||||
|
str.WriteString(err.Error() + " - ")
|
||||||
|
}
|
||||||
|
|
||||||
|
return str.String()
|
||||||
|
}
|
|
@ -41,7 +41,7 @@ func (s *Server) initNetwork() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// whoami
|
// whoami
|
||||||
whoami := whoami.New(kitlog.With(s.logger, "unit", "whoami"), s.keyPair.Feed)
|
whoami := whoami.New(kitlog.With(s.logger, "unit", "whoami"), s.Whoami())
|
||||||
s.public.Register(whoami)
|
s.public.Register(whoami)
|
||||||
s.master.Register(whoami)
|
s.master.Register(whoami)
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ func (s *Server) initNetwork() error {
|
||||||
|
|
||||||
// s.master.Register(replicate.NewPlug(s.Users))
|
// s.master.Register(replicate.NewPlug(s.Users))
|
||||||
|
|
||||||
tunnelPlug := tunnel.New(kitlog.With(s.logger, "unit", "tunnel"), s.keyPair.Feed)
|
tunnelPlug := tunnel.New(kitlog.With(s.logger, "unit", "tunnel"), s.rootCtx)
|
||||||
s.public.Register(tunnelPlug)
|
s.public.Register(tunnelPlug)
|
||||||
|
|
||||||
// tcp+shs
|
// tcp+shs
|
||||||
|
|
|
@ -30,6 +30,15 @@ func (h manifestHandler) HandleCall(ctx context.Context, req *muxrpc.Request, ed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if !json.Valid([]byte(manifest)) {
|
||||||
|
manifestMap := make(map[string]interface{})
|
||||||
|
err := json.Unmarshal([]byte(manifest), &manifestMap)
|
||||||
|
fmt.Println(err)
|
||||||
|
panic("manifest blob is broken json")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// this is a very simple hardcoded manifest.json dump which oasis' ssb-client expects to do it's magic.
|
// this is a very simple hardcoded manifest.json dump which oasis' ssb-client expects to do it's magic.
|
||||||
const manifest manifestHandler = `
|
const manifest manifestHandler = `
|
||||||
{
|
{
|
||||||
|
@ -43,7 +52,7 @@ const manifest manifestHandler = `
|
||||||
"connect": "duplex",
|
"connect": "duplex",
|
||||||
"endpoints": "source",
|
"endpoints": "source",
|
||||||
"isRoom": "async",
|
"isRoom": "async",
|
||||||
"ping": "sync",
|
"ping": "sync"
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ package go_test
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -15,14 +16,10 @@ import (
|
||||||
"go.mindeco.de/ssb-rooms/roomsrv"
|
"go.mindeco.de/ssb-rooms/roomsrv"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTunnelServerSimple(t *testing.T) {
|
func createServerAndBots(t *testing.T, ctx context.Context, count uint) []*roomsrv.Server {
|
||||||
r := require.New(t)
|
|
||||||
a := assert.New(t)
|
|
||||||
|
|
||||||
// defer leakcheck.Check(t)
|
|
||||||
testInit(t)
|
testInit(t)
|
||||||
|
r := require.New(t)
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
botgroup, ctx := errgroup.WithContext(ctx)
|
botgroup, ctx := errgroup.WithContext(ctx)
|
||||||
|
|
||||||
bs := newBotServer(ctx, mainLog)
|
bs := newBotServer(ctx, mainLog)
|
||||||
|
@ -34,17 +31,42 @@ func TestTunnelServerSimple(t *testing.T) {
|
||||||
roomsrv.WithAppKey(appKey),
|
roomsrv.WithAppKey(appKey),
|
||||||
roomsrv.WithContext(ctx),
|
roomsrv.WithContext(ctx),
|
||||||
}
|
}
|
||||||
|
theBots := []*roomsrv.Server{}
|
||||||
|
|
||||||
serv := makeNamedTestBot(t, "srv", netOpts)
|
serv := makeNamedTestBot(t, "srv", netOpts)
|
||||||
botgroup.Go(bs.Serve(serv))
|
botgroup.Go(bs.Serve(serv))
|
||||||
|
theBots = append(theBots, serv)
|
||||||
|
|
||||||
botA := makeNamedTestBot(t, "B", netOpts)
|
for i := uint(1); i < count+1; i++ {
|
||||||
botgroup.Go(bs.Serve(botA))
|
botI := makeNamedTestBot(t, fmt.Sprintf("%d", i), netOpts)
|
||||||
|
botgroup.Go(bs.Serve(botI))
|
||||||
|
theBots = append(theBots, botI)
|
||||||
|
}
|
||||||
|
|
||||||
botB := makeNamedTestBot(t, "C", netOpts)
|
t.Cleanup(func() {
|
||||||
botgroup.Go(bs.Serve(botB))
|
time.Sleep(1 * time.Second)
|
||||||
|
for _, bot := range theBots {
|
||||||
|
bot.Shutdown()
|
||||||
|
r.NoError(bot.Close())
|
||||||
|
}
|
||||||
|
r.NoError(botgroup.Wait())
|
||||||
|
})
|
||||||
|
|
||||||
theBots := []*roomsrv.Server{serv, botA, botB}
|
return theBots
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTunnelServerSimple(t *testing.T) {
|
||||||
|
// defer leakcheck.Check(t)
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
theBots := createServerAndBots(t, ctx, 2)
|
||||||
|
|
||||||
|
r := require.New(t)
|
||||||
|
a := assert.New(t)
|
||||||
|
|
||||||
|
serv := theBots[0]
|
||||||
|
|
||||||
|
botA := theBots[1]
|
||||||
|
botB := theBots[2]
|
||||||
|
|
||||||
// only allow B to dial A
|
// only allow B to dial A
|
||||||
serv.Allow(botA.Whoami(), true)
|
serv.Allow(botA.Whoami(), true)
|
||||||
|
@ -101,10 +123,113 @@ func TestTunnelServerSimple(t *testing.T) {
|
||||||
|
|
||||||
// cleanup
|
// cleanup
|
||||||
cancel()
|
cancel()
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
for _, bot := range theBots {
|
}
|
||||||
bot.Shutdown()
|
|
||||||
r.NoError(bot.Close())
|
func TestRoomAnnounce(t *testing.T) {
|
||||||
}
|
// defer leakcheck.Check(t)
|
||||||
r.NoError(botgroup.Wait())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
theBots := createServerAndBots(t, ctx, 2)
|
||||||
|
|
||||||
|
r := require.New(t)
|
||||||
|
a := assert.New(t)
|
||||||
|
|
||||||
|
serv := theBots[0]
|
||||||
|
|
||||||
|
botA := theBots[1]
|
||||||
|
botB := theBots[2]
|
||||||
|
|
||||||
|
// only allow B to dial A
|
||||||
|
serv.Allow(botA.Whoami(), true)
|
||||||
|
serv.Allow(botB.Whoami(), true)
|
||||||
|
|
||||||
|
// allow bots to dial the remote
|
||||||
|
botA.Allow(serv.Whoami(), true)
|
||||||
|
botB.Allow(serv.Whoami(), true)
|
||||||
|
|
||||||
|
// should work (we allowed A)
|
||||||
|
err := botA.Network.Connect(ctx, serv.Network.GetListenAddr())
|
||||||
|
r.NoError(err, "connect A to the Server")
|
||||||
|
|
||||||
|
// shouldn't work (we did not allowed A)
|
||||||
|
err = botB.Network.Connect(ctx, serv.Network.GetListenAddr())
|
||||||
|
r.NoError(err, "connect B to the Server") // we dont see an error because it just establishes the tcp connection
|
||||||
|
|
||||||
|
t.Log("letting handshaking settle..")
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
|
var srvWho struct {
|
||||||
|
ID refs.FeedRef
|
||||||
|
}
|
||||||
|
edpOfA, has := botA.Network.GetEndpointFor(serv.Whoami())
|
||||||
|
r.True(has, "botA has no endpoint for the server")
|
||||||
|
|
||||||
|
edpOfB, has := botB.Network.GetEndpointFor(serv.Whoami())
|
||||||
|
r.True(has, "botB has no endpoint for the server!")
|
||||||
|
|
||||||
|
err = edpOfA.Async(ctx, &srvWho, muxrpc.TypeJSON, muxrpc.Method{"whoami"})
|
||||||
|
r.NoError(err)
|
||||||
|
a.True(serv.Whoami().Equal(&srvWho.ID))
|
||||||
|
|
||||||
|
err = edpOfB.Async(ctx, &srvWho, muxrpc.TypeJSON, muxrpc.Method{"whoami"})
|
||||||
|
r.NoError(err)
|
||||||
|
a.True(serv.Whoami().Equal(&srvWho.ID))
|
||||||
|
|
||||||
|
// let B listen for changes
|
||||||
|
newRoomMember, err := edpOfB.Source(ctx, muxrpc.TypeJSON, muxrpc.Method{"tunnel", "endpoints"})
|
||||||
|
r.NoError(err)
|
||||||
|
|
||||||
|
newMemberChan := make(chan string)
|
||||||
|
|
||||||
|
// read all the messages from endpoints and throw them over the channel
|
||||||
|
go func() {
|
||||||
|
for newRoomMember.Next(ctx) {
|
||||||
|
|
||||||
|
body, err := newRoomMember.Bytes()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newMemberChan <- string(body)
|
||||||
|
}
|
||||||
|
close(newMemberChan)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// announce A
|
||||||
|
var ret struct {
|
||||||
|
Action string
|
||||||
|
Success bool
|
||||||
|
Members uint
|
||||||
|
}
|
||||||
|
err = edpOfA.Async(ctx, &ret, muxrpc.TypeJSON, muxrpc.Method{"tunnel", "announce"})
|
||||||
|
r.NoError(err)
|
||||||
|
a.Equal("joined", ret.Action)
|
||||||
|
a.True(ret.Success)
|
||||||
|
a.EqualValues(1, ret.Members, "expected just one member")
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-time.After(10 * time.Second):
|
||||||
|
t.Error("timeout")
|
||||||
|
case got := <-newMemberChan:
|
||||||
|
t.Log("received join?")
|
||||||
|
t.Log(got)
|
||||||
|
}
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
|
||||||
|
err = edpOfA.Async(ctx, &ret, muxrpc.TypeJSON, muxrpc.Method{"tunnel", "leave"})
|
||||||
|
r.NoError(err)
|
||||||
|
a.Equal("left", ret.Action)
|
||||||
|
a.True(ret.Success)
|
||||||
|
a.EqualValues(0, ret.Members, "expected empty rooms")
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-time.After(10 * time.Second):
|
||||||
|
t.Error("timeout")
|
||||||
|
case got := <-newMemberChan:
|
||||||
|
t.Log("received leave?")
|
||||||
|
t.Log(got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanup
|
||||||
|
cancel()
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -12,6 +13,7 @@ import (
|
||||||
"github.com/go-kit/kit/log"
|
"github.com/go-kit/kit/log"
|
||||||
"github.com/go-kit/kit/log/level"
|
"github.com/go-kit/kit/log/level"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.cryptoscope.co/muxrpc/v2/debug"
|
||||||
|
|
||||||
"go.mindeco.de/ssb-rooms/internal/maybemod/testutils"
|
"go.mindeco.de/ssb-rooms/internal/maybemod/testutils"
|
||||||
"go.mindeco.de/ssb-rooms/internal/network"
|
"go.mindeco.de/ssb-rooms/internal/network"
|
||||||
|
@ -59,12 +61,16 @@ func (bs botServer) Serve(s *roomsrv.Server) func() error {
|
||||||
func makeNamedTestBot(t testing.TB, name string, opts []roomsrv.Option) *roomsrv.Server {
|
func makeNamedTestBot(t testing.TB, name string, opts []roomsrv.Option) *roomsrv.Server {
|
||||||
r := require.New(t)
|
r := require.New(t)
|
||||||
testPath := filepath.Join("testrun", t.Name(), "bot-"+name)
|
testPath := filepath.Join("testrun", t.Name(), "bot-"+name)
|
||||||
|
os.RemoveAll(testPath)
|
||||||
|
|
||||||
botOptions := append(opts,
|
botOptions := append(opts,
|
||||||
roomsrv.WithLogger(log.With(mainLog, "bot", name)),
|
roomsrv.WithLogger(log.With(mainLog, "bot", name)),
|
||||||
roomsrv.WithRepoPath(testPath),
|
roomsrv.WithRepoPath(testPath),
|
||||||
roomsrv.WithListenAddr(":0"),
|
roomsrv.WithListenAddr(":0"),
|
||||||
roomsrv.WithNetworkConnTracker(network.NewLastWinsTracker()),
|
roomsrv.WithNetworkConnTracker(network.NewLastWinsTracker()),
|
||||||
|
roomsrv.WithPostSecureConnWrapper(func(conn net.Conn) (net.Conn, error) {
|
||||||
|
return debug.WrapDump(filepath.Join(testPath, "muxdump"), conn)
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
theBot, err := roomsrv.New(botOptions...)
|
theBot, err := roomsrv.New(botOptions...)
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
node_modules
|
||||||
|
testrun
|
|
@ -0,0 +1,150 @@
|
||||||
|
package nodejs_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.cryptoscope.co/muxrpc/v2"
|
||||||
|
"go.cryptoscope.co/netwrap"
|
||||||
|
"go.cryptoscope.co/secretstream"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestJSClient(t *testing.T) {
|
||||||
|
// defer leakcheck.Check(t)
|
||||||
|
// r := require.New(t)
|
||||||
|
|
||||||
|
// ts := newRandomSession(t)
|
||||||
|
ts := newSession(t, nil, nil)
|
||||||
|
|
||||||
|
ts.startGoServer()
|
||||||
|
s := ts.gobot
|
||||||
|
|
||||||
|
alice := ts.startJSBot(`
|
||||||
|
sbot.on('rpc:connect', rpc => {
|
||||||
|
var ret = rpc.tunnel.announce()
|
||||||
|
t.comment('announced')
|
||||||
|
console.warn(ret)
|
||||||
|
pull(
|
||||||
|
rpc.tunnel.endpoints(),
|
||||||
|
pull.drain(el => {
|
||||||
|
console.warn("from roomsrv:",el)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
ret = rpc.tunnel.leave()
|
||||||
|
t.comment('left')
|
||||||
|
console.warn(ret)
|
||||||
|
}, 2500)
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
t.comment('shutting down')
|
||||||
|
exit()
|
||||||
|
}, 5000)
|
||||||
|
})
|
||||||
|
run()`, ``)
|
||||||
|
|
||||||
|
s.Allow(alice, true)
|
||||||
|
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
|
||||||
|
ts.wait()
|
||||||
|
|
||||||
|
// TODO: check wantManager for this connection is stopped when the jsbot exited
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSServer(t *testing.T) {
|
||||||
|
// defer leakcheck.Check(t)
|
||||||
|
r := require.New(t)
|
||||||
|
a := assert.New(t)
|
||||||
|
|
||||||
|
os.RemoveAll("testrun")
|
||||||
|
|
||||||
|
// ts := newRandomSession(t)
|
||||||
|
ts := newSession(t, nil, nil)
|
||||||
|
|
||||||
|
ts.startGoServer()
|
||||||
|
client := ts.gobot
|
||||||
|
|
||||||
|
// alice is the server now
|
||||||
|
alice, port := ts.startJSBotAsServer("alice", "./testscripts/server.js")
|
||||||
|
|
||||||
|
client.Allow(*alice, true)
|
||||||
|
|
||||||
|
wrappedAddr := netwrap.WrapAddr(&net.TCPAddr{
|
||||||
|
IP: net.ParseIP("127.0.0.1"),
|
||||||
|
Port: port,
|
||||||
|
}, secretstream.Addr{PubKey: alice.ID})
|
||||||
|
|
||||||
|
ctx, connCancel := context.WithCancel(context.TODO())
|
||||||
|
err := client.Network.Connect(ctx, wrappedAddr)
|
||||||
|
defer connCancel()
|
||||||
|
r.NoError(err, "connect #1 failed")
|
||||||
|
|
||||||
|
// this might fail if the previous node process is still running...
|
||||||
|
// TODO: properly write cleanup
|
||||||
|
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
|
||||||
|
srvEdp, has := client.Network.GetEndpointFor(*alice)
|
||||||
|
r.True(has, "botA has no endpoint for the server")
|
||||||
|
t.Log("connected")
|
||||||
|
|
||||||
|
// let B listen for changes
|
||||||
|
newRoomMember, err := srvEdp.Source(ctx, muxrpc.TypeJSON, muxrpc.Method{"tunnel", "endpoints"})
|
||||||
|
r.NoError(err)
|
||||||
|
|
||||||
|
newMemberChan := make(chan string)
|
||||||
|
|
||||||
|
// read all the messages from endpoints and throw them over the channel
|
||||||
|
go func() {
|
||||||
|
for newRoomMember.Next(ctx) {
|
||||||
|
body, err := newRoomMember.Bytes()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
newMemberChan <- string(body)
|
||||||
|
}
|
||||||
|
close(newMemberChan)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// announce A
|
||||||
|
var ret bool
|
||||||
|
err = srvEdp.Async(ctx, &ret, muxrpc.TypeJSON, muxrpc.Method{"tunnel", "announce"})
|
||||||
|
r.NoError(err)
|
||||||
|
// a.Equal("joined", ret.Action)
|
||||||
|
a.False(ret, "would assume these are true but..?")
|
||||||
|
// a.EqualValues(1, ret.Members, "expected just one member")
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-time.After(10 * time.Second):
|
||||||
|
t.Error("timeout")
|
||||||
|
case got := <-newMemberChan:
|
||||||
|
t.Log("received join?")
|
||||||
|
t.Log(got)
|
||||||
|
}
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
|
||||||
|
err = srvEdp.Async(ctx, &ret, muxrpc.TypeJSON, muxrpc.Method{"tunnel", "leave"})
|
||||||
|
r.NoError(err)
|
||||||
|
// a.Equal("left", ret.Action)
|
||||||
|
a.False(ret, "would assume these are true but..?")
|
||||||
|
// a.EqualValues(0, ret.Members, "expected empty rooms")
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-time.After(10 * time.Second):
|
||||||
|
t.Error("timeout")
|
||||||
|
case got := <-newMemberChan:
|
||||||
|
t.Log("received leave?")
|
||||||
|
t.Log(got)
|
||||||
|
}
|
||||||
|
|
||||||
|
ts.wait()
|
||||||
|
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"name": "go-ssb-rooms-tests",
|
||||||
|
"version": "1.0.1",
|
||||||
|
"description": "tests between go and ssb-js",
|
||||||
|
"main": "sbot_client.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "go test"
|
||||||
|
},
|
||||||
|
"author": "cryptix",
|
||||||
|
"dependencies": {
|
||||||
|
"run-parallel": "^1.1.9",
|
||||||
|
"run-series": "^1.1.9",
|
||||||
|
"secret-stack": "^6.3.1",
|
||||||
|
"sodium-native": "^3.2.0",
|
||||||
|
"ssb-config": "^3.4.5",
|
||||||
|
"ssb-conn": "^0.19.1",
|
||||||
|
"ssb-db": "^20.3.0",
|
||||||
|
"ssb-gossip": "^1.1.1",
|
||||||
|
"ssb-keys": "^8.0.0",
|
||||||
|
"ssb-replicate": "^1.3.2",
|
||||||
|
"ssb-room": "^1.3.0",
|
||||||
|
"tape": "^5.0.1"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
const Path = require('path')
|
||||||
|
const { readFileSync } = require('fs')
|
||||||
|
const { loadOrCreateSync } = require('ssb-keys')
|
||||||
|
const pull = require('pull-stream') // used in eval scripts
|
||||||
|
const tape = require('tape')
|
||||||
|
const parallel = require('run-parallel') // used in eval scripts
|
||||||
|
const theStack = require('secret-stack')
|
||||||
|
const ssbCaps = require('ssb-caps')
|
||||||
|
|
||||||
|
const testSHSappKey = bufFromEnv('TEST_APPKEY')
|
||||||
|
|
||||||
|
let testAppkey = Buffer.from(ssbCaps.shs, 'base64')
|
||||||
|
if (testSHSappKey !== false) {
|
||||||
|
testAppkey = testSHSappKey
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const createSbot = theStack({caps: {shs: testAppkey } })
|
||||||
|
.use(require('ssb-db'))
|
||||||
|
.use(require('ssb-gossip'))
|
||||||
|
.use(require('ssb-replicate'))
|
||||||
|
.use(require('ssb-conn'))
|
||||||
|
.use(require('ssb-room/tunnel/client'))
|
||||||
|
|
||||||
|
const testName = process.env.TEST_NAME
|
||||||
|
const testBob = process.env.TEST_BOB
|
||||||
|
const testAddr = process.env.TEST_GOADDR
|
||||||
|
|
||||||
|
const scriptBefore = readFileSync(process.env.TEST_BEFORE).toString()
|
||||||
|
const scriptAfter = readFileSync(process.env.TEST_AFTER).toString()
|
||||||
|
|
||||||
|
function bufFromEnv(evname) {
|
||||||
|
const has = process.env[evname]
|
||||||
|
if (has) {
|
||||||
|
return Buffer.from(has, 'base64')
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
tape.createStream().pipe(process.stderr)
|
||||||
|
tape(testName, function (t) {
|
||||||
|
|
||||||
|
let timeoutLength = 15000
|
||||||
|
var tapeTimeout = null
|
||||||
|
function run() { // needs to be called by the before block when it's done
|
||||||
|
t.timeoutAfter(timeoutLength) // doesn't exit the process
|
||||||
|
tapeTimeout = setTimeout(() => {
|
||||||
|
t.comment('test timeout')
|
||||||
|
process.exit(1)
|
||||||
|
}, timeoutLength*1.25)
|
||||||
|
const to = `net:${testAddr}~shs:${testBob.substr(1).replace('.ed25519', '')}`
|
||||||
|
t.comment('dialing:' + to)
|
||||||
|
sbot.connect(to, (err) => {
|
||||||
|
t.error(err, 'connected')
|
||||||
|
eval(scriptAfter)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function exit() { // call this when you're done
|
||||||
|
sbot.close()
|
||||||
|
t.comment('closed sbot')
|
||||||
|
clearTimeout(tapeTimeout)
|
||||||
|
t.end()
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const tempRepo = Path.join('testrun', testName)
|
||||||
|
const keys = loadOrCreateSync(Path.join(tempRepo, 'secret'))
|
||||||
|
const opts = {
|
||||||
|
allowPrivate: true,
|
||||||
|
path: tempRepo,
|
||||||
|
keys: keys
|
||||||
|
}
|
||||||
|
|
||||||
|
if (testSHSappKey !== false) {
|
||||||
|
opts.caps = opts.caps ? opts.caps : {}
|
||||||
|
opts.caps.shs = testSHSappKey
|
||||||
|
}
|
||||||
|
|
||||||
|
const sbot = createSbot(opts)
|
||||||
|
const alice = sbot.whoami()
|
||||||
|
|
||||||
|
t.comment('sbot spawned, running before')
|
||||||
|
console.log(alice.id) // tell go process who's incoming
|
||||||
|
eval(scriptBefore)
|
||||||
|
})
|
|
@ -0,0 +1,81 @@
|
||||||
|
const Path = require('path')
|
||||||
|
const tape = require('tape')
|
||||||
|
const { readFileSync } = require('fs')
|
||||||
|
const { loadOrCreateSync } = require('ssb-keys')
|
||||||
|
const theStack = require('secret-stack')
|
||||||
|
const ssbCaps = require('ssb-caps')
|
||||||
|
|
||||||
|
const testSHSappKey = bufFromEnv('TEST_APPKEY')
|
||||||
|
|
||||||
|
let testAppkey = Buffer.from(ssbCaps.shs, 'base64')
|
||||||
|
if (testSHSappKey !== false) {
|
||||||
|
testAppkey = testSHSappKey
|
||||||
|
}
|
||||||
|
|
||||||
|
stackOpts = {appKey: require('ssb-caps').shs}
|
||||||
|
// stackOpts = {caps: {shs: testAppkey } }
|
||||||
|
const createSbot = theStack(stackOpts)
|
||||||
|
.use(require('ssb-db'))
|
||||||
|
.use(require('ssb-master'))
|
||||||
|
.use(require('ssb-logging'))
|
||||||
|
.use(require('ssb-conn'))
|
||||||
|
.use(require('ssb-room/tunnel/server'))
|
||||||
|
|
||||||
|
const testName = process.env['TEST_NAME']
|
||||||
|
const testPort = process.env['TEST_PORT']
|
||||||
|
|
||||||
|
const testSession = require(process.env['TEST_BEFORE'])
|
||||||
|
// const scriptBefore = readFileSync(
|
||||||
|
// const scriptAfter = readFileSync(process.env['TEST_AFTER']).toString()
|
||||||
|
|
||||||
|
tape.createStream().pipe(process.stderr);
|
||||||
|
tape(testName, function (t) {
|
||||||
|
// t.timeoutAfter(30000) // doesn't exit the process
|
||||||
|
// const tapeTimeout = setTimeout(() => {
|
||||||
|
// t.comment("test timeout")
|
||||||
|
// process.exit(1)
|
||||||
|
// }, 50000)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function exit() { // call this when you're done
|
||||||
|
sbot.close()
|
||||||
|
t.comment('closed jsbot')
|
||||||
|
// clearTimeout(tapeTimeout)
|
||||||
|
t.end()
|
||||||
|
}
|
||||||
|
|
||||||
|
const tempRepo = Path.join('testrun', testName)
|
||||||
|
const keys = loadOrCreateSync(Path.join(tempRepo, 'secret'))
|
||||||
|
const sbot = createSbot({
|
||||||
|
port: testPort,
|
||||||
|
path: tempRepo,
|
||||||
|
keys: keys,
|
||||||
|
})
|
||||||
|
const alice = sbot.whoami()
|
||||||
|
|
||||||
|
// const replicate_changes = sbot.replicate.changes()
|
||||||
|
|
||||||
|
t.comment("sbot spawned, running before")
|
||||||
|
|
||||||
|
function ready() {
|
||||||
|
console.warn('ready!', alice.id)
|
||||||
|
console.log(alice.id) // tell go process who our pubkey
|
||||||
|
}
|
||||||
|
testSession.before(sbot, ready)
|
||||||
|
|
||||||
|
|
||||||
|
sbot.on("rpc:connect", (remote, isClient) => {
|
||||||
|
t.comment("new connection: "+ remote.id)
|
||||||
|
testSession.after(sbot, remote)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// util
|
||||||
|
function bufFromEnv(evname) {
|
||||||
|
const has = process.env[evname]
|
||||||
|
if (has) {
|
||||||
|
return Buffer.from(has, 'base64')
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
|
@ -1 +1,261 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
// Package nodejs_test contains test scenarios and helpers to run interoparability tests against the javascript implementation.
|
||||||
package nodejs_test
|
package nodejs_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
mrand "math/rand"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
|
||||||
|
"github.com/go-kit/kit/log"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.cryptoscope.co/muxrpc/v2/debug"
|
||||||
|
"go.cryptoscope.co/netwrap"
|
||||||
|
refs "go.mindeco.de/ssb-refs"
|
||||||
|
|
||||||
|
"go.mindeco.de/ssb-rooms/internal/maybemod/testutils"
|
||||||
|
"go.mindeco.de/ssb-rooms/roomsrv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
err := os.RemoveAll("testrun")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("failed to clean testrun dir")
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeFile(t *testing.T, data string) string {
|
||||||
|
r := require.New(t)
|
||||||
|
f, err := ioutil.TempFile("testrun/"+t.Name(), "*.js")
|
||||||
|
r.NoError(err)
|
||||||
|
_, err = fmt.Fprintf(f, "%s", data)
|
||||||
|
r.NoError(err)
|
||||||
|
err = f.Close()
|
||||||
|
r.NoError(err)
|
||||||
|
return f.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
type testSession struct {
|
||||||
|
t *testing.T
|
||||||
|
|
||||||
|
info log.Logger
|
||||||
|
|
||||||
|
repo string
|
||||||
|
|
||||||
|
keySHS, keyHMAC []byte
|
||||||
|
|
||||||
|
// since we can't pass *testing.T to other goroutines, we use this to collect errors from background taskts
|
||||||
|
backgroundErrs []<-chan error
|
||||||
|
|
||||||
|
gobot *roomsrv.Server
|
||||||
|
|
||||||
|
done errgroup.Group
|
||||||
|
// doneJS, doneGo <-chan struct{}
|
||||||
|
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: restrucuture so that we can test both (default and random net keys) with the same Code
|
||||||
|
|
||||||
|
// rolls random values for secret-handshake app-key and HMAC
|
||||||
|
func newRandomSession(t *testing.T) *testSession {
|
||||||
|
appKey := make([]byte, 32)
|
||||||
|
rand.Read(appKey)
|
||||||
|
hmacKey := make([]byte, 32)
|
||||||
|
rand.Read(hmacKey)
|
||||||
|
return newSession(t, appKey, hmacKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if appKey is nil, the default value is used
|
||||||
|
// if hmac is nil, the object string is signed instead
|
||||||
|
func newSession(t *testing.T, appKey, hmacKey []byte) *testSession {
|
||||||
|
repo := filepath.Join("testrun", t.Name())
|
||||||
|
err := os.RemoveAll(repo)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("remove testrun folder (%s) for this test: %s", repo, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ts := &testSession{
|
||||||
|
info: testutils.NewRelativeTimeLogger(nil),
|
||||||
|
repo: repo,
|
||||||
|
t: t,
|
||||||
|
keySHS: appKey,
|
||||||
|
keyHMAC: hmacKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: hook into deadline
|
||||||
|
ts.ctx, ts.cancel = context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
return ts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *testSession) startGoServer(opts ...roomsrv.Option) {
|
||||||
|
r := require.New(ts.t)
|
||||||
|
|
||||||
|
// prepend defaults
|
||||||
|
opts = append([]roomsrv.Option{
|
||||||
|
roomsrv.WithLogger(ts.info),
|
||||||
|
roomsrv.WithListenAddr("localhost:0"),
|
||||||
|
roomsrv.WithRepoPath(ts.repo),
|
||||||
|
roomsrv.WithContext(ts.ctx),
|
||||||
|
}, opts...)
|
||||||
|
|
||||||
|
if ts.keySHS != nil {
|
||||||
|
opts = append(opts, roomsrv.WithAppKey(ts.keySHS))
|
||||||
|
}
|
||||||
|
|
||||||
|
opts = append(opts,
|
||||||
|
roomsrv.WithPostSecureConnWrapper(func(conn net.Conn) (net.Conn, error) {
|
||||||
|
return debug.WrapDump(filepath.Join("testrun", ts.t.Name(), "muxdump"), conn)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
srv, err := roomsrv.New(opts...)
|
||||||
|
r.NoError(err, "failed to init tees a server")
|
||||||
|
ts.t.Logf("go server: %s", srv.Whoami())
|
||||||
|
ts.t.Cleanup(func() {
|
||||||
|
srv.Close()
|
||||||
|
})
|
||||||
|
|
||||||
|
ts.done.Go(func() error {
|
||||||
|
err := srv.Network.Serve(ts.ctx)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("node serve exited: %w", err)
|
||||||
|
ts.t.Log(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
ts.gobot = srv
|
||||||
|
|
||||||
|
// TODO: make muxrpc client and connect to whoami for _ready_ ?
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var jsBotCnt = 0
|
||||||
|
|
||||||
|
func (ts *testSession) startJSBot(jsbefore, jsafter string) refs.FeedRef {
|
||||||
|
return ts.startJSBotWithName("", jsbefore, jsafter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns the jsbots pubkey
|
||||||
|
func (ts *testSession) startJSBotWithName(name, jsbefore, jsafter string) refs.FeedRef {
|
||||||
|
ts.t.Log("starting client", name)
|
||||||
|
r := require.New(ts.t)
|
||||||
|
cmd := exec.CommandContext(ts.ctx, "node", "./sbot_client.js")
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
outrc, err := cmd.StdoutPipe()
|
||||||
|
r.NoError(err)
|
||||||
|
|
||||||
|
if name == "" {
|
||||||
|
name = fmt.Sprint(ts.t.Name(), jsBotCnt)
|
||||||
|
}
|
||||||
|
jsBotCnt++
|
||||||
|
env := []string{
|
||||||
|
"TEST_NAME=" + name,
|
||||||
|
"TEST_BOB=" + ts.gobot.Whoami().Ref(),
|
||||||
|
"TEST_GOADDR=" + netwrap.GetAddr(ts.gobot.Network.GetListenAddr(), "tcp").String(),
|
||||||
|
"TEST_BEFORE=" + writeFile(ts.t, jsbefore),
|
||||||
|
"TEST_AFTER=" + writeFile(ts.t, jsafter),
|
||||||
|
}
|
||||||
|
|
||||||
|
if ts.keySHS != nil {
|
||||||
|
env = append(env, "TEST_APPKEY="+base64.StdEncoding.EncodeToString(ts.keySHS))
|
||||||
|
}
|
||||||
|
if ts.keyHMAC != nil {
|
||||||
|
env = append(env, "TEST_HMACKEY="+base64.StdEncoding.EncodeToString(ts.keyHMAC))
|
||||||
|
}
|
||||||
|
cmd.Env = env
|
||||||
|
r.NoError(cmd.Start(), "failed to init test js-sbot")
|
||||||
|
|
||||||
|
ts.done.Go(cmd.Wait)
|
||||||
|
ts.t.Cleanup(func() {
|
||||||
|
cmd.Process.Kill()
|
||||||
|
})
|
||||||
|
|
||||||
|
pubScanner := bufio.NewScanner(outrc) // TODO muxrpc comms?
|
||||||
|
r.True(pubScanner.Scan(), "multiple lines of output from js - expected #1 to be %s pubkey/id", name)
|
||||||
|
|
||||||
|
jsBotRef, err := refs.ParseFeedRef(pubScanner.Text())
|
||||||
|
r.NoError(err, "failed to get %s key from JS process")
|
||||||
|
ts.t.Logf("JS %s:%d %s", name, jsBotCnt, jsBotRef.Ref())
|
||||||
|
return *jsBotRef
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *testSession) startJSBotAsServer(name, testScriptFileName string) (*refs.FeedRef, int) {
|
||||||
|
ts.t.Log("starting srv", name)
|
||||||
|
r := require.New(ts.t)
|
||||||
|
cmd := exec.CommandContext(ts.ctx, "node", "./sbot_serv.js")
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
outrc, err := cmd.StdoutPipe()
|
||||||
|
r.NoError(err)
|
||||||
|
|
||||||
|
if name == "" {
|
||||||
|
name = fmt.Sprintf("jsbot-%d", jsBotCnt)
|
||||||
|
}
|
||||||
|
jsBotCnt++
|
||||||
|
|
||||||
|
var port = 1024 + mrand.Intn(23000)
|
||||||
|
|
||||||
|
env := []string{
|
||||||
|
"TEST_NAME=" + filepath.Join(ts.t.Name(), "jsbot-"+name),
|
||||||
|
"TEST_BOB=" + ts.gobot.Whoami().Ref(),
|
||||||
|
fmt.Sprintf("TEST_PORT=%d", port),
|
||||||
|
"TEST_BEFORE=" + testScriptFileName,
|
||||||
|
}
|
||||||
|
// if ts.keySHS != nil {
|
||||||
|
// env = append(env, "TEST_APPKEY="+base64.StdEncoding.EncodeToString(ts.keySHS))
|
||||||
|
// }
|
||||||
|
cmd.Env = env
|
||||||
|
|
||||||
|
r.NoError(cmd.Start(), "failed to init test js-sbot")
|
||||||
|
|
||||||
|
ts.done.Go(cmd.Wait)
|
||||||
|
ts.t.Cleanup(func() {
|
||||||
|
cmd.Process.Kill()
|
||||||
|
})
|
||||||
|
|
||||||
|
pubScanner := bufio.NewScanner(outrc) // TODO muxrpc comms?
|
||||||
|
r.True(pubScanner.Scan(), "multiple lines of output from js - expected #1 to be %s pubkey/id", name)
|
||||||
|
|
||||||
|
srvRef, err := refs.ParseFeedRef(pubScanner.Text())
|
||||||
|
r.NoError(err, "failed to get srvRef key from JS process")
|
||||||
|
ts.t.Logf("JS %s: %s port: %d", name, srvRef.Ref(), port)
|
||||||
|
return srvRef, port
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *testSession) wait() {
|
||||||
|
closeErrc := make(chan error)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
time.Sleep(15 * time.Second) // would be nice to get -test.timeout for this
|
||||||
|
|
||||||
|
ts.gobot.Shutdown()
|
||||||
|
closeErrc <- ts.gobot.Close()
|
||||||
|
close(closeErrc)
|
||||||
|
}()
|
||||||
|
|
||||||
|
for err := range testutils.MergeErrorChans(append(ts.backgroundErrs, closeErrc)...) {
|
||||||
|
require.NoError(ts.t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(ts.t, ts.done.Wait())
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
module.exports = {
|
||||||
|
before: (sbot, ready) => {
|
||||||
|
console.warn('before:', sbot.id)
|
||||||
|
setTimeout(ready, 1000)
|
||||||
|
// ready()
|
||||||
|
},
|
||||||
|
|
||||||
|
after: (sbot, client) => {
|
||||||
|
console.warn('after:', sbot.id, client.id)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue