1// Copyright 2015 Keybase, Inc. All rights reserved. Use of
2// this source code is governed by the included BSD license.
3
4package engine
5
6import (
7	"fmt"
8	"testing"
9	"time"
10
11	"golang.org/x/net/context"
12
13	"github.com/keybase/client/go/jsonhelpers"
14	libkb "github.com/keybase/client/go/libkb"
15	keybase1 "github.com/keybase/client/go/protocol/keybase1"
16	"github.com/stretchr/testify/assert"
17	"github.com/stretchr/testify/require"
18)
19
20type ProveUIMock struct {
21	username, recheck, overwrite, warning, checked bool
22	postID                                         string
23	outputInstructionsHook                         func(context.Context, keybase1.OutputInstructionsArg) error
24	okToCheckHook                                  func(context.Context, keybase1.OkToCheckArg) (bool, string, error)
25	checkingHook                                   func(context.Context, keybase1.CheckingArg) error
26}
27
28func (p *ProveUIMock) PromptOverwrite(_ context.Context, arg keybase1.PromptOverwriteArg) (bool, error) {
29	p.overwrite = true
30	return true, nil
31}
32
33func (p *ProveUIMock) PromptUsername(_ context.Context, arg keybase1.PromptUsernameArg) (string, error) {
34	p.username = true
35	return "", nil
36}
37
38func (p *ProveUIMock) OutputPrechecks(_ context.Context, arg keybase1.OutputPrechecksArg) error {
39	return nil
40}
41
42func (p *ProveUIMock) PreProofWarning(_ context.Context, arg keybase1.PreProofWarningArg) (bool, error) {
43	p.warning = true
44	return true, nil
45}
46
47func (p *ProveUIMock) OutputInstructions(ctx context.Context, arg keybase1.OutputInstructionsArg) error {
48	if p.outputInstructionsHook != nil {
49		return p.outputInstructionsHook(ctx, arg)
50	}
51	return nil
52}
53
54func (p *ProveUIMock) OkToCheck(ctx context.Context, arg keybase1.OkToCheckArg) (bool, error) {
55	if !p.checked {
56		p.checked = true
57		ok, postID, err := p.okToCheckHook(ctx, arg)
58		p.postID = postID
59		return ok, err
60	}
61	return false, fmt.Errorf("Check should have worked the first time!")
62}
63
64func (p *ProveUIMock) Checking(ctx context.Context, arg keybase1.CheckingArg) (err error) {
65	if p.checkingHook != nil {
66		err = p.checkingHook(ctx, arg)
67	}
68	p.checked = true
69	return err
70}
71
72func (p *ProveUIMock) ContinueChecking(ctx context.Context, _ int) (bool, error) {
73	return true, nil
74}
75
76func (p *ProveUIMock) DisplayRecheckWarning(_ context.Context, arg keybase1.DisplayRecheckWarningArg) error {
77	p.recheck = true
78	return nil
79}
80
81func proveRooter(g *libkb.GlobalContext, fu *FakeUser, sigVersion libkb.SigVersion) (*ProveUIMock, keybase1.SigID, error) {
82	return proveRooterWithSecretUI(g, fu, fu.NewSecretUI(), sigVersion)
83}
84
85func proveRooterWithSecretUI(g *libkb.GlobalContext, fu *FakeUser, secretUI libkb.SecretUI, sigVersion libkb.SigVersion) (*ProveUIMock, keybase1.SigID, error) {
86	sv := keybase1.SigVersion(sigVersion)
87	arg := keybase1.StartProofArg{
88		Service:      "rooter",
89		Username:     fu.Username,
90		Force:        false,
91		PromptPosted: true,
92		SigVersion:   &sv,
93	}
94	eng := NewProve(g, &arg)
95
96	okToCheckHook := func(ctx context.Context, arg keybase1.OkToCheckArg) (bool, string, error) {
97		sigID := eng.sigID
98		if sigID.IsNil() {
99			return false, "", fmt.Errorf("empty sigID; can't make a post")
100		}
101		apiArg := libkb.APIArg{
102			Endpoint:    "rooter",
103			SessionType: libkb.APISessionTypeREQUIRED,
104			Args: libkb.HTTPArgs{
105				"post": libkb.S{Val: sigID.ToMediumID()},
106			},
107		}
108		res, err := g.API.Post(libkb.NewMetaContext(ctx, g), apiArg)
109		ok := err == nil
110		var postID string
111		if ok {
112			pid, err := res.Body.AtKey("post_id").GetString()
113			if err == nil {
114				postID = pid
115			}
116		}
117		return ok, postID, err
118	}
119
120	proveUI := &ProveUIMock{okToCheckHook: okToCheckHook}
121
122	uis := libkb.UIs{
123		LogUI:    g.UI.GetLogUI(),
124		SecretUI: secretUI,
125		ProveUI:  proveUI,
126	}
127	m := libkb.NewMetaContextTODO(g).WithUIs(uis)
128	err := RunEngine2(m, eng)
129	return proveUI, eng.sigID, err
130}
131
132func proveRooterFail(g *libkb.GlobalContext, fu *FakeUser, sigVersion libkb.SigVersion) (*ProveUIMock, error) {
133	sv := keybase1.SigVersion(sigVersion)
134	arg := keybase1.StartProofArg{
135		Service:      "rooter",
136		Username:     fu.Username,
137		Force:        false,
138		PromptPosted: true,
139		SigVersion:   &sv,
140	}
141
142	eng := NewProve(g, &arg)
143
144	okToCheckHook := func(ctx context.Context, arg keybase1.OkToCheckArg) (bool, string, error) {
145		apiArg := libkb.APIArg{
146			Endpoint:    "rooter",
147			SessionType: libkb.APISessionTypeREQUIRED,
148			Args: libkb.HTTPArgs{
149				"post": libkb.S{Val: "XXXXXXX"},
150			},
151		}
152		res, err := g.API.Post(libkb.NewMetaContext(ctx, g), apiArg)
153		ok := err == nil
154		var postID string
155		if ok {
156			pid, err := res.Body.AtKey("post_id").GetString()
157			if err == nil {
158				postID = pid
159			}
160		}
161		return ok, postID, err
162	}
163
164	proveUI := &ProveUIMock{okToCheckHook: okToCheckHook}
165
166	uis := libkb.UIs{
167		LogUI:    g.UI.GetLogUI(),
168		SecretUI: fu.NewSecretUI(),
169		ProveUI:  proveUI,
170	}
171	m := libkb.NewMetaContextTODO(g).WithUIs(uis)
172	err := RunEngine2(m, eng)
173	return proveUI, err
174}
175
176func proveRooterRemove(g *libkb.GlobalContext, postID string) error {
177	apiArg := libkb.APIArg{
178		Endpoint:    "rooter/delete",
179		SessionType: libkb.APISessionTypeREQUIRED,
180		Args: libkb.HTTPArgs{
181			"post_id": libkb.S{Val: postID},
182		},
183	}
184	_, err := g.API.Post(libkb.NewMetaContextTODO(g), apiArg)
185	return err
186}
187
188func proveRooterOther(g *libkb.GlobalContext, fu *FakeUser, rooterUsername string, sigVersion libkb.SigVersion) (*ProveUIMock, keybase1.SigID, error) {
189	sv := keybase1.SigVersion(sigVersion)
190	arg := keybase1.StartProofArg{
191		Service:      "rooter",
192		Username:     rooterUsername,
193		Force:        false,
194		PromptPosted: true,
195		SigVersion:   &sv,
196	}
197
198	eng := NewProve(g, &arg)
199
200	okToCheckHook := func(ctx context.Context, arg keybase1.OkToCheckArg) (bool, string, error) {
201		sigID := eng.sigID
202		if sigID.IsNil() {
203			return false, "", fmt.Errorf("empty sigID; can't make a post")
204		}
205		apiArg := libkb.APIArg{
206			Endpoint:    "rooter",
207			SessionType: libkb.APISessionTypeREQUIRED,
208			Args: libkb.HTTPArgs{
209				"post":     libkb.S{Val: sigID.ToMediumID()},
210				"username": libkb.S{Val: rooterUsername},
211			},
212		}
213		res, err := g.API.Post(libkb.NewMetaContext(ctx, g), apiArg)
214		ok := err == nil
215		var postID string
216		if ok {
217			pid, err := res.Body.AtKey("post_id").GetString()
218			if err == nil {
219				postID = pid
220			}
221		}
222		return ok, postID, err
223	}
224
225	proveUI := &ProveUIMock{okToCheckHook: okToCheckHook}
226
227	uis := libkb.UIs{
228		LogUI:    g.UI.GetLogUI(),
229		SecretUI: fu.NewSecretUI(),
230		ProveUI:  proveUI,
231	}
232	m := libkb.NewMetaContextTODO(g).WithUIs(uis)
233	err := RunEngine2(m, eng)
234	return proveUI, eng.sigID, err
235}
236
237func proveGubbleSocial(tc libkb.TestContext, fu *FakeUser, sigVersion libkb.SigVersion) keybase1.SigID {
238	return proveGubbleUniverse(tc, "gubble.social", "gubble_social", fu, sigVersion)
239}
240
241func proveGubbleCloud(tc libkb.TestContext, fu *FakeUser, sigVersion libkb.SigVersion) keybase1.SigID {
242	return proveGubbleUniverse(tc, "gubble.cloud", "gubble_cloud", fu, sigVersion)
243}
244
245func proveGubbleUniverse(tc libkb.TestContext, serviceName, endpoint string, fu *FakeUser, sigVersion libkb.SigVersion) keybase1.SigID {
246	tc.T.Logf("proof for %s", serviceName)
247	g := tc.G
248	sv := keybase1.SigVersion(sigVersion)
249	proofService := g.GetProofServices().GetServiceType(context.Background(), serviceName)
250	require.NotNil(tc.T, proofService)
251
252	// Post a proof to the testing generic social service
253	arg := keybase1.StartProofArg{
254		Service:      proofService.GetTypeName(),
255		Username:     fu.Username,
256		Force:        false,
257		PromptPosted: true,
258		SigVersion:   &sv,
259	}
260	eng := NewProve(g, &arg)
261
262	// Post the proof to the gubble network and verify the sig hash
263	outputInstructionsHook := func(ctx context.Context, _ keybase1.OutputInstructionsArg) error {
264		sigID := eng.sigID
265		require.False(tc.T, sigID.IsNil())
266		mctx := libkb.NewMetaContext(ctx, g)
267
268		apiArg := libkb.APIArg{
269			Endpoint:    fmt.Sprintf("gubble_universe/%s", endpoint),
270			SessionType: libkb.APISessionTypeREQUIRED,
271			Args: libkb.HTTPArgs{
272				"sig_hash":      libkb.S{Val: sigID.String()},
273				"username":      libkb.S{Val: fu.Username},
274				"kb_username":   libkb.S{Val: fu.Username},
275				"kb_ua":         libkb.S{Val: libkb.UserAgent},
276				"json_redirect": libkb.B{Val: true},
277			},
278		}
279		_, err := g.API.Post(libkb.NewMetaContext(ctx, g), apiArg)
280		require.NoError(tc.T, err)
281
282		apiArg = libkb.APIArg{
283			Endpoint:    fmt.Sprintf("gubble_universe/%s/%s/proofs", endpoint, fu.Username),
284			SessionType: libkb.APISessionTypeNONE,
285		}
286		res, err := g.GetAPI().Get(mctx, apiArg)
287		require.NoError(tc.T, err)
288		objects, err := jsonhelpers.AtSelectorPath(res.Body, []keybase1.SelectorEntry{
289			{
290				IsKey: true,
291				Key:   "res",
292			},
293			{
294				IsKey: true,
295				Key:   "keybase_proofs",
296			},
297		}, tc.T.Logf, libkb.NewInvalidPVLSelectorError)
298		require.NoError(tc.T, err)
299		require.Len(tc.T, objects, 1)
300
301		var proofs []keybase1.ParamProofJSON
302		err = objects[0].UnmarshalAgain(&proofs)
303		require.NoError(tc.T, err)
304		require.True(tc.T, len(proofs) >= 1)
305		for _, proof := range proofs {
306			if proof.KbUsername == fu.Username && sigID.Eq(proof.SigHash) {
307				return nil
308			}
309		}
310		assert.Fail(tc.T, "proof not found")
311		return nil
312	}
313
314	proveUI := &ProveUIMock{outputInstructionsHook: outputInstructionsHook}
315	uis := libkb.UIs{
316		LogUI:    g.UI.GetLogUI(),
317		SecretUI: fu.NewSecretUI(),
318		ProveUI:  proveUI,
319	}
320	m := libkb.NewMetaContextTODO(g).WithUIs(uis)
321	err := RunEngine2(m, eng)
322	checkFailed(tc.T.(testing.TB))
323	require.NoError(tc.T, err)
324	require.False(tc.T, proveUI.overwrite)
325	require.False(tc.T, proveUI.warning)
326	require.False(tc.T, proveUI.recheck)
327	require.True(tc.T, proveUI.checked)
328	return eng.sigID
329}
330
331func proveGubbleSocialFail(tc libkb.TestContext, fu *FakeUser, sigVersion libkb.SigVersion) {
332	g := tc.G
333	sv := keybase1.SigVersion(sigVersion)
334	proofService := g.GetProofServices().GetServiceType(context.Background(), "gubble.social")
335	require.NotNil(tc.T, proofService, "expected to find gubble.social service type")
336	arg := keybase1.StartProofArg{
337		Service:      proofService.GetTypeName(),
338		Username:     fu.Username,
339		Force:        false,
340		PromptPosted: true,
341		SigVersion:   &sv,
342	}
343
344	eng := NewProve(g, &arg)
345	proveUI := &ProveUIMock{}
346	uis := libkb.UIs{
347		LogUI:    g.UI.GetLogUI(),
348		SecretUI: fu.NewSecretUI(),
349		ProveUI:  proveUI,
350	}
351	mctx := libkb.NewMetaContextTODO(g).WithUIs(uis)
352	mctx, cancel1 := mctx.WithTimeout(12 * time.Second)
353	defer cancel1()
354	mctx, cancel2 := libkb.NewMetaContextTODO(g).WithUIs(uis).WithContextCancel()
355	defer cancel2()
356
357	proveUI.checkingHook = func(_ context.Context, _ keybase1.CheckingArg) error {
358		if mctx.Ctx().Err() != nil {
359			// This is supposed to be the first thing to cancel the context.
360			assert.Fail(tc.T, "unexpectedly cancelled")
361		}
362		cancel2()
363		return nil
364	}
365
366	// This proof will never succeed, so the Prove engine would never stop of its own accord.
367	err := RunEngine2(mctx, eng)
368	require.Error(tc.T, err)
369	checkFailed(tc.T.(testing.TB))
370}
371
372func checkFailed(t testing.TB) {
373	if t.Failed() {
374		// The test failed. Possibly in anothe goroutine. Look earlier in the logs for the real failure.
375		require.FailNow(t, "test already failed")
376	}
377}
378