1// Copyright 2015 Keybase, Inc. All rights reserved. Use of 2// this source code is governed by the included BSD license. 3 4// +build !production 5 6package libkb 7 8import ( 9 "crypto/rand" 10 "encoding/hex" 11 "fmt" 12 "io/ioutil" 13 "os" 14 "path" 15 "path/filepath" 16 "runtime" 17 "strings" 18 "sync" 19 "time" 20 21 "golang.org/x/net/context" 22 "golang.org/x/sync/errgroup" 23 24 "github.com/keybase/client/go/gregor" 25 "github.com/keybase/client/go/logger" 26 "github.com/keybase/client/go/protocol/gregor1" 27 keybase1 "github.com/keybase/client/go/protocol/keybase1" 28 "github.com/stretchr/testify/require" 29) 30 31// TestConfig tracks libkb config during a test 32type TestConfig struct { 33 configFileName string 34} 35 36func (c *TestConfig) GetConfigFileName() string { return c.configFileName } 37 38// TestingTB is a copy of the exported parts of testing.TB. We define 39// this in order to avoid pulling in the "testing" package in exported 40// code. 41type TestingTB interface { 42 Error(args ...interface{}) 43 Errorf(format string, args ...interface{}) 44 Fail() 45 FailNow() 46 Failed() bool 47 Fatal(args ...interface{}) 48 Fatalf(format string, args ...interface{}) 49 Log(args ...interface{}) 50 Logf(format string, args ...interface{}) 51 Name() string 52 Skip(args ...interface{}) 53 SkipNow() 54 Skipf(format string, args ...interface{}) 55 Skipped() bool 56 Helper() 57} 58 59func MakeThinGlobalContextForTesting(t TestingTB) *GlobalContext { 60 g := NewGlobalContext().Init() 61 g.Log = logger.NewTestLogger(t) 62 return g 63} 64 65func makeLogGetter(t TestingTB) func() logger.Logger { 66 return func() logger.Logger { return logger.NewTestLogger(t) } 67} 68 69func (c *TestConfig) CleanTest() { 70 if c.configFileName != "" { 71 os.Remove(c.configFileName) 72 } 73} 74 75// TestOutput is a mock interface for capturing and testing output 76type TestOutput struct { 77 expected string 78 t TestingTB 79 called *bool 80} 81 82func NewTestOutput(e string, t TestingTB, c *bool) TestOutput { 83 return TestOutput{e, t, c} 84} 85 86func (to TestOutput) Write(p []byte) (n int, err error) { 87 output := string(p) 88 if to.expected != output { 89 to.t.Errorf("Expected output %s, got %s", to.expected, output) 90 } 91 *to.called = true 92 return len(p), nil 93} 94 95type TestContext struct { 96 G *GlobalContext 97 PrevGlobal *GlobalContext 98 Tp *TestParameters 99 // TODO: Rename this to TB. 100 T TestingTB 101 eg *errgroup.Group 102 cleanupCh chan struct{} 103 origLog logger.Logger 104} 105 106func (tc *TestContext) Cleanup() { 107 // stop the background logger 108 close(tc.cleanupCh) 109 err := tc.eg.Wait() 110 require.NoError(tc.T, err) 111 112 tc.G.Log.Debug("global context shutdown:") 113 mctx := NewMetaContextForTest(*tc) 114 err = tc.G.Shutdown(mctx) // could error due to missing pid file 115 if err != nil { 116 tc.G.Log.Warning("tc.G.Shutdown failed: %s", err) 117 } 118 if len(tc.Tp.Home) > 0 { 119 tc.G.Log.Debug("clearing stored secrets:") 120 err := tc.ClearAllStoredSecrets() 121 tc.G.Log.Debug("cleaning up %s", tc.Tp.Home) 122 os.RemoveAll(tc.Tp.Home) 123 require.NoError(tc.T, err) 124 } 125 tc.G.Log.Debug("cleanup complete") 126 127 // Don't use the test logger anymore, since it's now out of scope 128 tc.G.Log = tc.origLog 129} 130 131func (tc *TestContext) Logout() error { 132 return NewMetaContextForTest(*tc).LogoutKillSecrets() 133} 134 135func (tc TestContext) MoveGpgKeyringTo(dst TestContext) error { 136 137 mv := func(f string) (err error) { 138 return os.Rename(path.Join(tc.Tp.GPGHome, f), filepath.Join(dst.Tp.GPGHome, f)) 139 } 140 141 if err := mv("secring.gpg"); err != nil { 142 return err 143 } 144 return mv("pubring.gpg") 145} 146 147func (tc *TestContext) GenerateGPGKeyring(ids ...string) error { 148 tc.T.Logf("generating gpg keyring in %s", tc.Tp.GPGHome) 149 fsk, err := os.Create(path.Join(tc.Tp.GPGHome, "secring.gpg")) 150 if err != nil { 151 return err 152 } 153 defer fsk.Close() 154 fpk, err := os.Create(path.Join(tc.Tp.GPGHome, "pubring.gpg")) 155 if err != nil { 156 return err 157 } 158 defer fpk.Close() 159 160 for _, id := range ids { 161 bundle, err := tc.MakePGPKey(id) 162 if err != nil { 163 return err 164 } 165 166 err = bundle.Entity.SerializePrivate(fsk, nil) 167 if err != nil { 168 return err 169 } 170 171 err = bundle.Entity.Serialize(fpk) 172 if err != nil { 173 return err 174 } 175 } 176 177 return nil 178} 179 180func (tc *TestContext) MakePGPKey(id string) (*PGPKeyBundle, error) { 181 arg := PGPGenArg{ 182 PrimaryBits: 1024, 183 SubkeyBits: 1024, 184 PGPUids: []string{id}, 185 } 186 err := arg.Init() 187 if err != nil { 188 return nil, err 189 } 190 err = arg.CreatePGPIDs() 191 if err != nil { 192 return nil, err 193 } 194 return GeneratePGPKeyBundle(tc.G, arg, tc.G.UI.GetLogUI()) 195} 196 197// SimulatServiceRestart simulates a shutdown and restart (for client 198// state). Used by tests that need to clear out cached login state 199// without logging out. 200func (tc *TestContext) SimulateServiceRestart() { 201 tc.G.simulateServiceRestart() 202} 203 204func (tc TestContext) ClearAllStoredSecrets() error { 205 m := NewMetaContextForTest(tc) 206 usernames, err := tc.G.GetUsersWithStoredSecrets(m.Ctx()) 207 if err != nil { 208 return err 209 } 210 for _, username := range usernames { 211 nu := NewNormalizedUsername(username) 212 err = ClearStoredSecret(m, nu) 213 if err != nil { 214 return err 215 } 216 } 217 return nil 218} 219 220func (tc TestContext) Context() context.Context { return WithLogTag(context.Background(), "TST") } 221func (tc TestContext) MetaContext() MetaContext { return NewMetaContextForTest(tc) } 222 223var setupTestMu sync.Mutex 224 225func setupTestContext(tb TestingTB, name string, tcPrev *TestContext) (tc TestContext, err error) { 226 setupTestMu.Lock() 227 defer setupTestMu.Unlock() 228 tc.Tp = &TestParameters{ 229 SecretStorePrimingDisabled: true, 230 } 231 232 g := NewGlobalContext() 233 234 // In debugging mode, dump all log, don't use the test logger. 235 // We only use the environment variable to discover debug mode 236 tc.origLog = g.Log 237 if val, _ := getEnvBool("KEYBASE_DEBUG"); !val { 238 g.Log = logger.NewTestLogger(tb) 239 } 240 241 buf := make([]byte, 5) 242 if _, err = rand.Read(buf); err != nil { 243 return 244 } 245 // Uniquify name, since multiple tests may use the same name. 246 develName := fmt.Sprintf("%s_%s", name, hex.EncodeToString(buf)) 247 248 g.Init() 249 g.Log.Debug("SetupTest %s", develName) 250 251 // Set up our testing parameters. We might add others later on 252 if tcPrev != nil { 253 tc.Tp = tcPrev.Tp 254 } else if tc.Tp.Home, err = ioutil.TempDir(os.TempDir(), develName); err != nil { 255 return 256 } 257 258 g.Log.Debug("SetupTest home directory: %s", tc.Tp.Home) 259 260 // might as well be the same directory... 261 tc.Tp.GPGHome = tc.Tp.Home 262 tc.Tp.GPGOptions = []string{"--homedir=" + tc.Tp.GPGHome} 263 264 tc.Tp.Debug = false 265 tc.Tp.Devel = true 266 tc.Tp.DevelName = develName 267 tc.Tp.DevelPrefix = name 268 269 g.Env.Test = tc.Tp 270 271 // SecretStoreFile needs test home directory 272 g.secretStoreMu.Lock() 273 m := NewMetaContextTODO(g) 274 g.secretStore = NewSecretStoreLocked(m) 275 g.secretStoreMu.Unlock() 276 277 err = g.ConfigureLogging(nil) 278 if err != nil { 279 return TestContext{}, err 280 } 281 282 if err = g.ConfigureAPI(); err != nil { 283 return 284 } 285 286 // use stub engine for external api 287 g.XAPI = NewStubAPIEngine(g) 288 289 if err = g.ConfigureConfig(); err != nil { 290 return 291 } 292 if err = g.ConfigureTimers(); err != nil { 293 return 294 } 295 if err = g.ConfigureCaches(); err != nil { 296 return 297 } 298 if err = g.ConfigureMerkleClient(); err != nil { 299 return 300 } 301 g.UI = &nullui{gctx: g} 302 if err = g.UI.Configure(); err != nil { 303 return 304 } 305 if err = g.ConfigureKeyring(); err != nil { 306 return 307 } 308 309 g.GregorState = &FakeGregorState{} 310 g.SetUIDMapper(NewTestUIDMapper(g.GetUPAKLoader())) 311 tc.G = g 312 tc.T = tb 313 314 // Periodically log in the background until `Cleanup` is called. Tests that 315 // forget to call this will panic because of logging after the test 316 // completes. 317 cleanupCh := make(chan struct{}) 318 tc.cleanupCh = cleanupCh 319 tc.eg = &errgroup.Group{} 320 tc.eg.Go(func() error { 321 log := g.Log.CloneWithAddedDepth(1) 322 log.Debug("TestContext bg loop starting up") 323 for { 324 select { 325 case <-cleanupCh: 326 log.Debug("TestContext bg loop shutting down") 327 return nil 328 case <-time.After(time.Second): 329 log.Debug("TestContext bg loop not cleaned up yet") 330 } 331 } 332 }) 333 334 return 335} 336 337// The depth argument is now ignored. 338func SetupTest(tb TestingTB, name string, depth int) (tc TestContext) { 339 var err error 340 tc, err = setupTestContext(tb, name, nil) 341 if err != nil { 342 tb.Fatal(err) 343 } 344 if os.Getenv("KEYBASE_LOG_SETUPTEST_FUNCS") != "" { 345 depth := 0 346 // Walk up the stackframe looking for the function that starts with "Test". 347 for { 348 pc, file, line, ok := runtime.Caller(depth) 349 if ok { 350 fn := runtime.FuncForPC(pc) 351 fnName := filepath.Base(fn.Name()) 352 if !strings.Contains(fnName, ".Test") { 353 // Not the right frame. Bump depth and loop again. 354 depth++ 355 continue 356 } 357 // This is the right frame. 358 fmt.Fprintf(os.Stderr, "- SetupTest %s %s:%d\n", filepath.Base(fn.Name()), filepath.Base(file), line) 359 } else { 360 // We've walked off the end of the stack without finding what we were looking for. 361 fmt.Fprintf(os.Stderr, "- SetupTest FAILED TO GET STACKFRAME") 362 } 363 break 364 } 365 } 366 367 AddEnvironmentFeatureForTest(tc, EnvironmentFeatureAllowHighSkips) 368 // If journeycards are disabled, this may be helpful to get tests to pass: 369 // AddEnvironmentFeatureForTest(tc, FeatureJourneycard) 370 // AddEnvironmentFeatureForTest(tc, FeatureJourneycard) 371 372 return tc 373} 374 375func (tc *TestContext) SetRuntimeDir(s string) { 376 tc.Tp.RuntimeDir = s 377 tc.G.Env.Test.RuntimeDir = s 378} 379 380func (tc TestContext) Clone() (ret TestContext) { 381 var err error 382 ret, err = setupTestContext(tc.T, "", &tc) 383 if err != nil { 384 tc.T.Fatal(err) 385 } 386 return ret 387} 388 389type nullui struct { 390 gctx *GlobalContext 391} 392 393func (n *nullui) Printf(f string, args ...interface{}) (int, error) { 394 return fmt.Printf(f, args...) 395} 396 397func (n *nullui) PrintfStderr(f string, args ...interface{}) (int, error) { 398 return fmt.Fprintf(os.Stderr, f, args...) 399} 400 401func (n *nullui) PrintfUnescaped(f string, args ...interface{}) (int, error) { 402 return fmt.Printf(f, args...) 403} 404 405func (n *nullui) GetDumbOutputUI() DumbOutputUI { 406 return n 407} 408 409func (n *nullui) GetIdentifyUI() IdentifyUI { 410 return nil 411} 412func (n *nullui) GetIdentifyTrackUI() IdentifyUI { 413 return nil 414} 415func (n *nullui) GetLoginUI() LoginUI { 416 return nil 417} 418func (n *nullui) GetTerminalUI() TerminalUI { 419 return nil 420} 421func (n *nullui) GetSecretUI() SecretUI { 422 return nil 423} 424func (n *nullui) GetProveUI() ProveUI { 425 return nil 426} 427func (n *nullui) GetGPGUI() GPGUI { 428 return nil 429} 430func (n *nullui) GetLogUI() LogUI { 431 return n.gctx.Log 432} 433func (n *nullui) GetPgpUI() PgpUI { 434 return nil 435} 436func (n *nullui) GetProvisionUI(KexRole) ProvisionUI { 437 return nil 438} 439func (n *nullui) Prompt(string, bool, Checker) (string, error) { 440 return "", nil 441} 442func (n *nullui) PromptForConfirmation(prompt string) error { 443 return nil 444} 445func (n *nullui) Configure() error { 446 return nil 447} 448func (n *nullui) Shutdown() error { 449 return nil 450} 451 452type TestSecretUI struct { 453 Passphrase string 454 StoreSecret bool 455 CalledGetPassphrase bool 456} 457 458func (t *TestSecretUI) GetPassphrase(p keybase1.GUIEntryArg, terminal *keybase1.SecretEntryArg) (keybase1.GetPassphraseRes, error) { 459 t.CalledGetPassphrase = true 460 return keybase1.GetPassphraseRes{ 461 Passphrase: t.Passphrase, 462 StoreSecret: t.StoreSecret, 463 }, nil 464} 465 466type TestCancelSecretUI struct { 467 CallCount int 468} 469 470func (t *TestCancelSecretUI) GetPassphrase(_ keybase1.GUIEntryArg, _ *keybase1.SecretEntryArg) (keybase1.GetPassphraseRes, error) { 471 t.CallCount++ 472 return keybase1.GetPassphraseRes{}, InputCanceledError{} 473} 474 475type TestCountSecretUI struct { 476 Passphrase string 477 StoreSecret bool 478 CallCount int 479} 480 481func (t *TestCountSecretUI) GetPassphrase(p keybase1.GUIEntryArg, terminal *keybase1.SecretEntryArg) (keybase1.GetPassphraseRes, error) { 482 t.CallCount++ 483 return keybase1.GetPassphraseRes{ 484 Passphrase: t.Passphrase, 485 StoreSecret: t.StoreSecret, 486 }, nil 487} 488 489type TestLoginUI struct { 490 Username string 491 RevokeBackup bool 492 CalledGetEmailOrUsername int 493 ResetAccount keybase1.ResetPromptResponse 494 PassphraseRecovery bool 495} 496 497var _ LoginUI = (*TestLoginUI)(nil) 498 499func (t *TestLoginUI) GetEmailOrUsername(_ context.Context, _ int) (string, error) { 500 t.CalledGetEmailOrUsername++ 501 return t.Username, nil 502} 503 504func (t *TestLoginUI) PromptRevokePaperKeys(_ context.Context, arg keybase1.PromptRevokePaperKeysArg) (bool, error) { 505 return t.RevokeBackup, nil 506} 507 508func (t *TestLoginUI) DisplayPaperKeyPhrase(_ context.Context, arg keybase1.DisplayPaperKeyPhraseArg) error { 509 return nil 510} 511 512func (t *TestLoginUI) DisplayPrimaryPaperKey(_ context.Context, arg keybase1.DisplayPrimaryPaperKeyArg) error { 513 return nil 514} 515 516func (t *TestLoginUI) PromptResetAccount(_ context.Context, arg keybase1.PromptResetAccountArg) (keybase1.ResetPromptResponse, error) { 517 return t.ResetAccount, nil 518} 519 520func (t *TestLoginUI) DisplayResetProgress(_ context.Context, arg keybase1.DisplayResetProgressArg) error { 521 return nil 522} 523 524func (t *TestLoginUI) ExplainDeviceRecovery(_ context.Context, arg keybase1.ExplainDeviceRecoveryArg) error { 525 return nil 526} 527 528func (t *TestLoginUI) PromptPassphraseRecovery(_ context.Context, arg keybase1.PromptPassphraseRecoveryArg) (bool, error) { 529 return t.PassphraseRecovery, nil 530} 531 532func (t *TestLoginUI) ChooseDeviceToRecoverWith(_ context.Context, arg keybase1.ChooseDeviceToRecoverWithArg) (keybase1.DeviceID, error) { 533 return "", nil 534} 535 536func (t *TestLoginUI) DisplayResetMessage(_ context.Context, arg keybase1.DisplayResetMessageArg) error { 537 return nil 538} 539 540type TestLoginCancelUI struct { 541 TestLoginUI 542} 543 544func (t *TestLoginCancelUI) GetEmailOrUsername(_ context.Context, _ int) (string, error) { 545 return "", InputCanceledError{} 546} 547 548type FakeGregorState struct { 549 dismissedIDs []gregor.MsgID 550} 551 552var _ GregorState = (*FakeGregorState)(nil) 553 554func (f *FakeGregorState) State(_ context.Context) (gregor.State, error) { 555 return gregor1.State{}, nil 556} 557 558func (f *FakeGregorState) UpdateCategory(ctx context.Context, cat string, body []byte, 559 dtime gregor1.TimeOrOffset) (gregor1.MsgID, error) { 560 return gregor1.MsgID{}, nil 561} 562 563func (f *FakeGregorState) InjectItem(ctx context.Context, cat string, body []byte, dtime gregor1.TimeOrOffset) (gregor1.MsgID, error) { 564 return gregor1.MsgID{}, nil 565} 566 567func (f *FakeGregorState) DismissItem(_ context.Context, cli gregor1.IncomingInterface, id gregor.MsgID) error { 568 f.dismissedIDs = append(f.dismissedIDs, id) 569 return nil 570} 571 572func (f *FakeGregorState) LocalDismissItem(ctx context.Context, id gregor.MsgID) error { 573 return nil 574} 575 576func (f *FakeGregorState) PeekDismissedIDs() []gregor.MsgID { 577 return f.dismissedIDs 578} 579 580func (f *FakeGregorState) DismissCategory(ctx context.Context, cat gregor1.Category) error { 581 return nil 582} 583 584type TestUIDMapper struct { 585 ul UPAKLoader 586} 587 588func NewTestUIDMapper(ul UPAKLoader) TestUIDMapper { 589 return TestUIDMapper{ 590 ul: ul, 591 } 592} 593 594func (t TestUIDMapper) ClearUIDFullName(_ context.Context, _ UIDMapperContext, _ keybase1.UID) error { 595 return nil 596} 597 598func (t TestUIDMapper) ClearUIDAtEldestSeqno(_ context.Context, _ UIDMapperContext, _ keybase1.UID, _ keybase1.Seqno) error { 599 return nil 600} 601 602func (t TestUIDMapper) CheckUIDAgainstUsername(uid keybase1.UID, un NormalizedUsername) bool { 603 return true 604} 605 606func (t TestUIDMapper) MapHardcodedUsernameToUID(un NormalizedUsername) keybase1.UID { 607 if un.String() == "max" { 608 return keybase1.UID("dbb165b7879fe7b1174df73bed0b9500") 609 } 610 return keybase1.UID("") 611} 612 613func (t TestUIDMapper) InformOfEldestSeqno(ctx context.Context, g UIDMapperContext, uv keybase1.UserVersion) (bool, error) { 614 return true, nil 615} 616 617func (t TestUIDMapper) MapUIDsToUsernamePackages(ctx context.Context, g UIDMapperContext, uids []keybase1.UID, fullNameFreshness time.Duration, networkTimeBudget time.Duration, forceNetworkForFullNames bool) ([]UsernamePackage, error) { 618 var res []UsernamePackage 619 for _, uid := range uids { 620 name, err := t.ul.LookupUsernameUPAK(ctx, uid) 621 if err != nil { 622 return nil, err 623 } 624 res = append(res, UsernamePackage{NormalizedUsername: name}) 625 } 626 return res, nil 627} 628 629func (t TestUIDMapper) SetTestingNoCachingMode(enabled bool) { 630 631} 632 633func (t TestUIDMapper) MapUIDsToUsernamePackagesOffline(ctx context.Context, g UIDMapperContext, uids []keybase1.UID, fullNameFreshness time.Duration) ([]UsernamePackage, error) { 634 // Just call MapUIDsToUsernamePackages. TestUIDMapper does not respect 635 // freshness, network budget, nor forceNetwork arguments. 636 return t.MapUIDsToUsernamePackages(ctx, g, uids, fullNameFreshness, 0, true) 637} 638 639func NewMetaContextForTest(tc TestContext) MetaContext { 640 return NewMetaContextBackground(tc.G).WithLogTag("TST") 641} 642 643func NewMetaContextForTestWithLogUI(tc TestContext) MetaContext { 644 return NewMetaContextForTest(tc).WithUIs(UIs{ 645 LogUI: tc.G.UI.GetLogUI(), 646 }) 647} 648 649func CreateClonedDevice(tc TestContext, m MetaContext) { 650 runAndGetDeviceCloneState := func() DeviceCloneState { 651 _, _, err := UpdateDeviceCloneState(m) 652 require.NoError(tc.T, err) 653 d, err := GetDeviceCloneState(m) 654 require.NoError(tc.T, err) 655 return d 656 } 657 // setup: perform two runs, and then manually persist the earlier 658 // prior token to simulate a subsequent run by a cloned device 659 d0 := runAndGetDeviceCloneState() 660 runAndGetDeviceCloneState() 661 err := SetDeviceCloneState(m, d0) 662 require.NoError(tc.T, err) 663 664 d := runAndGetDeviceCloneState() 665 require.True(tc.T, d.IsClone()) 666} 667 668func AddEnvironmentFeatureForTest(tc TestContext, feature Feature) { 669 tc.Tp.EnvironmentFeatureFlags = append(tc.Tp.EnvironmentFeatureFlags, feature) 670} 671 672func RemoveEnvironmentFeatureForTest(tp *TestParameters, feature Feature) { 673 var flags FeatureFlags 674 for _, flag := range tp.EnvironmentFeatureFlags { 675 if flag != feature { 676 flags = append(flags, flag) 677 } 678 } 679 tp.EnvironmentFeatureFlags = flags 680} 681 682// newSecretStoreLockedForTests is a simple function to create 683// SecretStoreLocked for the purposes of unit tests outside of libkb package 684// which need finer control over how secret store is configured. 685// 686// Omitting dataDir argument will create memory-only secret store, similar to 687// how disabling "remember passphrase" would work. 688func newSecretStoreLockedForTests(m MetaContext, dataDir string) *SecretStoreLocked { 689 var disk SecretStoreAll 690 mem := NewSecretStoreMem() 691 if dataDir != "" { 692 disk = NewSecretStoreFile(dataDir) 693 } 694 695 return &SecretStoreLocked{ 696 mem: mem, 697 disk: disk, 698 } 699} 700 701func ReplaceSecretStoreForTests(tc TestContext, dataDir string) { 702 g := tc.G 703 g.secretStoreMu.Lock() 704 g.secretStore = newSecretStoreLockedForTests(NewMetaContextForTest(tc), dataDir) 705 g.secretStoreMu.Unlock() 706} 707 708func CreateReadOnlySecretStoreDir(tc TestContext) (string, func()) { 709 td, err := ioutil.TempDir("", "ss") 710 require.NoError(tc.T, err) 711 712 // Change mode of test dir to read-only so secret store on this dir can 713 // fail. 714 fi, err := os.Stat(td) 715 require.NoError(tc.T, err) 716 oldMode := fi.Mode() 717 _ = os.Chmod(td, 0400) 718 719 cleanup := func() { 720 _ = os.Chmod(td, oldMode) 721 if err := os.RemoveAll(td); err != nil { 722 tc.T.Log(err) 723 } 724 } 725 726 return td, cleanup 727} 728