~samwhited/xmpp

7725b6c7d07eceac85b561eacfccdbc23c6209b9 — Sam Whited a month ago ab527bb
internal/integration: make options more flexible

This pulls some of the defaults out into options and only sets them if
the user does not apply these options. It also allows us to get the JID
directly from the command so that we don't have to create and parse a
JID, set a password, and make a user for every single test.

Signed-off-by: Sam Whited <sam@samwhited.com>
M internal/integration/ejabberd/ejabberd.go => internal/integration/ejabberd/ejabberd.go +85 -17
@@ 41,8 41,13 @@ func New(ctx context.Context, opts ...integration.Option) (*integration.Cmd, err

// ConfigFile is an option that can be used to write a temporary Ejabberd config
// file.
// This will overwrite the existing config file and make most of the other
// options in this package noops.
// This option only exists for the rare occasion that you need complete control
// over the config file.
func ConfigFile(cfg Config) integration.Option {
	return func(cmd *integration.Cmd) error {
		cmd.Config = cfg
		err := integration.TempFile(cfgFileName, func(cmd *integration.Cmd, w io.Writer) error {
			return cfgTmpl.Execute(w, struct {
				Config


@@ 67,6 72,64 @@ func ConfigFile(cfg Config) integration.Option {
	}
}

func getConfig(cmd *integration.Cmd) Config {
	if cmd.Config == nil {
		cmd.Config = Config{}
	}
	return cmd.Config.(Config)
}

// ListenC2S listens for client-to-server (c2s) connections on a Unix domain
// socket.
func ListenC2S() integration.Option {
	return func(cmd *integration.Cmd) error {
		c2sListener, err := cmd.C2SListen("unix", filepath.Join(cmd.ConfigDir(), "c2s.socket"))
		if err != nil {
			return err
		}
		c2sSocket := c2sListener.Addr().(*net.UnixAddr).Name
		err = c2sListener.Close()
		if err != nil {
			return err
		}

		cfg := getConfig(cmd)
		cfg.C2SSocket = c2sSocket
		cmd.Config = cfg
		return nil
	}
}

// ListenS2S listens for server-to-server (s2s) connections on a Unix domain
// socket.
func ListenS2S() integration.Option {
	return func(cmd *integration.Cmd) error {
		s2sListener, err := cmd.S2SListen("unix", filepath.Join(cmd.ConfigDir(), "s2s.socket"))
		if err != nil {
			return err
		}
		s2sSocket := s2sListener.Addr().(*net.UnixAddr).Name

		cfg := getConfig(cmd)
		cfg.S2SSocket = s2sSocket
		cmd.Config = cfg
		return nil
	}
}

// VHost configures one or more virtual hosts.
// The default if this option is not provided is to create a single vhost called
// "localhost" and create a self-signed cert for it (if VHost is specified certs
// must be manually created).
func VHost(hosts ...string) integration.Option {
	return func(cmd *integration.Cmd) error {
		cfg := getConfig(cmd)
		cfg.VHosts = append(cfg.VHosts, hosts...)
		cmd.Config = cfg
		return nil
	}
}

func defaultConfig(cmd *integration.Cmd) error {
	for _, arg := range cmd.Cmd.Args {
		if arg == configFlag {


@@ 74,24 137,24 @@ func defaultConfig(cmd *integration.Cmd) error {
		}
	}

	c2sListener, err := cmd.C2SListen("unix", filepath.Join(cmd.ConfigDir(), "c2s.socket"))
	if err != nil {
		return err
	cfg := getConfig(cmd)
	if len(cfg.VHosts) == 0 {
		const vhost = "localhost"
		cfg.VHosts = append(cfg.VHosts, vhost)
		err := integration.Cert(vhost)(cmd)
		if err != nil {
			return err
		}
	}
	c2sSocket := c2sListener.Addr().(*net.UnixAddr).Name

	s2sListener, err := cmd.S2SListen("unix", filepath.Join(cmd.ConfigDir(), "s2s.socket"))
	if err != nil {
		return err
	cmd.Config = cfg
	if j, _ := cmd.User(); j.Equal(jid.JID{}) {
		err := CreateUser(context.TODO(), "me@"+cfg.VHosts[0], "password")(cmd)
		if err != nil {
			return err
		}
	}
	s2sSocket := s2sListener.Addr().(*net.UnixAddr).Name

	// The config file didn't exist, so create a default config.
	return ConfigFile(Config{
		VHosts:    []string{"localhost"},
		C2SSocket: c2sSocket,
		S2SSocket: s2sSocket,
	})(cmd)
	return ConfigFile(cfg)(cmd)
}

func inetrcFile(cmd *integration.Cmd) error {


@@ 125,14 188,19 @@ func ctlFunc(ctx context.Context, args ...string) func(*integration.Cmd) error {

// CreateUser returns an option that calls ejabberdctl to create a user.
// It is equivalent to calling:
// Ctl(ctx, "register", "localpart", "domainpart", "password").
// Ctl(ctx, "register", "localpart", "domainpart", "password") except that it
// also configures the underlying Cmd to know about the user.
func CreateUser(ctx context.Context, addr, pass string) integration.Option {
	return func(cmd *integration.Cmd) error {
		j, err := jid.Parse(addr)
		if err != nil {
			return err
		}
		return Ctl(ctx, "register", j.Localpart(), j.Domainpart(), pass)(cmd)
		err = Ctl(ctx, "register", j.Localpart(), j.Domainpart(), pass)(cmd)
		if err != nil {
			return err
		}
		return integration.User(j, pass)(cmd)
	}
}


M internal/integration/integration.go => internal/integration/integration.go +37 -18
@@ 50,6 50,12 @@ type Cmd struct {
	c2sNetwork  string
	s2sNetwork  string
	shutdown    func(*Cmd) error
	user        jid.JID
	pass        string

	// Config is meant to be used by internal packages like prosody and ejabberd
	// to store their internal representation of the config before writing it out.
	Config interface{}
}

// New creates a new, unstarted, command.


@@ 64,16 70,21 @@ func New(ctx context.Context, name string, opts ...Option) (*Cmd, error) {
		name: name,
		kill: cancel,
	}
	var err error
	cmd.cfgDir, err = ioutil.TempDir("", cmd.name)
	if err != nil {
		return nil, err
	}
	for _, opt := range opts {
		err := opt(cmd)
		err = opt(cmd)
		if err != nil {
			return nil, err
			return nil, fmt.Errorf("error applying option: %v", err)
		}
	}
	for _, f := range cmd.cfgF {
		err := f()
		err = f()
		if err != nil {
			return nil, err
			return nil, fmt.Errorf("error running config func: %w", err)
		}
	}



@@ 122,15 133,19 @@ func (cmd *Cmd) Close() error {
	if cmd.shutdown != nil {
		e = cmd.shutdown(cmd)
	}
	if cmd.cfgDir != "" {
		err := os.RemoveAll(cmd.cfgDir)
		if err != nil {
			return err
		}
	err := os.RemoveAll(cmd.cfgDir)
	if err != nil {
		return err
	}
	return e
}

// User returns the address and password of a user created on the server (if
// any).
func (cmd *Cmd) User() (jid.JID, string) {
	return cmd.user, cmd.pass
}

// DialClient attempts to connect to the server with a client-to-server (c2s)
// connection by dialing the address reserved by C2SListen and then negotiating
// a stream with the location set to the domainpart of j and the origin set to


@@ 149,9 164,9 @@ func (cmd *Cmd) DialServer(ctx context.Context, location, origin jid.JID, t *tes
func (cmd *Cmd) dial(ctx context.Context, s2s bool, location, origin jid.JID, t *testing.T, features ...xmpp.StreamFeature) (*xmpp.Session, error) {
	switch {
	case s2s && cmd.s2sListener == nil:
		return nil, errors.New("s2s not configured, please call S2SListen first")
		return nil, errors.New("s2s not configured, please configure an s2s listener")
	case !s2s && cmd.c2sListener == nil:
		return nil, errors.New("c2s not configured, please call C2SListen first")
		return nil, errors.New("c2s not configured, please configure a c2s listener")
	}

	addr := cmd.c2sListener.Addr().String()


@@ 187,6 202,16 @@ func (cmd *Cmd) dial(ctx context.Context, s2s bool, location, origin jid.JID, t 
// Option is used to configure a Cmd.
type Option func(cmd *Cmd) error

// User sets the values that will be returned by a call to cmd.User later. It
// does not actually create a user.
func User(user jid.JID, pass string) Option {
	return func(cmd *Cmd) error {
		cmd.user = user
		cmd.pass = pass
		return nil
	}
}

// Shutdown is run before the configuration is removed and is meant to
// gracefully shutdown the application in case it does not handle the kill
// signal correctly.


@@ 245,12 270,6 @@ func Cert(name string) Option {
// files.
func TempFile(cfgFileName string, f func(*Cmd, io.Writer) error) Option {
	return func(cmd *Cmd) (err error) {
		if cmd.cfgDir == "" {
			cmd.cfgDir, err = ioutil.TempDir("", cmd.name)
			if err != nil {
				return err
			}
		}
		dir := filepath.Dir(cfgFileName)
		if dir != "" && dir != "." && dir != "/" && dir != ".." {
			err = os.MkdirAll(filepath.Join(cmd.cfgDir, dir), 0700)


@@ 326,7 345,7 @@ func LogXML() Option {
	}
}

// Defer is an option that calls f the command is started.
// Defer is an option that calls f after the command is started.
func Defer(f func(*Cmd) error) Option {
	return func(cmd *Cmd) error {
		cmd.deferF = append(cmd.deferF, f)

M internal/integration/prosody/prosody.go => internal/integration/prosody/prosody.go +101 -24
@@ 36,8 36,13 @@ func New(ctx context.Context, opts ...integration.Option) (*integration.Cmd, err

// ConfigFile is an option that can be used to write a temporary Prosody config
// file.
// This will overwrite the existing config file and make most of the other
// options in this package noops.
// This option only exists for the rare occasion that you need complete control
// over the config file.
func ConfigFile(cfg Config) integration.Option {
	return func(cmd *integration.Cmd) error {
		cmd.Config = cfg
		err := integration.TempFile(cfgFileName, func(cmd *integration.Cmd, w io.Writer) error {
			return cfgTmpl.Execute(w, struct {
				Config


@@ 69,16 74,93 @@ func Ctl(ctx context.Context, args ...string) integration.Option {
	})
}

func getConfig(cmd *integration.Cmd) Config {
	if cmd.Config == nil {
		cmd.Config = Config{}
	}
	return cmd.Config.(Config)
}

// ListenC2S listens for client-to-server (c2s) connections on a random port.
func ListenC2S() integration.Option {
	return func(cmd *integration.Cmd) error {
		c2sListener, err := cmd.C2SListen("tcp", "[::1]:0")
		if err != nil {
			return err
		}
		// Prosody creates its own sockets and doesn't provide us with a way of
		// pointing it at an existing Unix domain socket or handing the filehandle for
		// the TCP connection to it on start, so we're effectively just listening to
		// get a random port that we'll use to configure Prosody, then we need to
		// close the connection and let Prosody listen on that port.
		// Technically this is racey, but it's not likely to be a problem in practice.
		c2sPort := c2sListener.Addr().(*net.TCPAddr).Port
		err = c2sListener.Close()
		if err != nil {
			return err
		}

		cfg := getConfig(cmd)
		cfg.C2SPort = c2sPort
		cmd.Config = cfg
		return nil
	}
}

// ListenS2S listens for server-to-server (s2s) connections on a random port.
func ListenS2S() integration.Option {
	return func(cmd *integration.Cmd) error {
		s2sListener, err := cmd.S2SListen("tcp", "[::1]:0")
		if err != nil {
			return err
		}
		// Prosody creates its own sockets and doesn't provide us with a way of
		// pointing it at an existing Unix domain socket or handing the filehandle for
		// the TCP connection to it on start, so we're effectively just listening to
		// get a random port that we'll use to configure Prosody, then we need to
		// close the connection and let Prosody listen on that port.
		// Technically this is racey, but it's not likely to be a problem in practice.
		s2sPort := s2sListener.Addr().(*net.TCPAddr).Port
		err = s2sListener.Close()
		if err != nil {
			return err
		}

		cfg := getConfig(cmd)
		cfg.S2SPort = s2sPort
		cmd.Config = cfg
		return nil
	}
}

// VHost configures one or more virtual hosts.
// The default if this option is not provided is to create a single vhost called
// "localhost" and create a self-signed cert for it (if VHost is specified certs
// must be manually created).
func VHost(hosts ...string) integration.Option {
	return func(cmd *integration.Cmd) error {
		cfg := getConfig(cmd)
		cfg.VHosts = append(cfg.VHosts, hosts...)
		cmd.Config = cfg
		return nil
	}
}

// CreateUser returns an option that calls prosodyctl to create a user.
// It is equivalent to calling:
// Ctl(ctx, "register", "localpart", "domainpart", "password").
// Ctl(ctx, "register", "localpart", "domainpart", "password") except that it
// also configures the underlying Cmd to know about the user.
func CreateUser(ctx context.Context, addr, pass string) integration.Option {
	return func(cmd *integration.Cmd) error {
		j, err := jid.Parse(addr)
		if err != nil {
			return err
		}
		return Ctl(ctx, "register", j.Localpart(), j.Domainpart(), pass)(cmd)
		err = Ctl(ctx, "register", j.Localpart(), j.Domainpart(), pass)(cmd)
		if err != nil {
			return err
		}
		return integration.User(j, pass)(cmd)
	}
}



@@ 88,30 170,25 @@ func defaultConfig(cmd *integration.Cmd) error {
			return nil
		}
	}
	c2sListener, err := cmd.C2SListen("tcp", "[::1]:0")
	if err != nil {
		return err

	cfg := getConfig(cmd)
	if len(cfg.VHosts) == 0 {
		const vhost = "localhost"
		cfg.VHosts = append(cfg.VHosts, vhost)
		err := integration.Cert(vhost)(cmd)
		if err != nil {
			return err
		}
	}
	// Prosody creates its own sockets and doesn't provide us with a way of
	// pointing it at an existing Unix domain socket or handing the filehandle for
	// the TCP connection to it on start, so we're effectively just listening to
	// get a random port that we'll use to configure Prosody, then we need to
	// close the connection and let Prosody listen on that port.
	// Technically this is racey, but it's not likely to be a problem in practice.
	defer c2sListener.Close()

	s2sListener, err := cmd.S2SListen("tcp", "[::1]:0")
	if err != nil {
		return err
	cmd.Config = cfg
	if j, _ := cmd.User(); j.Equal(jid.JID{}) {
		err := CreateUser(context.TODO(), "me@"+cfg.VHosts[0], "password")(cmd)
		if err != nil {
			return err
		}
	}
	defer s2sListener.Close()

	// The config file didn't exist, so create a default config.
	return ConfigFile(Config{
		VHosts:  []string{"localhost"},
		C2SPort: c2sListener.Addr().(*net.TCPAddr).Port,
		S2SPort: s2sListener.Addr().(*net.TCPAddr).Port,
	})(cmd)

	return ConfigFile(cfg)(cmd)
}

// Test starts a Prosody instance and returns a function that runs subtests

M ping/integration_test.go => ping/integration_test.go +25 -26
@@ 15,38 15,37 @@ import (
	"mellium.im/xmpp"
	"mellium.im/xmpp/internal/integration"
	"mellium.im/xmpp/internal/integration/prosody"
	"mellium.im/xmpp/jid"
	"mellium.im/xmpp/ping"
)

func TestIntegrationSendPing(t *testing.T) {
	j := jid.MustParse("me@localhost")
	const pass = "password"
	run := prosody.Test(context.TODO(), t,
	prosodyRun := prosody.Test(context.TODO(), t,
		integration.Log(),
		integration.Cert("localhost"),
		prosody.CreateUser(context.TODO(), j.String(), pass),
		prosody.ListenC2S(),
	)
	run(func(ctx context.Context, t *testing.T, cmd *integration.Cmd) {
		session, err := cmd.DialClient(ctx, j, t,
			xmpp.StartTLS(&tls.Config{
				InsecureSkipVerify: true,
			}),
			xmpp.SASL("", pass, sasl.Plain),
			xmpp.BindResource(),
		)
		if err != nil {
			t.Fatalf("error connecting: %v", err)
		}
		go func() {
			err := session.Serve(nil)
			if err != nil {
				t.Logf("error from serve: %v", err)
			}
		}()
		err = ping.Send(context.TODO(), session, session.RemoteAddr())
	prosodyRun(integrationSendPing)
}

func integrationSendPing(ctx context.Context, t *testing.T, cmd *integration.Cmd) {
	j, pass := cmd.User()
	session, err := cmd.DialClient(ctx, j, t,
		xmpp.StartTLS(&tls.Config{
			InsecureSkipVerify: true,
		}),
		xmpp.SASL("", pass, sasl.Plain),
		xmpp.BindResource(),
	)
	if err != nil {
		t.Fatalf("error connecting: %v", err)
	}
	go func() {
		err := session.Serve(nil)
		if err != nil {
			t.Errorf("error pinging: %v", err)
			t.Logf("error from serve: %v", err)
		}
	})
	}()
	err = ping.Send(context.TODO(), session, session.RemoteAddr())
	if err != nil {
		t.Errorf("error pinging: %v", err)
	}
}