go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/cvtesting/util.go (about)

     1  // Copyright 2020 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package cvtesting
    16  
    17  import (
    18  	"context"
    19  	cryptorand "crypto/rand"
    20  	"encoding/hex"
    21  	"fmt"
    22  	"math/rand"
    23  	"net/mail"
    24  	"os"
    25  	"regexp"
    26  	"strconv"
    27  	"strings"
    28  	"testing"
    29  	"time"
    30  
    31  	nativeDatastore "cloud.google.com/go/datastore"
    32  	"github.com/golang/mock/gomock"
    33  	"google.golang.org/api/option"
    34  
    35  	"go.chromium.org/luci/auth"
    36  	"go.chromium.org/luci/auth/identity"
    37  	"go.chromium.org/luci/common/clock"
    38  	"go.chromium.org/luci/common/clock/testclock"
    39  	"go.chromium.org/luci/common/data/stringset"
    40  	"go.chromium.org/luci/common/errors"
    41  	"go.chromium.org/luci/common/logging"
    42  	"go.chromium.org/luci/common/logging/gologger"
    43  	"go.chromium.org/luci/common/tsmon"
    44  	"go.chromium.org/luci/common/tsmon/distribution"
    45  	"go.chromium.org/luci/common/tsmon/store"
    46  	"go.chromium.org/luci/common/tsmon/target"
    47  	"go.chromium.org/luci/common/tsmon/types"
    48  	"go.chromium.org/luci/gae/filter/txndefer"
    49  	"go.chromium.org/luci/gae/impl/cloud"
    50  	"go.chromium.org/luci/gae/impl/memory"
    51  	"go.chromium.org/luci/gae/service/datastore"
    52  	"go.chromium.org/luci/gae/service/info"
    53  	serverauth "go.chromium.org/luci/server/auth"
    54  	"go.chromium.org/luci/server/auth/authtest"
    55  	"go.chromium.org/luci/server/auth/realms"
    56  	"go.chromium.org/luci/server/caching"
    57  	"go.chromium.org/luci/server/secrets"
    58  	"go.chromium.org/luci/server/tq"
    59  	"go.chromium.org/luci/server/tq/tqtesting"
    60  	_ "go.chromium.org/luci/server/tq/txn/datastore"
    61  
    62  	bbfake "go.chromium.org/luci/cv/internal/buildbucket/fake"
    63  	"go.chromium.org/luci/cv/internal/common"
    64  	"go.chromium.org/luci/cv/internal/common/bq"
    65  	"go.chromium.org/luci/cv/internal/common/tree"
    66  	"go.chromium.org/luci/cv/internal/common/tree/treetest"
    67  	"go.chromium.org/luci/cv/internal/configs/srvcfg"
    68  	"go.chromium.org/luci/cv/internal/gerrit"
    69  	gf "go.chromium.org/luci/cv/internal/gerrit/gerritfake"
    70  	listenerpb "go.chromium.org/luci/cv/settings/listener"
    71  
    72  	. "github.com/smartystreets/goconvey/convey"
    73  )
    74  
    75  const gaeTopLevelDomain = ".appspot.com"
    76  
    77  // TODO(tandrii): add fake config generation facilities.
    78  
    79  // Test encapsulates typical setup for CV test.
    80  //
    81  // Typical use:
    82  //
    83  //	ct := cvtesting.Test{}
    84  //	ctx, cancel := ct.SetUp(t)
    85  //	defer cancel()
    86  type Test struct {
    87  	// Env simulates CV environment.
    88  	Env *common.Env
    89  	// GFake is a Gerrit fake. Defaults to an empty one.
    90  	GFake *gf.Fake
    91  	// BuildbucketFake is a Buildbucket fake. Defaults to an empty one.
    92  	BuildbucketFake *bbfake.Fake
    93  	// TreeFake is a fake Tree. Defaults to an open Tree.
    94  	TreeFake *treetest.Fake
    95  	// BQFake is a fake BQ client.
    96  	BQFake *bq.Fake
    97  	// TQDispatcher is a dispatcher with which task classes must be registered.
    98  	//
    99  	// Must not be set.
   100  	TQDispatcher *tq.Dispatcher
   101  	// TQ allows to run TQ tasks.
   102  	TQ *tqtesting.Scheduler
   103  	// SucceededTQTasks is a list of the TQ tasks that were executed successfully.
   104  	SucceededTQTasks tqtesting.TaskList
   105  	FailedTQTasks    tqtesting.TaskList
   106  
   107  	// Clock allows to move time forward.
   108  	// By default, the time is moved automatically is something waits on it.
   109  	Clock testclock.TestClock
   110  	// TSMonStore store keeps all metrics in memory and allows examination.
   111  	TSMonStore store.Store
   112  
   113  	// MaxDuration limits how long a test can run as a fail safe.
   114  	//
   115  	// Defaults to 10s to most likely finish in pre/post submit tests,
   116  	// with limited CPU resources.
   117  	// Set to ~10ms when debugging a hung test.
   118  	MaxDuration time.Duration
   119  
   120  	// GoMockCtl is the controller for gomock.
   121  	GoMockCtl *gomock.Controller
   122  
   123  	// cleanupFuncs are executed in reverse order in cleanup().
   124  	cleanupFuncs []func()
   125  
   126  	// authDB is used to mock CrIA memberships.
   127  	authDB *authtest.FakeDB
   128  }
   129  
   130  type testingContextKeyType struct{}
   131  
   132  // IsTestingContext checks if the given context was derived from one created by
   133  // cvtesting.Test.SetUp().
   134  func IsTestingContext(ctx context.Context) bool {
   135  	return ctx.Value(testingContextKeyType{}) != nil
   136  }
   137  
   138  func (t *Test) SetUp(testingT *testing.T) (context.Context, func()) {
   139  	if t.Env == nil {
   140  		t.Env = &common.Env{
   141  			LogicalHostname: "luci-change-verifier" + gaeTopLevelDomain,
   142  			HTTPAddressBase: "https://luci-change-verifier" + gaeTopLevelDomain,
   143  			GAEInfo: struct {
   144  				CloudProject string
   145  				ServiceName  string
   146  				InstanceID   string
   147  			}{
   148  				CloudProject: "luci-change-verifier",
   149  				ServiceName:  "test-service",
   150  				InstanceID:   "test-instance",
   151  			},
   152  		}
   153  	}
   154  
   155  	t.setMaxDuration()
   156  	ctxShared := context.WithValue(context.Background(), testingContextKeyType{}, struct{}{})
   157  	// Don't set the deadline (timeout) into the context given to the test,
   158  	// as it may interfere with test clock.
   159  	ctx, cancel := context.WithCancel(ctxShared)
   160  	ctxTimed, cancelTimed := context.WithTimeout(ctxShared, t.MaxDuration)
   161  	go func(ctx context.Context) {
   162  		// Instead, watch for expiry of ctxTimed and cancel test `ctx`.
   163  		select {
   164  		case <-ctxTimed.Done():
   165  			cancel()
   166  		case <-ctx.Done():
   167  			// Normal test termination.
   168  			cancelTimed()
   169  		}
   170  	}(ctx)
   171  	t.cleanupFuncs = append(t.cleanupFuncs, func() {
   172  		// Fail the test if the test has timed out.
   173  		So(ctxTimed.Err(), ShouldBeNil)
   174  		cancel()
   175  		cancelTimed()
   176  	})
   177  
   178  	// setup the test clock first so that logger can use test clock timestamp.
   179  	ctx = t.setUpTestClock(ctx)
   180  	if testing.Verbose() {
   181  		// TODO(crbug/1282023): make this logger emit testclock-based timestamps.
   182  		ctx = logging.SetLevel(gologger.StdConfig.Use(ctx), logging.Debug)
   183  	}
   184  	// setup timerCallback after setup logger so that the logging in the
   185  	// callback function can honor the verbose mode.
   186  	ctx = t.setTestClockTimerCB(ctx)
   187  	ctx = caching.WithEmptyProcessCache(ctx)
   188  	ctx = secrets.GeneratePrimaryTinkAEADForTest(ctx)
   189  
   190  	if t.TQDispatcher != nil {
   191  		panic("TQDispatcher must not be set")
   192  	}
   193  	t.TQDispatcher = &tq.Dispatcher{}
   194  	ctx, t.TQ = tq.TestingContext(ctx, t.TQDispatcher)
   195  	t.TQ.TaskSucceeded = tqtesting.TasksCollector(&t.SucceededTQTasks)
   196  	t.TQ.TaskFailed = tqtesting.TasksCollector(&t.FailedTQTasks)
   197  
   198  	if t.GFake == nil {
   199  		t.GFake = &gf.Fake{}
   200  	}
   201  	if t.BuildbucketFake == nil {
   202  		t.BuildbucketFake = &bbfake.Fake{}
   203  	}
   204  	if t.TreeFake == nil {
   205  		t.TreeFake = treetest.NewFake(ctx, tree.Open)
   206  	}
   207  	if t.BQFake == nil {
   208  		t.BQFake = &bq.Fake{}
   209  	}
   210  
   211  	ctx = t.installDS(ctx)
   212  	ctx = txndefer.FilterRDS(ctx)
   213  	t.authDB = authtest.NewFakeDB()
   214  	ctx = serverauth.WithState(ctx, &authtest.FakeState{FakeDB: t.authDB})
   215  
   216  	ctx, _, _ = tsmon.WithFakes(ctx)
   217  	t.TSMonStore = store.NewInMemory(&target.Task{})
   218  	tsmon.GetState(ctx).SetStore(t.TSMonStore)
   219  
   220  	t.GoMockCtl = gomock.NewController(testingT)
   221  	if err := srvcfg.SetTestListenerConfig(ctx, &listenerpb.Settings{}, nil); err != nil {
   222  		panic(err)
   223  	}
   224  
   225  	return ctx, t.cleanup
   226  }
   227  
   228  func (t *Test) cleanup() {
   229  	for i := len(t.cleanupFuncs) - 1; i >= 0; i-- {
   230  		t.cleanupFuncs[i]()
   231  	}
   232  }
   233  
   234  func (t *Test) RoundTestClock(multiple time.Duration) {
   235  	t.Clock.Set(t.Clock.Now().Add(multiple).Truncate(multiple))
   236  }
   237  
   238  func (t *Test) GFactory() gerrit.Factory {
   239  	return gerrit.CachingFactory(16, gerrit.TimeLimitedFactory(gerrit.InstrumentedFactory(t.GFake)))
   240  }
   241  
   242  // TSMonSentValue returns the latest value of the given metric.
   243  //
   244  // If not set, returns nil.
   245  func (t *Test) TSMonSentValue(ctx context.Context, m types.Metric, fieldVals ...any) any {
   246  	resetTime := time.Time{}
   247  	return t.TSMonStore.Get(ctx, m, resetTime, fieldVals)
   248  }
   249  
   250  // TSMonSentDistr returns the latest distr value of the given metric.
   251  //
   252  // If not set, returns nil.
   253  // Panics if metric's value is not a distribution.
   254  func (t *Test) TSMonSentDistr(ctx context.Context, m types.Metric, fieldVals ...any) *distribution.Distribution {
   255  	v := t.TSMonSentValue(ctx, m, fieldVals...)
   256  	if v == nil {
   257  		return nil
   258  	}
   259  	d, ok := v.(*distribution.Distribution)
   260  	if !ok {
   261  		panic(fmt.Errorf("metric %q value is not a %T, but %T", m.Info().Name, d, v))
   262  	}
   263  	return d
   264  }
   265  
   266  func (t *Test) setMaxDuration() {
   267  	// Can't use Go's test timeout because it is per TestXYZ func,
   268  	// which typically instantiates & runs several `cvtesting.Test`s.
   269  	switch s := os.Getenv("CV_TEST_TIMEOUT_SEC"); {
   270  	case s != "":
   271  		v, err := strconv.ParseInt(s, 10, 31)
   272  		if err != nil {
   273  			panic(err)
   274  		}
   275  		t.MaxDuration = time.Duration(v) * time.Second
   276  	case t.MaxDuration != time.Duration(0):
   277  		// TODO(tandrii): remove the possibility to override this per test in favor
   278  		// of CV_TEST_TIMEOUT_SEC env var.
   279  	case raceDetectionEnabled:
   280  		t.MaxDuration = 90 * time.Second
   281  	default:
   282  		t.MaxDuration = 20 * time.Second
   283  	}
   284  }
   285  
   286  // DisableProjectInGerritListener updates the cached config to disable LUCI
   287  // projects matching a given regexp in Listener.
   288  func (t *Test) DisableProjectInGerritListener(ctx context.Context, projectRE string) {
   289  	cfg, err := srvcfg.GetListenerConfig(ctx, nil)
   290  	if err != nil {
   291  		panic(err)
   292  	}
   293  	existing := stringset.NewFromSlice(cfg.DisabledProjectRegexps...)
   294  	existing.Add(projectRE)
   295  	cfg.DisabledProjectRegexps = existing.ToSortedSlice()
   296  	if err := srvcfg.SetTestListenerConfig(ctx, cfg, nil); err != nil {
   297  		panic(err)
   298  	}
   299  }
   300  
   301  func (t *Test) installDS(ctx context.Context) context.Context {
   302  	if !strings.HasSuffix(t.Env.LogicalHostname, gaeTopLevelDomain) {
   303  		panic(fmt.Errorf("Env.LogicalHostname %q doesn't end with %q", t.Env.LogicalHostname, gaeTopLevelDomain))
   304  	}
   305  	appID := t.Env.LogicalHostname[:len(t.Env.LogicalHostname)-len(gaeTopLevelDomain)]
   306  
   307  	if ctx, ok := t.installDSReal(ctx); ok {
   308  		return memory.UseInfo(ctx, appID)
   309  	}
   310  	if ctx, ok := t.installDSEmulator(ctx); ok {
   311  		return memory.UseInfo(ctx, appID)
   312  	}
   313  
   314  	ctx = memory.UseWithAppID(ctx, appID)
   315  	// CV runs against Firestore backend, which is consistent.
   316  	datastore.GetTestable(ctx).Consistent(true)
   317  	// Intentionally not enabling AutoIndex so that new code accidentally needing
   318  	// a new index adds it both here (for the rest of CV tests to work, notably
   319  	// e2e ones) and into appengine/index.yaml.
   320  	datastore.GetTestable(ctx).AutoIndex(false)
   321  	return ctx
   322  }
   323  
   324  // installDSProd configures CV tests to run with actual DS.
   325  //
   326  // If DATASTORE_PROJECT ENV var isn't set, returns false.
   327  //
   328  // To use, first
   329  //
   330  //	$ luci-auth context -- bash
   331  //	$ export DATASTORE_PROJECT=my-cloud-project-with-datastore
   332  //
   333  // and then run go tests the usual way, e.g.:
   334  //
   335  //	$ go test ./...
   336  func (t *Test) installDSReal(ctx context.Context) (context.Context, bool) {
   337  	project := os.Getenv("DATASTORE_PROJECT")
   338  	if project == "" {
   339  		return ctx, false
   340  	}
   341  	if project == "luci-change-verifier" {
   342  		panic("Don't use production CV project. Using -dev is OK.")
   343  	}
   344  
   345  	at := auth.NewAuthenticator(ctx, auth.SilentLogin, auth.Options{
   346  		Scopes: serverauth.CloudOAuthScopes,
   347  	})
   348  	ts, err := at.TokenSource()
   349  	if err != nil {
   350  		err = errors.Annotate(err, "failed to initialize the token source (are you in `$ luci-auth context`?)").Err()
   351  		So(err, ShouldBeNil)
   352  	}
   353  
   354  	logging.Debugf(ctx, "Using DS of project %q", project)
   355  	client, err := nativeDatastore.NewClient(ctx, project, option.WithTokenSource(ts))
   356  	So(err, ShouldBeNil)
   357  	return t.installDSshared(ctx, project, client), true
   358  }
   359  
   360  // installDSEmulator configures CV tests to run with DS emulator.
   361  //
   362  // If DATASTORE_EMULATOR_HOST ENV var isn't set, returns false.
   363  //
   364  // To use, run
   365  //
   366  //	$ gcloud beta emulators datastore start --consistency=1.0
   367  //
   368  // and export DATASTORE_EMULATOR_HOST as printed by above command.
   369  //
   370  // NOTE: as of Feb 2021, emulator runs in legacy Datastore mode,
   371  // not Firestore.
   372  func (t *Test) installDSEmulator(ctx context.Context) (context.Context, bool) {
   373  	emulatorHost := os.Getenv("DATASTORE_EMULATOR_HOST")
   374  	if emulatorHost == "" {
   375  		return ctx, false
   376  	}
   377  
   378  	logging.Debugf(ctx, "Using DS emulator at %q", emulatorHost)
   379  	client, err := nativeDatastore.NewClient(ctx, "luci-gae-emulator-test")
   380  	So(err, ShouldBeNil)
   381  	return t.installDSshared(ctx, "luci-gae-emulator-test", client), true
   382  }
   383  
   384  func (t *Test) installDSshared(ctx context.Context, cloudProject string, client *nativeDatastore.Client) context.Context {
   385  	t.cleanupFuncs = append(t.cleanupFuncs, func() {
   386  		if err := client.Close(); err != nil {
   387  			logging.Errorf(ctx, "failed to close DS client: %s", err)
   388  		}
   389  	})
   390  	ctx = (&cloud.ConfigLite{ProjectID: cloudProject, DS: client}).Use(ctx)
   391  	maybeCleanupOldDSNamespaces(ctx)
   392  
   393  	// Enter a namespace for this tests.
   394  	ns := genDSNamespaceName(time.Now())
   395  	logging.Debugf(ctx, "Using %q DS namespace", ns)
   396  	ctx = info.MustNamespace(ctx, ns)
   397  	// Failure to clear is hard before the test,
   398  	// ignored after the test.
   399  	So(clearDS(ctx), ShouldBeNil)
   400  	t.cleanupFuncs = append(t.cleanupFuncs, func() {
   401  		if err := clearDS(ctx); err != nil {
   402  			logging.Errorf(ctx, "failed to clean DS namespace %s: %s", ns, err)
   403  		}
   404  	})
   405  	return ctx
   406  }
   407  
   408  func genDSNamespaceName(t time.Time) string {
   409  	rnd := make([]byte, 8)
   410  	if _, err := cryptorand.Read(rnd); err != nil {
   411  		panic(err)
   412  	}
   413  	return fmt.Sprintf("testing-%s-%s", time.Now().Format("2006-01-02"), hex.EncodeToString(rnd))
   414  }
   415  
   416  var dsNamespaceRegexp = regexp.MustCompile(`^testing-(\d{4}-\d\d-\d\d)-[0-9a-f]+$`)
   417  
   418  func isOldTestDSNamespace(ns string, now time.Time) bool {
   419  	m := dsNamespaceRegexp.FindSubmatch([]byte(ns))
   420  	if len(m) == 0 {
   421  		return false
   422  	}
   423  	// Anything up ~2 days old should be kept to avoid accidentally removing
   424  	// currently under test namespace in presence of timezones and out of sync
   425  	// clocks.
   426  	const maxAge = 2 * 24 * time.Hour
   427  	t, err := time.Parse("2006-01-02", string(m[1]))
   428  	if err != nil {
   429  		panic(err)
   430  	}
   431  	return now.Sub(t) > maxAge
   432  }
   433  
   434  func clearDS(ctx context.Context) error {
   435  	// Execute a kindless query to clear entire namespace.
   436  	q := datastore.NewQuery("").KeysOnly(true)
   437  	var allKeys []*datastore.Key
   438  	if err := datastore.GetAll(ctx, q, &allKeys); err != nil {
   439  		return errors.Annotate(err, "failed to get entities").Err()
   440  	}
   441  	if err := datastore.Delete(ctx, allKeys); err != nil {
   442  		return errors.Annotate(err, "failed to delete %d entities", len(allKeys)).Err()
   443  	}
   444  	return nil
   445  }
   446  
   447  func maybeCleanupOldDSNamespaces(ctx context.Context) {
   448  	if rand.Intn(1024) < 1020 { // ~99% of cases.
   449  		return
   450  	}
   451  	q := datastore.NewQuery("__namespace__").KeysOnly(true)
   452  	var allKeys []*datastore.Key
   453  	if err := datastore.GetAll(ctx, q, &allKeys); err != nil {
   454  		logging.Warningf(ctx, "failed to query all namespaces: %s", err)
   455  		return
   456  	}
   457  	now := time.Now()
   458  	var toDelete []string
   459  	for _, k := range allKeys {
   460  		ns := k.StringID()
   461  		if isOldTestDSNamespace(ns, now) {
   462  			toDelete = append(toDelete, ns)
   463  		}
   464  	}
   465  	logging.Debugf(ctx, "cleaning up %d old namespaces", len(toDelete))
   466  	for _, ns := range toDelete {
   467  		logging.Debugf(ctx, "cleaning up %s", ns)
   468  		if err := clearDS(info.MustNamespace(ctx, ns)); err != nil {
   469  			logging.Errorf(ctx, "failed to clean old DS namespace %s: %s", ns, err)
   470  		}
   471  	}
   472  }
   473  
   474  // setUpTestClock simulates passage of time w/o idling CPU.
   475  func (t *Test) setUpTestClock(ctx context.Context) context.Context {
   476  	if t.Clock != nil {
   477  		return clock.Set(ctx, t.Clock)
   478  	}
   479  	// Use a date-time that is easy to eyeball in logs.
   480  	utc := time.Date(2020, time.February, 2, 10, 30, 00, 0, time.UTC)
   481  	// But set it up in a clock as a local time to expose incorrect assumptions of UTC.
   482  	now := time.Date(2020, time.February, 2, 13, 30, 00, 0, time.FixedZone("Fake local", 3*60*60))
   483  	So(now.Equal(utc), ShouldBeTrue)
   484  	ctx, t.Clock = testclock.UseTime(ctx, now)
   485  	return ctx
   486  }
   487  
   488  // setTestClockTimerCB moves test time forward if something we recognize waits
   489  // for it.
   490  func (t *Test) setTestClockTimerCB(ctx context.Context) context.Context {
   491  	// Testclock calls this callback every time something is waiting.
   492  	// To avoid getting stuck tests, we need to move testclock forward by the
   493  	// requested duration in most cases but not all.
   494  	moveIf := stringset.NewFromSlice(
   495  		// Used by tqtesting to wait until ETA of the next task.
   496  		tqtesting.ClockTag,
   497  		// Used to retry on outgoing requests for BB and Gerrit.
   498  		common.LaunchRetryClockTag,
   499  	)
   500  	ignoreIf := stringset.NewFromSlice(
   501  		// Used in clock.WithTimeout(ctx) | clock.WithDeadline(ctx).
   502  		clock.ContextDeadlineTag,
   503  	)
   504  	t.Clock.SetTimerCallback(func(dur time.Duration, timer clock.Timer) {
   505  		tags := testclock.GetTags(timer)
   506  		move, ignore := 0, 0
   507  		for _, tag := range tags {
   508  			switch {
   509  			case moveIf.Has(tag):
   510  				move++
   511  			case ignoreIf.Has(tag):
   512  				ignore++
   513  			default:
   514  				// Ignore by default, but log it to help fix the test if it gets stuck.
   515  				logging.Warningf(ctx, "ignoring unexpected timer tag: %q. If test is stuck, add tag to `moveIf` above this log line", tag)
   516  			}
   517  		}
   518  		// In ~all cases, there is exactly 1 tag, but be future proof.
   519  		switch {
   520  		case move > 0:
   521  			logging.Debugf(ctx, "moving test clock %s by %s forward for %s", t.Clock.Now(), dur, tags)
   522  			t.Clock.Add(dur)
   523  		case ignore == 0:
   524  			logging.Warningf(ctx, "ignoring timer without tags. If test is stuck, tag the waits via `clock` library")
   525  		}
   526  	})
   527  	return ctx
   528  }
   529  
   530  // AddMember adds a given member into a given luci auth group.
   531  //
   532  // The email may omit domain. In that case, this method will add "@example.com"
   533  // as the domain name.
   534  func (t *Test) AddMember(email, group string) {
   535  	if _, err := mail.ParseAddress(email); err != nil {
   536  		email = fmt.Sprintf("%s@example.com", email)
   537  	}
   538  	id, err := identity.MakeIdentity(fmt.Sprintf("user:%s", email))
   539  	if err != nil {
   540  		panic(err)
   541  	}
   542  	t.authDB.AddMocks(authtest.MockMembership(id, group))
   543  }
   544  
   545  // AddPermission grants permission to the member in the given realm.
   546  //
   547  // The email may omit domain. In that case, this method will add "@example.com"
   548  // as the domain name.
   549  func (t *Test) AddPermission(email string, perm realms.Permission, realm string) {
   550  	if _, err := mail.ParseAddress(email); err != nil {
   551  		email = fmt.Sprintf("%s@example.com", email)
   552  	}
   553  	id, err := identity.MakeIdentity(fmt.Sprintf("user:%s", email))
   554  	if err != nil {
   555  		panic(err)
   556  	}
   557  	t.authDB.AddMocks(authtest.MockPermission(id, realm, perm))
   558  }
   559  
   560  func (t *Test) ResetMockedAuthDB(ctx context.Context) {
   561  	t.authDB = authtest.NewFakeDB()
   562  	serverauth.GetState(ctx).(*authtest.FakeState).FakeDB = t.authDB
   563  }