go-ssb-room/muxrpc/test/nodejs/setup_test.go

289 lines
7.7 KiB
Go

// SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021
//
// SPDX-License-Identifier: MIT
// Package nodejs_test contains test scenarios and helpers to run interoparability tests against the javascript implementation.
package nodejs_test
import (
"bufio"
"context"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"io"
mrand "math/rand"
"net"
"os"
"os/exec"
"path/filepath"
"testing"
"time"
"github.com/ssbc/go-muxrpc/v2/debug"
"github.com/ssbc/go-netwrap"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.mindeco.de/log"
"golang.org/x/sync/errgroup"
refs "github.com/ssbc/go-ssb-refs"
"github.com/ssbc/go-ssb-room/v2/internal/maybemod/testutils"
"github.com/ssbc/go-ssb-room/v2/internal/network"
"github.com/ssbc/go-ssb-room/v2/internal/signinwithssb"
"github.com/ssbc/go-ssb-room/v2/roomdb"
"github.com/ssbc/go-ssb-room/v2/roomdb/mockdb"
"github.com/ssbc/go-ssb-room/v2/roomsrv"
)
func init() {
err := os.RemoveAll("testrun")
if err != nil {
fmt.Println("failed to clean testrun dir")
panic(err)
}
}
type testSession struct {
t *testing.T
info log.Logger
repo string
keySHS []byte
done errgroup.Group
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)
return newSession(t, appKey)
}
// 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 []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,
}
// todo: hook into deadline
ts.ctx, ts.cancel = context.WithCancel(context.Background())
return ts
}
func (ts *testSession) startGoServer(
membersDB roomdb.MembersService,
aliasDB roomdb.AliasesService,
opts ...roomsrv.Option,
) *roomsrv.Server {
r := require.New(ts.t)
netInfo := network.ServerEndpointDetails{
Domain: "go.test.room.server",
ListenAddressMUXRPC: "localhost:0",
}
// prepend defaults
opts = append([]roomsrv.Option{
roomsrv.WithLogger(ts.info),
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) {
ref, err := network.GetFeedRefFromAddr(conn.RemoteAddr())
if err != nil {
return nil, err
}
fname := filepath.Join("testrun", ts.t.Name(), "muxdump", ref.ShortSigil())
return debug.WrapDump(fname, conn)
}),
)
// not needed for testing yet
sb := signinwithssb.NewSignalBridge()
authSessionsDB := new(mockdb.FakeAuthWithSSBService)
fakeConfig := new(mockdb.FakeRoomConfig)
deniedKeysDB := new(mockdb.FakeDeniedKeysService)
srv, err := roomsrv.New(membersDB, deniedKeysDB, aliasDB, authSessionsDB, sb, fakeConfig, netInfo, opts...)
r.NoError(err, "failed to init tees a server")
ts.t.Logf("go server: %s", srv.Whoami().String())
ts.t.Cleanup(func() {
ts.t.Log("bot close:", srv.Close())
})
ts.done.Go(func() error {
err := srv.Network.Serve(ts.ctx)
// if the muxrpc protocol fucks up by e.g. unpacking body data into a header, this type of error will be surfaced here and look scary in the test output
// example: https://github.com/ssbc/go-ssb-room/pull/85#issuecomment-801106687
if err != nil && !errors.Is(err, context.Canceled) {
err = fmt.Errorf("go server exited: %w", err)
ts.t.Log(err)
return err
}
return nil
})
return srv
}
var jsBotCnt = 0
// starts a node process in the client role. returns the jsbots pubkey
func (ts *testSession) startJSClient(
name,
testScript string,
// the perr the client should connect to at first (here usually the room server)
peerAddr net.Addr,
peerRef refs.FeedRef,
) 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.Sprintf("jsbot-%d", jsBotCnt)
}
jsBotCnt++
// copy test scripts (maybe later with templates if we need to)
cmd.Dir = filepath.Join("testrun", ts.t.Name(), name)
os.MkdirAll(cmd.Dir, 0700)
err = exec.Command("cp", "-r", "testscripts", cmd.Dir).Run()
r.NoError(err)
env := []string{
"TEST_NAME=" + name,
"TEST_REPO=" + cmd.Dir,
"TEST_PEERADDR=" + netwrap.GetAddr(peerAddr, "tcp").String(),
"TEST_PEERREF=" + peerRef.String(),
"TEST_SESSIONSCRIPT=" + testScript,
// "DEBUG=ssb:room:tunnel:*",
}
if ts.keySHS != nil {
env = append(env, "TEST_APPKEY="+base64.StdEncoding.EncodeToString(ts.keySHS))
}
cmd.Env = env
started := time.Now()
r.NoError(cmd.Start(), "failed to init test js-sbot")
ts.done.Go(func() error {
err := cmd.Wait()
ts.t.Logf("node client %s: exited with %v (after %s)", name, err, time.Since(started))
// we need to return the error code to have an idea if any of the tape assertions failed
if err != nil {
return fmt.Errorf("node client %s exited with %s", name, err)
}
return nil
})
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)
go io.Copy(os.Stderr, outrc) // restore node stdout to stderr behavior
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.String())
return jsBotRef
}
// startJSBotAsServer returns the servers public key and it's TCP port on localhost.
// This is only here to check compliance against the old javascript server.
// We don't care so much about it's internal behavior, just that clients can connect through it.
func (ts *testSession) startJSBotAsServer(name, testScriptFileName string) (*refs.FeedRef, int) {
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++
// copy test scripts (maybe later with templates if we need to)
cmd.Dir = filepath.Join("testrun", ts.t.Name(), name)
os.MkdirAll(cmd.Dir, 0700)
err = exec.Command("cp", "-r", "testscripts", cmd.Dir).Run()
r.NoError(err)
var port = 1024 + mrand.Intn(23000)
env := []string{
"TEST_NAME=jsbot-" + name,
"TEST_REPO=" + cmd.Dir,
fmt.Sprintf("TEST_PORT=%d", port),
"TEST_SESSIONSCRIPT=" + testScriptFileName,
// "DEBUG=ssb:room:tunnel:*",
}
if ts.keySHS != nil {
env = append(env, "TEST_APPKEY="+base64.StdEncoding.EncodeToString(ts.keySHS))
}
cmd.Env = env
started := time.Now()
r.NoError(cmd.Start(), "failed to init test js-sbot")
ts.done.Go(func() error {
err := cmd.Wait()
ts.t.Logf("node server %s: exited with %v (after %s)", name, err, time.Since(started))
return nil
})
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.String(), port)
return &srvRef, port
}
func (ts *testSession) wait() {
ts.cancel()
assert.NoError(ts.t, ts.done.Wait())
}