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)
+ }
}