github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/systests/multiuser_common_test.go (about)

     1  package systests
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"testing"
     8  	"time"
     9  
    10  	client "github.com/keybase/client/go/client"
    11  	engine "github.com/keybase/client/go/engine"
    12  	libkb "github.com/keybase/client/go/libkb"
    13  	logger "github.com/keybase/client/go/logger"
    14  	chat1 "github.com/keybase/client/go/protocol/chat1"
    15  	keybase1 "github.com/keybase/client/go/protocol/keybase1"
    16  	service "github.com/keybase/client/go/service"
    17  	teams "github.com/keybase/client/go/teams"
    18  	clockwork "github.com/keybase/clockwork"
    19  	rpc "github.com/keybase/go-framed-msgpack-rpc/rpc"
    20  	"github.com/stretchr/testify/require"
    21  	contextOld "golang.org/x/net/context"
    22  )
    23  
    24  // Tests for systests with multiuser, multidevice situations.
    25  // The abbreviation "smu" means "Systests Multi User".  So
    26  // smuUser is a 'Systests Multi User User"'
    27  
    28  type smuUser struct {
    29  	ctx            *smuContext
    30  	devices        []*smuDeviceWrapper
    31  	backupKeys     []backupKey
    32  	usernamePrefix string
    33  	username       string
    34  	userInfo       *signupInfo
    35  	primary        *smuDeviceWrapper
    36  	notifications  *teamNotifyHandler
    37  }
    38  
    39  type smuContext struct {
    40  	t         *testing.T
    41  	log       logger.Logger
    42  	fakeClock clockwork.FakeClock
    43  	users     map[string](*smuUser)
    44  }
    45  
    46  func newSMUContext(t *testing.T) *smuContext {
    47  	ret := &smuContext{
    48  		t:         t,
    49  		users:     make(map[string](*smuUser)),
    50  		fakeClock: clockwork.NewFakeClockAt(time.Now()),
    51  	}
    52  	return ret
    53  }
    54  
    55  func (smc *smuContext) cleanup() {
    56  	for _, v := range smc.users {
    57  		v.cleanup()
    58  	}
    59  }
    60  
    61  func (u *smuUser) cleanup() {
    62  	if u == nil {
    63  		return
    64  	}
    65  	for _, d := range u.devices {
    66  		d.tctx.Cleanup()
    67  		if d.service != nil {
    68  			d.service.Stop(0)
    69  			err := d.stop()
    70  			require.NoError(d.tctx.T, err)
    71  		}
    72  		for _, cl := range d.clones {
    73  			cl.Cleanup()
    74  		}
    75  		for _, cl := range d.usedClones {
    76  			cl.Cleanup()
    77  		}
    78  	}
    79  }
    80  
    81  // smuDeviceWrapper wraps a mock "device", meaning an independent running service and
    82  // some connected clients.
    83  type smuDeviceWrapper struct {
    84  	ctx                *smuContext
    85  	tctx               *libkb.TestContext
    86  	clones, usedClones []*libkb.TestContext
    87  	deviceKey          keybase1.PublicKey
    88  	stopCh             chan error
    89  	service            *service.Service
    90  	cli                rpc.GenericClient
    91  	xp                 rpc.Transporter
    92  }
    93  
    94  func (d *smuDeviceWrapper) KID() keybase1.KID {
    95  	return d.deviceKey.KID
    96  }
    97  
    98  func (d *smuDeviceWrapper) startService(numClones int) {
    99  	for i := 0; i < numClones; i++ {
   100  		d.clones = append(d.clones, cloneContext(d.tctx))
   101  	}
   102  	d.stopCh = make(chan error)
   103  	svc := service.NewService(d.tctx.G, false)
   104  	d.service = svc
   105  	startCh := svc.GetStartChannel()
   106  	go func() {
   107  		d.stopCh <- svc.Run()
   108  	}()
   109  	<-startCh
   110  }
   111  
   112  func (d *smuDeviceWrapper) stop() error {
   113  	return <-d.stopCh
   114  }
   115  
   116  func (d *smuDeviceWrapper) clearUPAKCache() {
   117  	_, err := d.tctx.G.LocalDb.Nuke()
   118  	require.NoError(d.tctx.T, err)
   119  	d.tctx.G.GetUPAKLoader().ClearMemory()
   120  }
   121  
   122  type smuTerminalUI struct{}
   123  
   124  func (t smuTerminalUI) ErrorWriter() io.Writer                                        { return nil }
   125  func (t smuTerminalUI) Output(string) error                                           { return nil }
   126  func (t smuTerminalUI) OutputDesc(libkb.OutputDescriptor, string) error               { return nil }
   127  func (t smuTerminalUI) OutputWriter() io.Writer                                       { return nil }
   128  func (t smuTerminalUI) UnescapedOutputWriter() io.Writer                              { return nil }
   129  func (t smuTerminalUI) Printf(fmt string, args ...interface{}) (int, error)           { return 0, nil }
   130  func (t smuTerminalUI) PrintfUnescaped(fmt string, args ...interface{}) (int, error)  { return 0, nil }
   131  func (t smuTerminalUI) Prompt(libkb.PromptDescriptor, string) (string, error)         { return "", nil }
   132  func (t smuTerminalUI) PromptForConfirmation(prompt string) error                     { return nil }
   133  func (t smuTerminalUI) PromptPassword(libkb.PromptDescriptor, string) (string, error) { return "", nil }
   134  func (t smuTerminalUI) PromptPasswordMaybeScripted(libkb.PromptDescriptor, string) (string, error) {
   135  	return "", nil
   136  }
   137  func (t smuTerminalUI) PromptYesNo(libkb.PromptDescriptor, string, libkb.PromptDefault) (bool, error) {
   138  	return false, nil
   139  }
   140  func (t smuTerminalUI) Tablify(headings []string, rowfunc func() []string) {}
   141  func (t smuTerminalUI) TerminalSize() (width int, height int)              { return }
   142  
   143  type signupInfoSecretUI struct {
   144  	signupInfo *signupInfo
   145  	log        logger.Logger
   146  }
   147  
   148  func (s signupInfoSecretUI) GetPassphrase(p keybase1.GUIEntryArg, terminal *keybase1.SecretEntryArg) (res keybase1.GetPassphraseRes, err error) {
   149  	if p.Type == keybase1.PassphraseType_PAPER_KEY {
   150  		res.Passphrase = s.signupInfo.displayedPaperKey
   151  	} else {
   152  		res.Passphrase = s.signupInfo.passphrase
   153  	}
   154  	s.log.Debug("| GetPassphrase: %v -> %v", p, res)
   155  	return res, err
   156  }
   157  
   158  type usernameLoginUI struct {
   159  	username string
   160  }
   161  
   162  var _ libkb.LoginUI = (*usernameLoginUI)(nil)
   163  
   164  func (s usernameLoginUI) GetEmailOrUsername(contextOld.Context, int) (string, error) {
   165  	return s.username, nil
   166  }
   167  func (s usernameLoginUI) PromptRevokePaperKeys(contextOld.Context, keybase1.PromptRevokePaperKeysArg) (ret bool, err error) {
   168  	return false, nil
   169  }
   170  func (s usernameLoginUI) DisplayPaperKeyPhrase(contextOld.Context, keybase1.DisplayPaperKeyPhraseArg) error {
   171  	return nil
   172  }
   173  func (s usernameLoginUI) DisplayPrimaryPaperKey(contextOld.Context, keybase1.DisplayPrimaryPaperKeyArg) error {
   174  	return nil
   175  }
   176  func (s usernameLoginUI) PromptResetAccount(_ context.Context, arg keybase1.PromptResetAccountArg) (keybase1.ResetPromptResponse, error) {
   177  	return keybase1.ResetPromptResponse_NOTHING, nil
   178  }
   179  func (s usernameLoginUI) DisplayResetProgress(_ context.Context, arg keybase1.DisplayResetProgressArg) error {
   180  	return nil
   181  }
   182  func (s usernameLoginUI) ExplainDeviceRecovery(_ context.Context, arg keybase1.ExplainDeviceRecoveryArg) error {
   183  	return nil
   184  }
   185  func (s usernameLoginUI) PromptPassphraseRecovery(_ context.Context, arg keybase1.PromptPassphraseRecoveryArg) (bool, error) {
   186  	return false, nil
   187  }
   188  func (s usernameLoginUI) ChooseDeviceToRecoverWith(_ context.Context, arg keybase1.ChooseDeviceToRecoverWithArg) (keybase1.DeviceID, error) {
   189  	return "", nil
   190  }
   191  func (s usernameLoginUI) DisplayResetMessage(_ context.Context, arg keybase1.DisplayResetMessageArg) error {
   192  	return nil
   193  }
   194  
   195  func (d *smuDeviceWrapper) popClone() *libkb.TestContext {
   196  	if len(d.clones) == 0 {
   197  		panic("ran out of cloned environments")
   198  	}
   199  	ret := d.clones[0]
   200  	d.clones = d.clones[1:]
   201  	// Hold a reference to this clone for cleanup
   202  	d.usedClones = append(d.usedClones, ret)
   203  	ui := genericUI{
   204  		g:          ret.G,
   205  		TerminalUI: smuTerminalUI{},
   206  	}
   207  	ret.G.SetUI(&ui)
   208  	return ret
   209  }
   210  
   211  func (smc *smuContext) setupDeviceHelper(u *smuUser, puk bool) *smuDeviceWrapper {
   212  	tctx := setupTest(smc.t, u.usernamePrefix)
   213  	tctx.Tp.DisableUpgradePerUserKey = !puk
   214  	tctx.G.SetClock(smc.fakeClock)
   215  	ret := &smuDeviceWrapper{ctx: smc, tctx: tctx}
   216  	u.devices = append(u.devices, ret)
   217  	if u.primary == nil {
   218  		u.primary = ret
   219  	}
   220  	if smc.log == nil {
   221  		smc.log = tctx.G.Log
   222  	}
   223  	return ret
   224  }
   225  
   226  func (smc *smuContext) installKeybaseForUser(usernamePrefix string, numClones int) *smuUser {
   227  	user := &smuUser{ctx: smc, usernamePrefix: usernamePrefix}
   228  	smc.users[usernamePrefix] = user
   229  	smc.newDevice(user, numClones)
   230  	return user
   231  }
   232  
   233  func (smc *smuContext) installKeybaseForUserNoPUK(usernamePrefix string, numClones int) *smuUser {
   234  	user := &smuUser{ctx: smc, usernamePrefix: usernamePrefix}
   235  	smc.users[usernamePrefix] = user
   236  	smc.newDevice(user, numClones)
   237  	return user
   238  }
   239  
   240  func (smc *smuContext) newDevice(u *smuUser, numClones int) *smuDeviceWrapper {
   241  	return smc.newDeviceHelper(u, numClones, true)
   242  }
   243  
   244  func (smc *smuContext) newDeviceHelper(u *smuUser, numClones int, puk bool) *smuDeviceWrapper {
   245  	ret := smc.setupDeviceHelper(u, puk)
   246  	ret.startService(numClones)
   247  	ret.startClient()
   248  	return ret
   249  }
   250  
   251  func (u *smuUser) primaryDevice() *smuDeviceWrapper {
   252  	return u.primary
   253  }
   254  
   255  func (d *smuDeviceWrapper) userClient() keybase1.UserClient {
   256  	return keybase1.UserClient{Cli: d.cli}
   257  }
   258  
   259  func (d *smuDeviceWrapper) ctlClient() keybase1.CtlClient {
   260  	return keybase1.CtlClient{Cli: d.cli}
   261  }
   262  
   263  func (d *smuDeviceWrapper) rpcClient() rpc.GenericClient {
   264  	return d.cli
   265  }
   266  
   267  func (d *smuDeviceWrapper) transport() rpc.Transporter {
   268  	return d.xp
   269  }
   270  
   271  func (d *smuDeviceWrapper) startClient() {
   272  	var err error
   273  	tctx := d.popClone()
   274  	d.cli, d.xp, err = client.GetRPCClientWithContext(tctx.G)
   275  	if err != nil {
   276  		d.ctx.t.Fatal(err)
   277  	}
   278  }
   279  
   280  func (d *smuDeviceWrapper) loadEncryptionKIDs() (devices []keybase1.KID, backups []backupKey) {
   281  	keyMap := make(map[keybase1.KID]keybase1.PublicKey)
   282  	keys, err := d.userClient().LoadMyPublicKeys(context.TODO(), 0)
   283  	if err != nil {
   284  		d.ctx.t.Fatalf("Failed to LoadMyPublicKeys: %s", err)
   285  	}
   286  	for _, key := range keys {
   287  		keyMap[key.KID] = key
   288  	}
   289  
   290  	for _, key := range keys {
   291  		if key.IsSibkey {
   292  			continue
   293  		}
   294  		parent, found := keyMap[keybase1.KID(key.ParentID)]
   295  		if !found {
   296  			continue
   297  		}
   298  
   299  		switch parent.DeviceType {
   300  		case keybase1.DeviceTypeV2_PAPER:
   301  			backups = append(backups, backupKey{KID: key.KID, deviceID: parent.DeviceID})
   302  		case keybase1.DeviceTypeV2_DESKTOP:
   303  			devices = append(devices, key.KID)
   304  		default:
   305  		}
   306  	}
   307  	return devices, backups
   308  }
   309  
   310  func (u *smuUser) signup() {
   311  	u.signupHelper(true, false)
   312  }
   313  
   314  func (u *smuUser) signupNoPUK() {
   315  	u.signupHelper(false, false)
   316  }
   317  
   318  func (u *smuUser) signupHelper(puk, paper bool) {
   319  	ctx := u.ctx
   320  	userInfo := randomUser(u.usernamePrefix)
   321  	u.userInfo = userInfo
   322  	dw := u.primaryDevice()
   323  	tctx := dw.popClone()
   324  	tctx.Tp.DisableUpgradePerUserKey = !puk
   325  	g := tctx.G
   326  	signupUI := signupUI{
   327  		info:         userInfo,
   328  		Contextified: libkb.NewContextified(g),
   329  	}
   330  	g.SetUI(&signupUI)
   331  	signup := client.NewCmdSignupRunner(g)
   332  	signup.SetTestWithPaper(paper)
   333  	if err := signup.Run(); err != nil {
   334  		ctx.t.Fatal(err)
   335  	}
   336  	ctx.t.Logf("signed up %s", userInfo.username)
   337  	u.username = userInfo.username
   338  	var backupKey backupKey
   339  	devices, backups := dw.loadEncryptionKIDs()
   340  	if len(devices) != 1 {
   341  		ctx.t.Fatalf("Expected 1 device back; got %d", len(devices))
   342  	}
   343  	dw.deviceKey.KID = devices[0]
   344  	if paper {
   345  		if len(backups) != 1 {
   346  			ctx.t.Fatalf("Expected 1 backup back; got %d", len(backups))
   347  		}
   348  		backupKey = backups[0]
   349  		backupKey.secret = signupUI.info.displayedPaperKey
   350  		u.backupKeys = append(u.backupKeys, backupKey)
   351  	}
   352  
   353  	// Reconfigure config subsystem in Primary Global Context and also
   354  	// in all clones. This has to be done after signup because the
   355  	// username changes, and so does config filename.
   356  	err := dw.tctx.G.ConfigureConfig()
   357  	require.NoError(ctx.t, err)
   358  	for _, clone := range dw.clones {
   359  		err = clone.G.ConfigureConfig()
   360  		require.NoError(ctx.t, err)
   361  	}
   362  }
   363  
   364  func (u *smuUser) perUserKeyUpgrade() error {
   365  	g := u.getPrimaryGlobalContext()
   366  	arg := &engine.PerUserKeyUpgradeArgs{}
   367  	eng := engine.NewPerUserKeyUpgrade(g, arg)
   368  	uis := libkb.UIs{
   369  		LogUI: g.UI.GetLogUI(),
   370  	}
   371  	m := libkb.NewMetaContextTODO(g).WithUIs(uis)
   372  	err := engine.RunEngine2(m, eng)
   373  	return err
   374  }
   375  
   376  type smuTeam struct {
   377  	ID   keybase1.TeamID
   378  	name string
   379  }
   380  
   381  type smuImplicitTeam struct {
   382  	ID keybase1.TeamID
   383  }
   384  
   385  func (u *smuUser) registerForNotifications() {
   386  	u.notifications = newTeamNotifyHandler()
   387  	srv := rpc.NewServer(u.primaryDevice().transport(), nil)
   388  	if err := srv.Register(keybase1.NotifyTeamProtocol(u.notifications)); err != nil {
   389  		u.ctx.t.Fatal(err)
   390  	}
   391  	ncli := keybase1.NotifyCtlClient{Cli: u.primaryDevice().rpcClient()}
   392  	if err := ncli.SetNotifications(context.TODO(), keybase1.NotificationChannels{Team: true}); err != nil {
   393  		u.ctx.t.Fatal(err)
   394  	}
   395  }
   396  
   397  // nolint
   398  func (u *smuUser) waitForNewlyAddedToTeamByID(teamID keybase1.TeamID) {
   399  	u.ctx.t.Logf("waiting for newly added to team %s", teamID)
   400  
   401  	// process 10 team rotations or 10s worth of time
   402  	for i := 0; i < 10; i++ {
   403  		select {
   404  		case tid := <-u.notifications.newlyAddedToTeam:
   405  			u.ctx.t.Logf("team newly added notification received: %v", tid)
   406  			if tid.Eq(teamID) {
   407  				u.ctx.t.Logf("notification matched!")
   408  				return
   409  			}
   410  			u.ctx.t.Logf("ignoring newly added message (expected teamID = %q)", teamID)
   411  		case <-time.After(1 * time.Second * libkb.CITimeMultiplier(u.getPrimaryGlobalContext())):
   412  		}
   413  	}
   414  	u.ctx.t.Fatalf("timed out waiting for team newly added %s", teamID)
   415  }
   416  
   417  func (u *smuUser) waitForTeamAbandoned(teamID keybase1.TeamID) {
   418  	u.ctx.t.Logf("waiting for team abandoned %s", teamID)
   419  
   420  	// process 10 team rotations or 10s worth of time
   421  	for i := 0; i < 10; i++ {
   422  		select {
   423  		case abandonID := <-u.notifications.abandonCh:
   424  			u.ctx.t.Logf("team abandon notification received: %v", abandonID)
   425  			if abandonID.Eq(teamID) {
   426  				u.ctx.t.Logf("abandon matched!")
   427  				return
   428  			}
   429  			u.ctx.t.Logf("ignoring abandon message (expected teamID = %q)", teamID)
   430  		case <-time.After(1 * time.Second * libkb.CITimeMultiplier(u.getPrimaryGlobalContext())):
   431  		}
   432  	}
   433  	u.ctx.t.Fatalf("timed out waiting for team abandon %s", teamID)
   434  }
   435  
   436  func (u *smuUser) getTeamsClient() keybase1.TeamsClient {
   437  	return keybase1.TeamsClient{Cli: u.primaryDevice().rpcClient()}
   438  }
   439  
   440  func (u *smuUser) pollForMembershipUpdate(team smuTeam, keyGen keybase1.PerTeamKeyGeneration,
   441  	poller func(d keybase1.TeamDetails) bool) keybase1.TeamDetails {
   442  	wait := 100 * time.Millisecond
   443  	var totalWait time.Duration
   444  	i := 0
   445  	for {
   446  		cli := u.getTeamsClient()
   447  		details, err := cli.TeamGet(context.TODO(), keybase1.TeamGetArg{Name: team.name})
   448  		if err != nil {
   449  			u.ctx.t.Fatal(err)
   450  		}
   451  		// If the caller specified a "poller" that means we should keep polling until
   452  		// the predicate turns true
   453  		if details.KeyGeneration == keyGen && (poller == nil || poller(details)) {
   454  			u.ctx.log.Debug("found key generation %d", keyGen)
   455  			return details
   456  		}
   457  		if i == 9 {
   458  			break
   459  		}
   460  		i++
   461  		u.ctx.log.Debug("in pollForMembershipUpdate: iter=%d; missed it, now waiting for %s (latest details.KG = %d; poller=%v)", i, wait, details.KeyGeneration, (poller != nil))
   462  		kickTeamRekeyd(u.getPrimaryGlobalContext(), u.ctx.t)
   463  		time.Sleep(wait)
   464  		totalWait += wait
   465  		wait *= 2
   466  	}
   467  	require.FailNowf(u.ctx.t, "pollForMembershipUpdate timed out",
   468  		"Failed to find the needed key generation (%d) after %s of waiting (%d iterations)",
   469  		keyGen, totalWait, i)
   470  	return keybase1.TeamDetails{}
   471  }
   472  
   473  func (u *smuUser) pollForTeamSeqnoLink(team smuTeam, toSeqno keybase1.Seqno) {
   474  	for i := 0; i < 20; i++ {
   475  		details, err := teams.Load(context.TODO(), u.getPrimaryGlobalContext(), keybase1.LoadTeamArg{
   476  			Name:        team.name,
   477  			ForceRepoll: true,
   478  		})
   479  		if err != nil {
   480  			u.ctx.t.Fatalf("error while loading team %q: %v", team.name, err)
   481  		}
   482  
   483  		if details.CurrentSeqno() >= toSeqno {
   484  			u.ctx.t.Logf("Found new seqno %d at poll loop iter %d", details.CurrentSeqno(), i)
   485  			return
   486  		}
   487  
   488  		time.Sleep(500 * time.Millisecond)
   489  	}
   490  
   491  	u.ctx.t.Fatalf("timed out waiting for team %s seqno link %d", team, toSeqno)
   492  }
   493  
   494  func (u *smuUser) createTeam(writers []*smuUser) smuTeam {
   495  	return u.createTeam2(nil, writers, nil, nil)
   496  }
   497  
   498  func (u *smuUser) createTeam2(readers, writers, admins, owners []*smuUser) smuTeam {
   499  	name := u.username + "t"
   500  	nameK1, err := keybase1.TeamNameFromString(name)
   501  	require.NoError(u.ctx.t, err)
   502  	cli := u.getTeamsClient()
   503  	x, err := cli.TeamCreate(context.TODO(), keybase1.TeamCreateArg{Name: nameK1.String()})
   504  	require.NoError(u.ctx.t, err)
   505  	lists := [][]*smuUser{readers, writers, admins, owners}
   506  	roles := []keybase1.TeamRole{keybase1.TeamRole_READER,
   507  		keybase1.TeamRole_WRITER, keybase1.TeamRole_ADMIN, keybase1.TeamRole_OWNER}
   508  	for i, list := range lists {
   509  		for _, u2 := range list {
   510  			_, err = cli.TeamAddMember(context.TODO(), keybase1.TeamAddMemberArg{
   511  				TeamID:   x.TeamID,
   512  				Username: u2.username,
   513  				Role:     roles[i],
   514  			})
   515  			require.NoError(u.ctx.t, err)
   516  		}
   517  	}
   518  	return smuTeam{ID: x.TeamID, name: name}
   519  }
   520  
   521  func (u *smuUser) lookupImplicitTeam(create bool, displayName string, public bool) smuImplicitTeam {
   522  	cli := u.getTeamsClient()
   523  	var err error
   524  	var res keybase1.LookupImplicitTeamRes
   525  	if create {
   526  		res, err = cli.LookupOrCreateImplicitTeam(context.TODO(), keybase1.LookupOrCreateImplicitTeamArg{Name: displayName, Public: public})
   527  	} else {
   528  		res, err = cli.LookupImplicitTeam(context.TODO(), keybase1.LookupImplicitTeamArg{Name: displayName, Public: public})
   529  	}
   530  	if err != nil {
   531  		u.ctx.t.Fatal(err)
   532  	}
   533  	return smuImplicitTeam{ID: res.TeamID}
   534  }
   535  
   536  func (u *smuUser) loadTeam(teamname string, admin bool) *teams.Team {
   537  	team, err := teams.Load(context.Background(), u.getPrimaryGlobalContext(), keybase1.LoadTeamArg{
   538  		Name:        teamname,
   539  		NeedAdmin:   admin,
   540  		ForceRepoll: true,
   541  	})
   542  	require.NoError(u.ctx.t, err)
   543  	return team
   544  }
   545  
   546  func (u *smuUser) addTeamMember(team smuTeam, member *smuUser, role keybase1.TeamRole) {
   547  	cli := u.getTeamsClient()
   548  	_, err := cli.TeamAddMember(context.TODO(), keybase1.TeamAddMemberArg{
   549  		TeamID:   team.ID,
   550  		Username: member.username,
   551  		Role:     role,
   552  	})
   553  	require.NoError(u.ctx.t, err)
   554  }
   555  
   556  func (u *smuUser) addWriter(team smuTeam, w *smuUser) {
   557  	u.addTeamMember(team, w, keybase1.TeamRole_WRITER)
   558  }
   559  
   560  func (u *smuUser) addAdmin(team smuTeam, w *smuUser) {
   561  	u.addTeamMember(team, w, keybase1.TeamRole_ADMIN)
   562  }
   563  
   564  func (u *smuUser) editMember(team *smuTeam, username string, role keybase1.TeamRole) {
   565  	err := u.getTeamsClient().TeamEditMember(context.TODO(), keybase1.TeamEditMemberArg{
   566  		Name:     team.name,
   567  		Username: username,
   568  		Role:     role,
   569  	})
   570  	require.NoError(u.ctx.t, err)
   571  }
   572  
   573  func (u *smuUser) reAddUserAfterReset(team smuImplicitTeam, w *smuUser) {
   574  	cli := u.getTeamsClient()
   575  	err := cli.TeamReAddMemberAfterReset(context.TODO(), keybase1.TeamReAddMemberAfterResetArg{
   576  		Id:       team.ID,
   577  		Username: w.username,
   578  	})
   579  	require.NoError(u.ctx.t, err)
   580  }
   581  
   582  func (u *smuUser) reset() {
   583  	g := u.getPrimaryGlobalContext()
   584  	ui := genericUI{
   585  		g:        g,
   586  		SecretUI: u.secretUI(),
   587  	}
   588  	g.SetUI(&ui)
   589  	cmd := client.NewCmdAccountResetRunner(g)
   590  	err := cmd.Run()
   591  	if err != nil {
   592  		u.ctx.t.Fatal(err)
   593  	}
   594  }
   595  
   596  func (u *smuUser) delete() {
   597  	g := u.getPrimaryGlobalContext()
   598  	ui := genericUI{
   599  		g:          g,
   600  		SecretUI:   u.secretUI(),
   601  		TerminalUI: smuTerminalUI{},
   602  	}
   603  	g.SetUI(&ui)
   604  	cmd := client.NewCmdAccountDeleteRunner(g)
   605  	err := cmd.Run()
   606  	if err != nil {
   607  		u.ctx.t.Fatal(err)
   608  	}
   609  }
   610  
   611  func (u *smuUser) dbNuke() {
   612  	err := u.primaryDevice().ctlClient().DbNuke(context.TODO(), 0)
   613  	require.NoError(u.ctx.t, err)
   614  }
   615  
   616  func (u *smuUser) userVersion() keybase1.UserVersion {
   617  	uv, err := u.primaryDevice().userClient().MeUserVersion(context.Background(), keybase1.MeUserVersionArg{ForcePoll: true})
   618  	require.NoError(u.ctx.t, err)
   619  	return uv
   620  }
   621  
   622  func (u *smuUser) MetaContext() libkb.MetaContext {
   623  	return libkb.NewMetaContextForTest(*u.primaryDevice().tctx)
   624  }
   625  
   626  func (u *smuUser) getPrimaryGlobalContext() *libkb.GlobalContext {
   627  	return u.primaryDevice().tctx.G
   628  }
   629  
   630  func (u *smuUser) setUIDMapperNoCachingMode(enabled bool) {
   631  	u.getPrimaryGlobalContext().UIDMapper.SetTestingNoCachingMode(enabled)
   632  }
   633  
   634  func (u *smuUser) loginAfterReset(numClones int) *smuDeviceWrapper {
   635  	return u.loginAfterResetHelper(numClones, true)
   636  }
   637  
   638  func (u *smuUser) loginAfterResetNoPUK(numClones int) *smuDeviceWrapper {
   639  	return u.loginAfterResetHelper(numClones, false)
   640  }
   641  
   642  func (u *smuUser) loginAfterResetHelper(numClones int, puk bool) *smuDeviceWrapper {
   643  	dev := u.ctx.newDeviceHelper(u, numClones, puk)
   644  	u.primary = dev
   645  	g := dev.tctx.G
   646  	ui := genericUI{
   647  		g:           g,
   648  		SecretUI:    u.secretUI(),
   649  		LoginUI:     usernameLoginUI{u.userInfo.username},
   650  		ProvisionUI: nullProvisionUI{randomDevice()},
   651  	}
   652  	g.SetUI(&ui)
   653  	cmd := client.NewCmdLoginRunner(g)
   654  	err := cmd.Run()
   655  	require.NoError(u.ctx.t, err, "login after reset")
   656  	return dev
   657  }
   658  
   659  func (u *smuUser) secretUI() signupInfoSecretUI {
   660  	return signupInfoSecretUI{u.userInfo, u.ctx.log}
   661  }
   662  
   663  func (u *smuUser) teamGet(team smuTeam) (keybase1.TeamDetails, error) {
   664  	cli := u.getTeamsClient()
   665  	details, err := cli.TeamGet(context.TODO(), keybase1.TeamGetArg{Name: team.name})
   666  	return details, err
   667  }
   668  
   669  func (u *smuUser) teamMemberDetails(team smuTeam, user *smuUser) ([]keybase1.TeamMemberDetails, error) {
   670  	teamDetails, err := u.teamGet(team)
   671  	if err != nil {
   672  		return nil, err
   673  	}
   674  	var all []keybase1.TeamMemberDetails
   675  	all = append(all, teamDetails.Members.Owners...)
   676  	all = append(all, teamDetails.Members.Admins...)
   677  	all = append(all, teamDetails.Members.Writers...)
   678  	all = append(all, teamDetails.Members.Readers...)
   679  
   680  	var matches []keybase1.TeamMemberDetails
   681  	for _, m := range all {
   682  		if m.Username == user.username {
   683  			matches = append(matches, m)
   684  		}
   685  	}
   686  	if len(matches) == 0 {
   687  		return nil, libkb.NotFoundError{}
   688  	}
   689  	return matches, nil
   690  }
   691  
   692  func (u *smuUser) isMemberActive(team smuTeam, user *smuUser) (bool, error) {
   693  	details, err := u.teamMemberDetails(team, user)
   694  	u.ctx.t.Logf("isMemberActive team member details for %s: %+v", user.username, details)
   695  	if err != nil {
   696  		return false, err
   697  	}
   698  	for _, d := range details {
   699  		if d.Status.IsActive() {
   700  			return true, nil
   701  		}
   702  	}
   703  	return false, nil
   704  }
   705  
   706  func (u *smuUser) assertMemberActive(team smuTeam, user *smuUser) {
   707  	active, err := u.isMemberActive(team, user)
   708  	require.NoError(u.ctx.t, err, "assertMemberInactive error: %s", err)
   709  	require.True(u.ctx.t, active, "user %s is inactive (expected active)", user.username)
   710  }
   711  
   712  func (u *smuUser) assertMemberInactive(team smuTeam, user *smuUser) {
   713  	active, err := u.isMemberActive(team, user)
   714  	require.NoError(u.ctx.t, err, "assertMemberInactive error: %s", err)
   715  	require.False(u.ctx.t, active, "user %s is active (expected inactive)", user.username)
   716  }
   717  
   718  func (u *smuUser) assertMemberMissing(team smuTeam, user *smuUser) {
   719  	_, err := u.teamMemberDetails(team, user)
   720  	require.Error(user.ctx.t, err, "member should not be found")
   721  	require.Equal(user.ctx.t, libkb.NotFoundError{}, err, "member should not be found")
   722  }
   723  
   724  func (u *smuUser) uid() keybase1.UID {
   725  	return u.primaryDevice().tctx.G.Env.GetUID()
   726  }
   727  
   728  func (u *smuUser) openTeam(team smuTeam, role keybase1.TeamRole) {
   729  	cli := u.getTeamsClient()
   730  	err := cli.TeamSetSettings(context.Background(), keybase1.TeamSetSettingsArg{
   731  		TeamID: team.ID,
   732  		Settings: keybase1.TeamSettings{
   733  			Open:   true,
   734  			JoinAs: role,
   735  		},
   736  	})
   737  	if err != nil {
   738  		u.ctx.t.Fatal(err)
   739  	}
   740  }
   741  
   742  func (u *smuUser) requestAccess(team smuTeam) {
   743  	cli := u.getTeamsClient()
   744  	_, err := cli.TeamRequestAccess(context.Background(), keybase1.TeamRequestAccessArg{
   745  		Name: team.name,
   746  	})
   747  	if err != nil {
   748  		u.ctx.t.Fatal(err)
   749  	}
   750  }
   751  
   752  func (u *smuUser) readChatsWithError(team smuTeam) (messages []chat1.MessageUnboxed, err error) {
   753  	return u.readChatsWithErrorAndDevice(team, u.primaryDevice(), 0)
   754  }
   755  
   756  // readChatsWithErrorAndDevice reads chats from `team` to get *at least*
   757  // `nMessages` messages.
   758  func (u *smuUser) readChatsWithErrorAndDevice(team smuTeam, dev *smuDeviceWrapper, nMessages int) (messages []chat1.MessageUnboxed, err error) {
   759  	tctx := dev.popClone()
   760  
   761  	pollInterval := 100 * time.Millisecond
   762  	timeLimit := 10 * time.Second
   763  	timeout := time.After(timeLimit)
   764  
   765  pollLoop:
   766  	for i := 0; ; i++ {
   767  		runner := client.NewCmdChatReadRunner(tctx.G)
   768  		runner.SetTeamChatForTest(team.name)
   769  		_, messages, err = runner.Fetch()
   770  		if err != nil {
   771  			u.ctx.t.Logf("readChatsWithErrorAndDevice failure: %s", err.Error())
   772  			return nil, err
   773  		}
   774  
   775  		if len(messages) >= nMessages {
   776  			u.ctx.t.Logf("readChatsWithErrorAndDevice success after retrying %d times, got %d msgs, asked for %d", i, len(messages), nMessages)
   777  			return messages, nil
   778  		}
   779  
   780  		u.ctx.t.Logf("readChatsWithErrorAndDevice trying again in %s (i=%d)", pollInterval, i)
   781  		select {
   782  		case <-timeout:
   783  			break pollLoop
   784  		case <-time.After(pollInterval):
   785  		}
   786  	}
   787  
   788  	u.ctx.t.Logf("Failed to readChatsWithErrorAndDevice after polling for %s", timeLimit)
   789  	return nil, fmt.Errorf("failed to read messages after polling for %s", timeLimit)
   790  }
   791  
   792  func (u *smuUser) readChats(team smuTeam, nMessages int) {
   793  	u.readChatsWithDevice(team, u.primaryDevice(), nMessages)
   794  }
   795  
   796  func (u *smuUser) readChatsWithDevice(team smuTeam, dev *smuDeviceWrapper, nMessages int) {
   797  	messages, err := u.readChatsWithErrorAndDevice(team, dev, nMessages)
   798  	t := u.ctx.t
   799  	require.NoError(t, err)
   800  
   801  	// Filter out journeycards
   802  	originalLen := len(messages)
   803  	n := 0 // https://github.com/golang/go/wiki/SliceTricks#filter-in-place
   804  	for _, msg := range messages {
   805  		if !msg.IsJourneycard() {
   806  			messages[n] = msg
   807  			n++
   808  		}
   809  	}
   810  	messages = messages[:n]
   811  	if originalLen < len(messages) {
   812  		t.Logf("filtered out %v journeycard messages", originalLen-len(messages))
   813  	}
   814  
   815  	if len(messages) != nMessages {
   816  		t.Logf("messages: %v", chat1.MessageUnboxedDebugLines(messages))
   817  	}
   818  	require.Len(t, messages, nMessages)
   819  
   820  	for i, msg := range messages {
   821  		require.Equal(t, msg.Valid().MessageBody.Text().Body, fmt.Sprintf("%d", len(messages)-i-1))
   822  	}
   823  	divDebug(u.ctx, "readChat success for %s", u.username)
   824  }
   825  
   826  func (u *smuUser) sendChat(t smuTeam, msg string) {
   827  	tctx := u.primaryDevice().popClone()
   828  	runner := client.NewCmdChatSendRunner(tctx.G)
   829  	runner.SetTeamChatForTest(t.name)
   830  	runner.SetMessage(msg)
   831  	err := runner.Run()
   832  	if err != nil {
   833  		u.ctx.t.Fatal(err)
   834  	}
   835  }