code.vegaprotocol.io/vega@v0.79.0/wallet/service/v2/connections/manager_test.go (about) 1 // Copyright (C) 2023 Gobalsky Labs Limited 2 // 3 // This program is free software: you can redistribute it and/or modify 4 // it under the terms of the GNU Affero General Public License as 5 // published by the Free Software Foundation, either version 3 of the 6 // License, or (at your option) any later version. 7 // 8 // This program is distributed in the hope that it will be useful, 9 // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 // GNU Affero General Public License for more details. 12 // 13 // You should have received a copy of the GNU Affero General Public License 14 // along with this program. If not, see <http://www.gnu.org/licenses/>. 15 16 package connections_test 17 18 import ( 19 "context" 20 "fmt" 21 "testing" 22 "time" 23 24 "code.vegaprotocol.io/vega/libs/jsonrpc" 25 vgrand "code.vegaprotocol.io/vega/libs/rand" 26 "code.vegaprotocol.io/vega/wallet/api" 27 apimocks "code.vegaprotocol.io/vega/wallet/api/mocks" 28 "code.vegaprotocol.io/vega/wallet/service/v2/connections" 29 "code.vegaprotocol.io/vega/wallet/service/v2/connections/mocks" 30 "code.vegaprotocol.io/vega/wallet/wallet" 31 32 "github.com/golang/mock/gomock" 33 "github.com/stretchr/testify/assert" 34 "github.com/stretchr/testify/require" 35 ) 36 37 func TestManager(t *testing.T) { 38 t.Run("Start a new session from scratch succeeds", testStartNewSessionFromScratchSucceeds) 39 t.Run("Loading long-living connections succeeds", testLoadingLongLivingConnectionsSucceeding) 40 t.Run("Asynchronous updates on wallets update the connections", testAsynchronousUpdateOnWalletsUpdateTheConnection) 41 t.Run("Asynchronous updates on long-living tokens update the connections", testAsynchronousUpdateOnLongLivingTokensUpdateTheConnection) 42 t.Run("Reloading previous sessions succeeds", testReloadingPreviousSessionsSucceeds) 43 } 44 45 func testStartNewSessionFromScratchSucceeds(t *testing.T) { 46 ctx, _ := randomTraceID(t) 47 48 // given 49 // The prefix with "b" will help to test the sorting while listing connections. 50 hostnameB := "b" + vgrand.RandomStr(5) 51 expectedWallet, expectedKeyPairs := randomWallet(t) 52 // Tainting one key to prove it's not loaded as an allowed key. 53 require.NoError(t, expectedWallet.TaintKey(expectedKeyPairs[1].PublicKey())) 54 55 // setup 56 manager := newTestManagerBuilder(t) 57 manager.timeService.EXPECT().Now().AnyTimes().Return(time.Now()) 58 manager.walletStore.EXPECT().OnUpdate(gomock.Any()).Times(1) 59 manager.tokenStore.EXPECT().OnUpdate(gomock.Any()).Times(1) 60 manager.tokenStore.EXPECT().ListTokens().Times(1).Return(nil, nil) 61 manager.sessionStore.EXPECT().ListSessions(gomock.Any()).Times(1).Return(nil, nil) 62 manager.sessionStore.EXPECT().TrackSession(gomock.Any()).Times(1).Return(nil) 63 manager.Build() 64 65 // when initiating the session connection for the first time, we get a token back. 66 firstSession, err := manager.StartSession(hostnameB, expectedWallet) 67 68 // then 69 require.NoError(t, err) 70 assert.NotEmpty(t, firstSession) 71 72 // when 73 firstCw, err := manager.ConnectedWallet(ctx, hostnameB, firstSession) 74 75 // then 76 require.Nil(t, err) 77 assert.Equal(t, expectedWallet.Name(), firstCw.Name()) 78 assert.Equal(t, hostnameB, firstCw.Hostname()) 79 // The used hostname is not permitted to list keys on the wallet. 80 assert.Equal(t, expectedWallet.Permissions(hostnameB).CanListKeys(), firstCw.CanListKeys()) 81 // Since the hostname is not allowed to list keys, there is no allowed keys. 82 assert.Empty(t, firstCw.AllowedKeys()) 83 // Regular sessions require user intervention, and this, interaction. 84 assert.True(t, firstCw.RequireInteraction()) 85 86 // when using the token from different hostname, it fails. 87 _, err = manager.ConnectedWallet(ctx, vgrand.RandomStr(5), firstSession) 88 89 // then 90 require.NotNil(t, err) 91 assert.Equal(t, jsonrpc.NewServerError(api.ErrorCodeAuthenticationFailure, connections.ErrHostnamesMismatchForThisToken), err) 92 93 // setup 94 manager.sessionStore.EXPECT().TrackSession(gomock.Any()).Times(1).Return(nil) 95 96 // when re-initiating a session connection on same hostname-wallet pair, the 97 // original session is voided, and a new token is generated. 98 secondSession, err := manager.StartSession(hostnameB, expectedWallet) 99 100 // then the tokens are different 101 require.NoError(t, err) 102 assert.NotEmpty(t, secondSession) 103 assert.NotEqual(t, firstSession, secondSession) 104 105 // given 106 // The prefix with "a" will help to test the sorting while listing the connections. 107 hostnameA := "a" + vgrand.RandomStr(5) 108 109 // setup 110 manager.sessionStore.EXPECT().TrackSession(gomock.Any()).Times(1).Return(nil) 111 // Allowing a hostname to list keys. 112 require.NoError(t, expectedWallet.UpdatePermissions(hostnameA, wallet.Permissions{ 113 PublicKeys: wallet.PublicKeysPermission{ 114 Access: wallet.ReadAccess, 115 AllowedKeys: nil, 116 }, 117 })) 118 119 // then 120 thirdSession, err := manager.StartSession(hostnameA, expectedWallet) 121 122 // then a brand-new token is issued 123 require.NoError(t, err) 124 assert.NotEmpty(t, thirdSession) 125 assert.NotEqual(t, firstSession, thirdSession) 126 assert.NotEqual(t, secondSession, thirdSession) 127 128 // when 129 secondCw, err := manager.ConnectedWallet(ctx, hostnameA, thirdSession) 130 131 // then 132 require.Nil(t, err) 133 assert.Equal(t, expectedWallet.Name(), secondCw.Name()) 134 assert.Equal(t, hostnameA, secondCw.Hostname()) 135 // The used hostname is permitted to list keys on the wallet. 136 assert.Equal(t, expectedWallet.Permissions(hostnameA).CanListKeys(), secondCw.CanListKeys()) 137 // Since the hostname is allowed to list all keys, it will allow all non-tainted keys. 138 assertRightAllowedKeys(t, []wallet.KeyPair{expectedKeyPairs[0], expectedKeyPairs[2]}, secondCw.AllowedKeys()) 139 // Regular sessions require user intervention, and thus, interaction. 140 assert.True(t, secondCw.RequireInteraction()) 141 142 // given 143 // The prefix with "a" will help to test the sorting while listing the connections. 144 hostnameC := "c" + vgrand.RandomStr(5) 145 146 // setup 147 manager.sessionStore.EXPECT().TrackSession(gomock.Any()).Times(1).Return(nil) 148 // Allowing a hostname to list keys. 149 require.NoError(t, expectedWallet.UpdatePermissions(hostnameC, wallet.Permissions{ 150 PublicKeys: wallet.PublicKeysPermission{ 151 Access: wallet.ReadAccess, 152 AllowedKeys: []string{expectedKeyPairs[2].PublicKey()}, 153 }, 154 })) 155 156 // when 157 fourthSession, err := manager.StartSession(hostnameC, expectedWallet) 158 159 // then a brand-new token is issued 160 require.NoError(t, err) 161 assert.NotEmpty(t, fourthSession) 162 assert.NotEqual(t, firstSession, fourthSession) 163 assert.NotEqual(t, secondSession, fourthSession) 164 assert.NotEqual(t, thirdSession, fourthSession) 165 166 // when 167 thirdCw, err := manager.ConnectedWallet(ctx, hostnameC, fourthSession) 168 169 // then 170 require.Nil(t, err) 171 assert.Equal(t, expectedWallet.Name(), thirdCw.Name()) 172 assert.Equal(t, hostnameC, thirdCw.Hostname()) 173 // The used hostname is permitted to list keys on the wallet. 174 assert.Equal(t, expectedWallet.Permissions(hostnameC).CanListKeys(), thirdCw.CanListKeys()) 175 // Since the hostname is allowed to list all keys, it will allow all non-tainted keys. 176 assertRightAllowedKeys(t, []wallet.KeyPair{expectedKeyPairs[2]}, thirdCw.AllowedKeys()) 177 // Regular sessions require user intervention, and thus, interaction. 178 assert.True(t, thirdCw.RequireInteraction()) 179 180 // when 181 liveConnections := manager.ListSessionConnections() 182 183 // then 184 assert.Equal(t, []api.Connection{ 185 { 186 Hostname: hostnameA, 187 Wallet: expectedWallet.Name(), 188 }, { 189 Hostname: hostnameB, 190 Wallet: expectedWallet.Name(), 191 }, { 192 Hostname: hostnameC, 193 Wallet: expectedWallet.Name(), 194 }, 195 }, liveConnections) 196 197 // setup 198 manager.EndSessionConnection(hostnameA, expectedWallet.Name()) 199 200 // when listing the connections after one as been ended, the ended one is not 201 // listed. 202 liveConnections = manager.ListSessionConnections() 203 204 // then 205 assert.Equal(t, []api.Connection{ 206 { 207 Hostname: hostnameB, 208 Wallet: expectedWallet.Name(), 209 }, { 210 Hostname: hostnameC, 211 Wallet: expectedWallet.Name(), 212 }, 213 }, liveConnections) 214 215 // when trying to get the connection previously ended, it fails. 216 secondCw, err = manager.ConnectedWallet(ctx, hostnameC, thirdSession) 217 218 // then 219 require.NotNil(t, err) 220 assert.Equal(t, jsonrpc.NewServerError(api.ErrorCodeAuthenticationFailure, connections.ErrNoConnectionAssociatedThisAuthenticationToken), err) 221 222 // setup 223 manager.EndAllSessionConnections() 224 225 // when listing the connections after they've been all closed, none of them 226 // are listed. 227 liveConnections = manager.ListSessionConnections() 228 229 // then 230 assert.Empty(t, liveConnections) 231 } 232 233 func testLoadingLongLivingConnectionsSucceeding(t *testing.T) { 234 ctx := context.Background() 235 236 // given 237 someHostname := vgrand.RandomStr(4) 238 expectedWallet, expectedKeyPairs := randomWallet(t) 239 // Tainting one key to prove it's not loaded as an allowed key. 240 require.NoError(t, expectedWallet.TaintKey(expectedKeyPairs[1].PublicKey())) 241 expectedToken := connections.TokenDescription{ 242 CreationDate: time.Now(), 243 Token: randomToken(t), 244 Wallet: connections.WalletCredentials{ 245 Name: expectedWallet.Name(), 246 Passphrase: vgrand.RandomStr(5), 247 }, 248 } 249 expectedTokens := []connections.TokenSummary{ 250 { 251 Token: expectedToken.Token, 252 CreationDate: expectedToken.CreationDate, 253 }, 254 } 255 256 // setup 257 manager := newTestManagerBuilder(t) 258 manager.timeService.EXPECT().Now().AnyTimes().Return(time.Now()) 259 manager.walletStore.EXPECT().OnUpdate(gomock.Any()).Times(1) 260 manager.tokenStore.EXPECT().OnUpdate(gomock.Any()).Times(1) 261 manager.tokenStore.EXPECT().ListTokens().Times(1).Return(expectedTokens, nil) 262 manager.tokenStore.EXPECT().DescribeToken(expectedToken.Token).Times(1).Return(expectedToken, nil) 263 manager.walletStore.EXPECT().UnlockWallet(gomock.Any(), expectedToken.Wallet.Name, expectedToken.Wallet.Passphrase).Times(1).Return(nil) 264 manager.walletStore.EXPECT().GetWallet(gomock.Any(), expectedToken.Wallet.Name).Times(1).Return(expectedWallet, nil) 265 manager.sessionStore.EXPECT().ListSessions(gomock.Any()).Times(1).Return(nil, nil) 266 manager.Build() 267 268 // when retrieving the connection associated to the long-living token. 269 // Note: Long-living token are not tied to a hostname. 270 cw, err := manager.ConnectedWallet(ctx, someHostname, expectedToken.Token) 271 272 // then 273 require.Nil(t, err) 274 assert.Equal(t, expectedWallet.Name(), cw.Name()) 275 // There is no restriction on who can use a long-living token. 276 assert.Empty(t, cw.Hostname()) 277 // This is always allowed on long-living tokens. 278 assert.True(t, cw.CanListKeys()) 279 // Tainted keys are excluded from the allowed keys. 280 assertRightAllowedKeys(t, []wallet.KeyPair{expectedKeyPairs[0], expectedKeyPairs[2]}, cw.AllowedKeys()) 281 // Long-living token should not require user interaction. 282 assert.False(t, cw.RequireInteraction()) 283 284 // when trying to end the connection, it doesn't close it. 285 manager.EndSessionConnectionWithToken(expectedToken.Token) 286 unclosedCW, err := manager.ConnectedWallet(ctx, someHostname, expectedToken.Token) 287 288 // then 289 require.Nil(t, err) 290 // We can't close a long-living token connection. 291 assert.Equal(t, cw, unclosedCW) 292 293 // when ending all session connections, it doesn't close the long-living connection. 294 manager.EndAllSessionConnections() 295 unclosedCW, err = manager.ConnectedWallet(ctx, someHostname, expectedToken.Token) 296 297 // then 298 require.Nil(t, err) 299 // We can't close a long-living token connection. 300 assert.Equal(t, cw, unclosedCW) 301 } 302 303 func testAsynchronousUpdateOnWalletsUpdateTheConnection(t *testing.T) { 304 ctx, _ := randomTraceID(t) 305 306 // given 307 var onWalletUpdateCb func(context.Context, wallet.Event) 308 hostname := vgrand.RandomStr(5) 309 expectedWallet, expectedKeyPairs := randomWallet(t) 310 311 // setup 312 manager := newTestManagerBuilder(t) 313 manager.timeService.EXPECT().Now().AnyTimes().Return(time.Now()) 314 manager.walletStore.EXPECT().OnUpdate(gomock.Any()).Times(1).Do(func(cb func(context.Context, wallet.Event)) { 315 // Capturing the callback, so we can use it like the wallet store would. 316 onWalletUpdateCb = cb 317 }) 318 manager.tokenStore.EXPECT().OnUpdate(gomock.Any()).Times(1) 319 manager.tokenStore.EXPECT().ListTokens().Times(1).Return(nil, nil) 320 manager.sessionStore.EXPECT().ListSessions(gomock.Any()).Times(1).Return(nil, nil) 321 manager.sessionStore.EXPECT().TrackSession(gomock.Any()).Times(1).Return(nil) 322 manager.Build() 323 324 // when initiating the session connection for the first time, we get a token back. 325 session, err := manager.StartSession(hostname, expectedWallet) 326 327 // then 328 require.NoError(t, err) 329 assert.NotEmpty(t, session) 330 331 // when 332 cw, err := manager.ConnectedWallet(ctx, hostname, session) 333 334 // then 335 require.Nil(t, err) 336 assert.Equal(t, expectedWallet.Name(), cw.Name()) 337 assert.Equal(t, hostname, cw.Hostname()) 338 assert.False(t, cw.CanListKeys()) 339 assert.Empty(t, cw.AllowedKeys()) 340 assert.True(t, cw.RequireInteraction()) 341 342 // setup 343 // Add some permissions to the wallet to see if the connection is updated 344 // accordingly. 345 require.NoError(t, expectedWallet.UpdatePermissions(hostname, wallet.Permissions{ 346 PublicKeys: wallet.PublicKeysPermission{ 347 Access: wallet.ReadAccess, 348 AllowedKeys: []string{expectedKeyPairs[2].PublicKey()}, 349 }, 350 })) 351 352 // when 353 // Simulating an external wallet update. 354 onWalletUpdateCb(ctx, wallet.NewUnlockedWalletUpdatedEvent(expectedWallet)) 355 cw, err = manager.ConnectedWallet(ctx, hostname, session) 356 357 // then 358 require.Nil(t, err) 359 assert.Equal(t, expectedWallet.Name(), cw.Name()) 360 assert.Equal(t, hostname, cw.Hostname()) 361 assert.True(t, cw.CanListKeys()) 362 assertRightAllowedKeys(t, []wallet.KeyPair{expectedKeyPairs[2]}, cw.AllowedKeys()) 363 assert.True(t, cw.RequireInteraction()) 364 365 // when 366 // Simulating an external wallet update. 367 onWalletUpdateCb(ctx, wallet.NewUnlockedWalletUpdatedEvent(expectedWallet)) 368 cw, err = manager.ConnectedWallet(ctx, hostname, session) 369 370 // then 371 require.Nil(t, err) 372 assert.Equal(t, expectedWallet.Name(), cw.Name()) 373 assert.Equal(t, hostname, cw.Hostname()) 374 assert.True(t, cw.CanListKeys()) 375 assertRightAllowedKeys(t, []wallet.KeyPair{expectedKeyPairs[2]}, cw.AllowedKeys()) 376 assert.True(t, cw.RequireInteraction()) 377 378 // setup 379 previousName := expectedWallet.Name() 380 expectedWallet.SetName(vgrand.RandomStr(5)) 381 manager.walletStore.EXPECT().GetWallet(gomock.Any(), expectedWallet.Name()).Times(1).Return(expectedWallet, nil) 382 383 // when 384 // Simulating an external wallet rename. 385 onWalletUpdateCb(ctx, wallet.NewWalletRenamedEvent(previousName, expectedWallet.Name())) 386 cw, err = manager.ConnectedWallet(ctx, hostname, session) 387 388 // then 389 require.Nil(t, err) 390 assert.Equal(t, expectedWallet.Name(), cw.Name()) 391 assert.Equal(t, hostname, cw.Hostname()) 392 assert.True(t, cw.CanListKeys()) 393 assertRightAllowedKeys(t, []wallet.KeyPair{expectedKeyPairs[2]}, cw.AllowedKeys()) 394 assert.True(t, cw.RequireInteraction()) 395 396 // setup 397 manager.sessionStore.EXPECT().DeleteSession(gomock.Any(), session).Times(1).Return(nil) 398 399 // when 400 // Simulating an external wallet removal. 401 onWalletUpdateCb(ctx, wallet.NewWalletRemovedEvent(expectedWallet.Name())) 402 cw, err = manager.ConnectedWallet(ctx, hostname, session) 403 404 // then 405 require.NotNil(t, err) 406 assert.Equal(t, jsonrpc.NewServerError(api.ErrorCodeAuthenticationFailure, connections.ErrNoConnectionAssociatedThisAuthenticationToken), err) 407 require.Empty(t, cw) 408 } 409 410 func testAsynchronousUpdateOnLongLivingTokensUpdateTheConnection(t *testing.T) { 411 ctx, _ := randomTraceID(t) 412 413 // given 414 var onTokenUpdateCb func(context.Context, ...connections.TokenDescription) 415 passphrase := vgrand.RandomStr(5) 416 expectedWallet, expectedKeyPairs := randomWallet(t) 417 firstToken := connections.TokenDescription{ 418 CreationDate: time.Now(), 419 Token: randomToken(t), 420 Wallet: connections.WalletCredentials{ 421 Name: expectedWallet.Name(), 422 Passphrase: passphrase, 423 }, 424 } 425 expectedTokens := []connections.TokenSummary{ 426 { 427 Token: firstToken.Token, 428 CreationDate: firstToken.CreationDate, 429 }, 430 } 431 432 // setup 433 manager := newTestManagerBuilder(t) 434 manager.timeService.EXPECT().Now().AnyTimes().Return(time.Now()) 435 manager.walletStore.EXPECT().OnUpdate(gomock.Any()).Times(1) 436 manager.tokenStore.EXPECT().OnUpdate(gomock.Any()).Times(1).Do(func(cb func(context.Context, ...connections.TokenDescription)) { 437 // Capturing the callback, so we can use it like the token store would. 438 onTokenUpdateCb = cb 439 }) 440 manager.tokenStore.EXPECT().ListTokens().Times(1).Return(expectedTokens, nil) 441 manager.tokenStore.EXPECT().DescribeToken(firstToken.Token).Times(1).Return(firstToken, nil) 442 manager.walletStore.EXPECT().UnlockWallet(gomock.Any(), firstToken.Wallet.Name, firstToken.Wallet.Passphrase).Times(1).Return(nil) 443 manager.walletStore.EXPECT().GetWallet(gomock.Any(), firstToken.Wallet.Name).Times(1).Return(expectedWallet, nil) 444 manager.sessionStore.EXPECT().ListSessions(gomock.Any()).Times(1).Return(nil, nil) 445 manager.Build() 446 447 // when retrieving the connection associated to the long-living token. 448 // Note: Long-living token are not tied to a hostname. 449 cw, err := manager.ConnectedWallet(ctx, "", firstToken.Token) 450 451 // then 452 require.Nil(t, err) 453 assert.Equal(t, expectedWallet.Name(), cw.Name()) 454 assert.Empty(t, cw.Hostname()) 455 assert.True(t, cw.CanListKeys()) 456 assertRightAllowedKeys(t, []wallet.KeyPair{expectedKeyPairs[0], expectedKeyPairs[1], expectedKeyPairs[2]}, cw.AllowedKeys()) 457 assert.False(t, cw.RequireInteraction()) 458 459 // given 460 secondToken := connections.TokenDescription{ 461 CreationDate: time.Now(), 462 Token: randomToken(t), 463 Wallet: connections.WalletCredentials{ 464 Name: expectedWallet.Name(), 465 Passphrase: passphrase, 466 }, 467 } 468 469 // setup 470 manager.walletStore.EXPECT().UnlockWallet(ctx, firstToken.Wallet.Name, firstToken.Wallet.Passphrase).Times(1).Return(nil) 471 manager.walletStore.EXPECT().GetWallet(ctx, firstToken.Wallet.Name).Times(1).Return(expectedWallet, nil) 472 manager.walletStore.EXPECT().UnlockWallet(ctx, secondToken.Wallet.Name, secondToken.Wallet.Passphrase).Times(1).Return(nil) 473 manager.walletStore.EXPECT().GetWallet(ctx, secondToken.Wallet.Name).Times(1).Return(expectedWallet, nil) 474 475 // when simulating the creation of a second token. 476 onTokenUpdateCb(ctx, firstToken, secondToken) 477 478 // when 479 cw, err = manager.ConnectedWallet(ctx, "", secondToken.Token) 480 481 // then 482 require.Nil(t, err) 483 assert.Equal(t, expectedWallet.Name(), cw.Name()) 484 assert.Empty(t, cw.Hostname()) 485 assert.True(t, cw.CanListKeys()) 486 assertRightAllowedKeys(t, []wallet.KeyPair{expectedKeyPairs[0], expectedKeyPairs[1], expectedKeyPairs[2]}, cw.AllowedKeys()) 487 assert.False(t, cw.RequireInteraction()) 488 489 // setup 490 manager.walletStore.EXPECT().UnlockWallet(ctx, secondToken.Wallet.Name, secondToken.Wallet.Passphrase).Times(1).Return(nil) 491 manager.walletStore.EXPECT().GetWallet(ctx, secondToken.Wallet.Name).Times(1).Return(expectedWallet, nil) 492 493 // when simulating the deletion of the first token 494 onTokenUpdateCb(ctx, secondToken) 495 496 // when 497 cw, err = manager.ConnectedWallet(ctx, "", firstToken.Token) 498 499 // then 500 require.NotNil(t, err) 501 assert.Equal(t, jsonrpc.NewServerError(api.ErrorCodeAuthenticationFailure, connections.ErrNoConnectionAssociatedThisAuthenticationToken), err) 502 assert.Empty(t, cw) 503 } 504 505 func testReloadingPreviousSessionsSucceeds(t *testing.T) { 506 ctx, traceID := randomTraceID(t) 507 508 // given 509 hostnameA := "a" + vgrand.RandomStr(5) 510 hostnameB := "b" + vgrand.RandomStr(5) 511 walletA, _ := randomWalletWithName(t, "a"+vgrand.RandomStr(5)) 512 walletB, _ := randomWalletWithName(t, "b"+vgrand.RandomStr(5)) 513 walletAPassphrase := vgrand.RandomStr(5) 514 nonExistingWallet := vgrand.RandomStr(5) 515 tokenOnNonExistingWallet := randomToken(t) 516 token1 := randomToken(t) 517 token2 := randomToken(t) 518 token3 := randomToken(t) 519 previousSessions := []connections.Session{ 520 { 521 Token: token1, 522 Hostname: hostnameA, 523 Wallet: walletA.Name(), 524 }, { 525 Token: token2, 526 Hostname: hostnameB, 527 Wallet: walletA.Name(), 528 }, { 529 Token: token3, 530 Hostname: hostnameB, 531 Wallet: walletB.Name(), 532 }, { 533 Token: tokenOnNonExistingWallet, 534 Hostname: hostnameB, 535 Wallet: nonExistingWallet, // Emulate a non-existing wallet. 536 }, 537 } 538 539 // setup 540 manager := newTestManagerBuilder(t) 541 manager.timeService.EXPECT().Now().AnyTimes().Return(time.Now()) 542 manager.walletStore.EXPECT().OnUpdate(gomock.Any()).Times(1) 543 manager.tokenStore.EXPECT().OnUpdate(gomock.Any()).Times(1) 544 manager.tokenStore.EXPECT().ListTokens().Times(1).Return(nil, nil) 545 manager.sessionStore.EXPECT().ListSessions(gomock.Any()).Times(1).Return(previousSessions, nil) 546 gomock.InOrder( 547 manager.walletStore.EXPECT().WalletExists(gomock.Any(), walletA.Name()).Times(1).Return(true, nil), 548 manager.walletStore.EXPECT().WalletExists(gomock.Any(), walletA.Name()).Times(1).Return(true, nil), 549 manager.walletStore.EXPECT().WalletExists(gomock.Any(), walletB.Name()).Times(1).Return(true, nil), 550 manager.walletStore.EXPECT().WalletExists(gomock.Any(), nonExistingWallet).Times(1).Return(false, nil), 551 ) 552 manager.sessionStore.EXPECT().DeleteSession(gomock.Any(), tokenOnNonExistingWallet).Times(1).Return(nil) 553 gomock.InOrder( 554 manager.walletStore.EXPECT().IsWalletAlreadyUnlocked(gomock.Any(), walletA.Name()).Times(1).Return(false, nil), 555 manager.walletStore.EXPECT().IsWalletAlreadyUnlocked(gomock.Any(), walletA.Name()).Times(1).Return(false, nil), 556 manager.walletStore.EXPECT().IsWalletAlreadyUnlocked(gomock.Any(), walletB.Name()).Times(1).Return(true, nil), 557 ) 558 manager.walletStore.EXPECT().GetWallet(gomock.Any(), walletB.Name()).Times(1).Return(walletB, nil) 559 manager.Build() 560 561 // when listing the session connections, all but the one with a non-existing 562 // wallet should be returned. 563 connectionList := manager.ListSessionConnections() 564 565 // then 566 assert.Equal(t, []api.Connection{ 567 { 568 Hostname: hostnameA, 569 Wallet: walletA.Name(), 570 }, { 571 Hostname: hostnameB, 572 Wallet: walletA.Name(), 573 }, { 574 Hostname: hostnameB, 575 Wallet: walletB.Name(), 576 }, 577 }, connectionList) 578 579 // when verifying connections to walletB are full restored 580 cw1, err := manager.ConnectedWallet(ctx, hostnameB, token3) 581 582 // then 583 require.Nil(t, err) 584 assert.Equal(t, walletB.Name(), cw1.Name()) 585 assert.Equal(t, hostnameB, cw1.Hostname()) 586 587 // setup verifying connecting a closed connection trigger the passphrase 588 // pipeline only once, and restore all connections associated to that wallet. 589 manager.interactor.EXPECT().NotifyInteractionSessionBegan(ctx, traceID, api.WalletUnlockingWorkflow, uint8(2)).Times(1).Return(nil) 590 manager.interactor.EXPECT().NotifyInteractionSessionEnded(ctx, traceID).Times(1) 591 manager.interactor.EXPECT().RequestPassphrase(ctx, traceID, uint8(1), walletA.Name(), gomock.Any()).Times(1).Return(walletAPassphrase, nil) 592 manager.walletStore.EXPECT().UnlockWallet(ctx, walletA.Name(), walletAPassphrase).Times(1).Return(nil) 593 manager.walletStore.EXPECT().GetWallet(ctx, walletA.Name()).Times(1).Return(walletA, nil) 594 manager.interactor.EXPECT().NotifySuccessfulRequest(ctx, traceID, uint8(2), gomock.Any()).Times(1) 595 596 // when 597 cw2, err := manager.ConnectedWallet(ctx, hostnameA, token1) 598 599 // then 600 require.Nil(t, err) 601 assert.Equal(t, walletA.Name(), cw2.Name()) 602 assert.Equal(t, hostnameA, cw2.Hostname()) 603 604 // when 605 cw3, err := manager.ConnectedWallet(ctx, hostnameB, token2) 606 607 // then 608 require.Nil(t, err) 609 assert.Equal(t, walletA.Name(), cw3.Name()) 610 assert.Equal(t, hostnameB, cw3.Hostname()) 611 } 612 613 type testManager struct { 614 *connections.Manager 615 timeService *mocks.MockTimeService 616 walletStore *mocks.MockWalletStore 617 tokenStore *mocks.MockTokenStore 618 sessionStore *mocks.MockSessionStore 619 interactor *apimocks.MockInteractor 620 } 621 622 func (tm *testManager) Build() { 623 manager, err := connections.NewManager( 624 tm.timeService, 625 tm.walletStore, 626 tm.tokenStore, 627 tm.sessionStore, 628 tm.interactor, 629 ) 630 if err != nil { 631 panic(fmt.Errorf("could not initialise the manager: %w", err)) 632 } 633 634 tm.Manager = manager 635 } 636 637 func newTestManagerBuilder(t *testing.T) *testManager { 638 t.Helper() 639 640 ctrl := gomock.NewController(t) 641 timeService := mocks.NewMockTimeService(ctrl) 642 walletStore := mocks.NewMockWalletStore(ctrl) 643 tokenStore := mocks.NewMockTokenStore(ctrl) 644 sessionStore := mocks.NewMockSessionStore(ctrl) 645 interactor := apimocks.NewMockInteractor(ctrl) 646 647 return &testManager{ 648 timeService: timeService, 649 walletStore: walletStore, 650 tokenStore: tokenStore, 651 sessionStore: sessionStore, 652 interactor: interactor, 653 } 654 }