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