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  }