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