github.com/mysteriumnetwork/node@v0.0.0-20240516044423-365054f76801/tequilapi/endpoints/connection_test.go (about)

     1  /*/*
     2   * Copyright (C) 2017 The "MysteriumNetwork/node" Authors.
     3   *
     4   * This program is free software: you can redistribute it and/or modify
     5   * it under the terms of the GNU General Public License as published by
     6   * the Free Software Foundation, either version 3 of the License, or
     7   * (at your option) any later version.
     8   *
     9   * This program is distributed in the hope that it will be useful,
    10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    12   * GNU General Public License for more details.
    13   *
    14   * You should have received a copy of the GNU General Public License
    15   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    16   */
    17  
    18  package endpoints
    19  
    20  import (
    21  	"context"
    22  	"math/big"
    23  	"net/http"
    24  	"net/http/httptest"
    25  	"strings"
    26  	"testing"
    27  	"time"
    28  
    29  	"github.com/ethereum/go-ethereum/common"
    30  	"github.com/pkg/errors"
    31  	"github.com/stretchr/testify/assert"
    32  
    33  	"github.com/mysteriumnetwork/go-rest/apierror"
    34  	"github.com/mysteriumnetwork/node/consumer/bandwidth"
    35  	"github.com/mysteriumnetwork/node/core/connection"
    36  	"github.com/mysteriumnetwork/node/core/connection/connectionstate"
    37  	"github.com/mysteriumnetwork/node/core/discovery/proposal"
    38  	"github.com/mysteriumnetwork/node/core/state/event"
    39  	"github.com/mysteriumnetwork/node/datasize"
    40  	"github.com/mysteriumnetwork/node/eventbus"
    41  	"github.com/mysteriumnetwork/node/identity"
    42  	"github.com/mysteriumnetwork/node/identity/registry"
    43  	"github.com/mysteriumnetwork/node/market"
    44  	"github.com/mysteriumnetwork/payments/crypto"
    45  )
    46  
    47  type mockConnectionManager struct {
    48  	onConnectReturn      error
    49  	onDisconnectReturn   error
    50  	onCheckChannelReturn error
    51  	onStatusReturn       connectionstate.Status
    52  	disconnectCount      int
    53  	requestedConsumerID  identity.Identity
    54  	requestedProvider    identity.Identity
    55  	requestedHermesID    common.Address
    56  	requestedServiceType string
    57  }
    58  
    59  func (cm *mockConnectionManager) Connect(consumerID identity.Identity, hermesID common.Address, proposalLookup connection.ProposalLookup, options connection.ConnectParams) error {
    60  	proposal, _ := proposalLookup()
    61  	if proposal == nil {
    62  		return errors.New("no proposal")
    63  	}
    64  
    65  	cm.requestedConsumerID = consumerID
    66  	cm.requestedHermesID = hermesID
    67  	cm.requestedProvider = identity.FromAddress(proposal.ProviderID)
    68  	cm.requestedServiceType = proposal.ServiceType
    69  	return cm.onConnectReturn
    70  }
    71  
    72  func (cm *mockConnectionManager) Status(int) connectionstate.Status {
    73  	return cm.onStatusReturn
    74  }
    75  
    76  func (cm *mockConnectionManager) Stats(int) connectionstate.Statistics {
    77  	return connectionstate.Statistics{}
    78  }
    79  
    80  func (cm *mockConnectionManager) Disconnect(int) error {
    81  	cm.disconnectCount++
    82  	return cm.onDisconnectReturn
    83  }
    84  
    85  func (cm *mockConnectionManager) CheckChannel(context.Context) error {
    86  	return cm.onCheckChannelReturn
    87  }
    88  
    89  func (cm *mockConnectionManager) Reconnect(int) {
    90  	return
    91  }
    92  
    93  func mockRepositoryWithProposal(providerID, serviceType string) *mockProposalRepository {
    94  	sampleProposal := proposal.PricedServiceProposal{
    95  		ServiceProposal: market.ServiceProposal{
    96  			ServiceType: serviceType,
    97  			Location:    TestLocation,
    98  			ProviderID:  providerID,
    99  		},
   100  	}
   101  
   102  	return &mockProposalRepository{
   103  		proposals: []proposal.PricedServiceProposal{sampleProposal},
   104  	}
   105  }
   106  
   107  func TestAddRoutesForConnectionAddsRoutes(t *testing.T) {
   108  	router := summonTestGin()
   109  	state := connectionstate.Status{State: connectionstate.NotConnected}
   110  	fakeManager := &mockConnectionManager{
   111  		onStatusReturn: state,
   112  	}
   113  	fakeState := &mockStateProvider{stateToReturn: event.State{Connections: make(map[string]event.Connection)}}
   114  	fakeState.stateToReturn.Connections["1"] = event.Connection{
   115  		Session:    state,
   116  		Statistics: connectionstate.Statistics{BytesSent: 1, BytesReceived: 2},
   117  	}
   118  
   119  	mockedProposalProvider := mockRepositoryWithProposal("node1", "noop")
   120  	err := AddRoutesForConnection(fakeManager, fakeState, mockedProposalProvider, mockIdentityRegistryInstance, eventbus.New(), &mockAddressProvider{})(router)
   121  	assert.NoError(t, err)
   122  
   123  	tests := []struct {
   124  		method         string
   125  		path           string
   126  		body           string
   127  		expectedStatus int
   128  		expectedJSON   string
   129  	}{
   130  		{
   131  			http.MethodGet, "/connection", "",
   132  			http.StatusOK, `{"status": "NotConnected"}`,
   133  		},
   134  		{
   135  			http.MethodPut, "/connection", `{"consumer_id": "me", "provider_id": "node1", "hermes_id":"hermes", "service_type": "noop"}`,
   136  			http.StatusCreated, `{"status": "NotConnected"}`,
   137  		},
   138  		{
   139  			http.MethodDelete, "/connection", "",
   140  			http.StatusAccepted, "",
   141  		},
   142  		{
   143  			http.MethodGet, "/connection/statistics", "",
   144  			http.StatusOK, `{
   145  				"bytes_sent": 1,
   146  				"bytes_received": 2,
   147  				"throughput_received": 0,
   148  				"throughput_sent": 0,
   149  				"duration": 0,
   150  				"tokens_spent": 0,
   151  				"spent_tokens": {
   152  					"ether": "0",
   153  					"human": "0",
   154  					"wei": "0"
   155  				}
   156  			}`,
   157  		},
   158  	}
   159  
   160  	for _, test := range tests {
   161  		resp := httptest.NewRecorder()
   162  		req := httptest.NewRequest(test.method, test.path, strings.NewReader(test.body))
   163  		router.ServeHTTP(resp, req)
   164  		assert.Equal(t, test.expectedStatus, resp.Code)
   165  		if test.expectedJSON != "" {
   166  			assert.JSONEq(t, test.expectedJSON, resp.Body.String())
   167  		} else {
   168  			assert.Equal(t, "", resp.Body.String())
   169  		}
   170  	}
   171  }
   172  
   173  func TestStateIsReturnedFromStore(t *testing.T) {
   174  	manager := &mockConnectionManager{
   175  		onStatusReturn: connectionstate.Status{
   176  			StartedAt:  time.Time{},
   177  			ConsumerID: identity.Identity{},
   178  			HermesID:   common.Address{},
   179  			State:      connectionstate.Disconnecting,
   180  			SessionID:  "1",
   181  			Proposal:   proposal.PricedServiceProposal{},
   182  		},
   183  	}
   184  
   185  	router := summonTestGin()
   186  	err := AddRoutesForConnection(manager, nil, &mockProposalRepository{}, mockIdentityRegistryInstance, eventbus.New(), &mockAddressProvider{})(router)
   187  	assert.NoError(t, err)
   188  
   189  	req := httptest.NewRequest(http.MethodGet, "/connection", nil)
   190  	resp := httptest.NewRecorder()
   191  
   192  	router.ServeHTTP(resp, req)
   193  
   194  	assert.Equal(t, http.StatusOK, resp.Code)
   195  	assert.JSONEq(
   196  		t,
   197  		`{
   198  			"status" : "Disconnecting",
   199  			"session_id" : "1"
   200  		}`,
   201  		resp.Body.String(),
   202  	)
   203  }
   204  
   205  func TestPutReturns400ErrorIfRequestBodyIsNotJSON(t *testing.T) {
   206  	fakeManager := mockConnectionManager{}
   207  
   208  	router := summonTestGin()
   209  	err := AddRoutesForConnection(&fakeManager, nil, &mockProposalRepository{}, mockIdentityRegistryInstance, eventbus.New(), &mockAddressProvider{})(router)
   210  	assert.NoError(t, err)
   211  
   212  	req := httptest.NewRequest(http.MethodPut, "/connection", strings.NewReader("a"))
   213  	resp := httptest.NewRecorder()
   214  
   215  	router.ServeHTTP(resp, req)
   216  	assert.Equal(t, http.StatusBadRequest, resp.Code)
   217  	assert.Equal(t, "parse_failed", apierror.Parse(resp.Result()).Err.Code)
   218  }
   219  
   220  func TestPutReturns422ErrorIfRequestBodyIsMissingFieldValues(t *testing.T) {
   221  	fakeManager := mockConnectionManager{}
   222  
   223  	router := summonTestGin()
   224  	err := AddRoutesForConnection(&fakeManager, nil, &mockProposalRepository{}, mockIdentityRegistryInstance, eventbus.New(), &mockAddressProvider{})(router)
   225  	assert.NoError(t, err)
   226  
   227  	req := httptest.NewRequest(http.MethodPut, "/connection", strings.NewReader("{}"))
   228  	resp := httptest.NewRecorder()
   229  
   230  	router.ServeHTTP(resp, req)
   231  
   232  	assert.Equal(t, http.StatusBadRequest, resp.Code)
   233  	apiErr := apierror.Parse(resp.Result())
   234  	assert.Equal(t, "validation_failed", apiErr.Err.Code)
   235  	assert.Contains(t, apiErr.Err.Fields, "consumer_id")
   236  	assert.Equal(t, "required", apiErr.Err.Fields["consumer_id"].Code)
   237  }
   238  
   239  func TestPutWithValidBodyCreatesConnection(t *testing.T) {
   240  	state := connectionstate.Status{
   241  		State:     connectionstate.Connected,
   242  		SessionID: "1",
   243  	}
   244  	fakeManager := mockConnectionManager{onStatusReturn: state}
   245  	fakeState := &mockStateProvider{stateToReturn: event.State{Connections: make(map[string]event.Connection)}}
   246  	fakeState.stateToReturn.Connections["1"] = event.Connection{
   247  		Session: state,
   248  	}
   249  
   250  	proposalProvider := mockRepositoryWithProposal("required-node", "openvpn")
   251  	req := httptest.NewRequest(
   252  		http.MethodPut,
   253  		"/connection",
   254  		strings.NewReader(
   255  			`{
   256  				"consumer_id" : "my-identity",
   257  				"provider_id" : "required-node",
   258  				"hermes_id" : "hermes"
   259  			}`))
   260  	resp := httptest.NewRecorder()
   261  
   262  	g := summonTestGin()
   263  	err := AddRoutesForConnection(&fakeManager, fakeState, proposalProvider, mockIdentityRegistryInstance, eventbus.New(), &mockAddressProvider{})(g)
   264  	assert.NoError(t, err)
   265  
   266  	g.ServeHTTP(resp, req)
   267  
   268  	assert.Equal(t, identity.FromAddress("my-identity"), fakeManager.requestedConsumerID)
   269  	assert.Equal(t, common.HexToAddress("hermes"), fakeManager.requestedHermesID)
   270  	assert.Equal(t, identity.FromAddress("required-node"), fakeManager.requestedProvider)
   271  	assert.Equal(t, "openvpn", fakeManager.requestedServiceType)
   272  
   273  	assert.Equal(t, http.StatusCreated, resp.Code)
   274  	assert.JSONEq(
   275  		t,
   276  		`{
   277  			"status" : "Connected",
   278  			"session_id" : "1"
   279  		}`,
   280  		resp.Body.String(),
   281  	)
   282  }
   283  
   284  func TestPutUnregisteredIdentityReturnsError(t *testing.T) {
   285  	fakeManager := mockConnectionManager{}
   286  
   287  	proposalProvider := mockRepositoryWithProposal("required-node", "openvpn")
   288  	mir := *mockIdentityRegistryInstance
   289  	mir.RegistrationStatus = registry.Unregistered
   290  
   291  	req := httptest.NewRequest(
   292  		http.MethodPut,
   293  		"/connection",
   294  		strings.NewReader(
   295  			`{
   296  				"consumer_id" : "my-identity",
   297  				"provider_id" : "required-node",
   298  				"hermes_id" : "hermes"
   299  			}`))
   300  	resp := httptest.NewRecorder()
   301  
   302  	g := summonTestGin()
   303  	err := AddRoutesForConnection(&fakeManager, &mockStateProvider{}, proposalProvider, &mir, eventbus.New(), &mockAddressProvider{})(g)
   304  	assert.NoError(t, err)
   305  
   306  	g.ServeHTTP(resp, req)
   307  
   308  	assert.Equal(t, http.StatusUnprocessableEntity, resp.Code)
   309  	assert.Equal(t, "err_id_not_registered", apierror.Parse(resp.Result()).Err.Code)
   310  }
   311  
   312  func TestPutFailedRegistrationCheckReturnsError(t *testing.T) {
   313  	fakeManager := mockConnectionManager{}
   314  
   315  	proposalProvider := mockRepositoryWithProposal("required-node", "openvpn")
   316  	mir := *mockIdentityRegistryInstance
   317  	mir.RegistrationCheckError = errors.New("explosions everywhere")
   318  
   319  	req := httptest.NewRequest(
   320  		http.MethodPut,
   321  		"/connection",
   322  		strings.NewReader(
   323  			`{
   324  				"consumer_id" : "my-identity",
   325  				"provider_id" : "required-node",
   326  				"hermes_id" : "hermes"
   327  			}`))
   328  	resp := httptest.NewRecorder()
   329  
   330  	g := summonTestGin()
   331  	err := AddRoutesForConnection(&fakeManager, &mockStateProvider{}, proposalProvider, &mir, eventbus.New(), &mockAddressProvider{})(g)
   332  	assert.NoError(t, err)
   333  
   334  	g.ServeHTTP(resp, req)
   335  
   336  	assert.Equal(t, http.StatusInternalServerError, resp.Code)
   337  	apiErr := apierror.Parse(resp.Result())
   338  	assert.Equal(t, "err_id_registration_status_check", apiErr.Err.Code)
   339  	assert.Equal(t, "Failed to check ID registration status: explosions everywhere", apiErr.Message())
   340  }
   341  
   342  func TestPutWithServiceTypeOverridesDefault(t *testing.T) {
   343  	fakeManager := mockConnectionManager{}
   344  
   345  	mystAPI := mockRepositoryWithProposal("required-node", "noop")
   346  	req := httptest.NewRequest(
   347  		http.MethodPut,
   348  		"/connection",
   349  		strings.NewReader(
   350  			`{
   351  				"consumer_id" : "my-identity",
   352  				"provider_id" : "required-node",
   353  				"hermes_id": "hermes",
   354  				"service_type": "noop"
   355  			}`))
   356  	resp := httptest.NewRecorder()
   357  
   358  	g := summonTestGin()
   359  	err := AddRoutesForConnection(&fakeManager, &mockStateProvider{}, mystAPI, mockIdentityRegistryInstance, eventbus.New(), &mockAddressProvider{})(g)
   360  	assert.NoError(t, err)
   361  
   362  	g.ServeHTTP(resp, req)
   363  
   364  	assert.Equal(t, http.StatusCreated, resp.Code)
   365  
   366  	assert.Equal(t, identity.FromAddress("required-node"), fakeManager.requestedProvider)
   367  	assert.Equal(t, common.HexToAddress("hermes"), fakeManager.requestedHermesID)
   368  	assert.Equal(t, identity.FromAddress("required-node"), fakeManager.requestedProvider)
   369  	assert.Equal(t, "noop", fakeManager.requestedServiceType)
   370  }
   371  
   372  func TestDeleteCallsDisconnect(t *testing.T) {
   373  	fakeManager := mockConnectionManager{}
   374  
   375  	req := httptest.NewRequest(http.MethodDelete, "/connection", nil)
   376  	resp := httptest.NewRecorder()
   377  
   378  	g := summonTestGin()
   379  	err := AddRoutesForConnection(&fakeManager, nil, &mockProposalRepository{}, mockIdentityRegistryInstance, eventbus.New(), &mockAddressProvider{})(g)
   380  	assert.NoError(t, err)
   381  
   382  	g.ServeHTTP(resp, req)
   383  
   384  	assert.Equal(t, http.StatusAccepted, resp.Code)
   385  
   386  	assert.Equal(t, fakeManager.disconnectCount, 1)
   387  }
   388  
   389  func TestGetStatisticsEndpointReturnsStatistics(t *testing.T) {
   390  	fakeState := &mockStateProvider{stateToReturn: event.State{Connections: make(map[string]event.Connection)}}
   391  	fakeState.stateToReturn.Connections["1"] = event.Connection{
   392  		Statistics: connectionstate.Statistics{BytesSent: 1, BytesReceived: 2},
   393  		Throughput: bandwidth.Throughput{Up: datasize.BitSpeed(1000), Down: datasize.BitSpeed(2000)},
   394  		Invoice:    crypto.Invoice{AgreementTotal: big.NewInt(10001)},
   395  	}
   396  
   397  	manager := mockConnectionManager{}
   398  
   399  	resp := httptest.NewRecorder()
   400  
   401  	req := httptest.NewRequest(
   402  		http.MethodGet,
   403  		"/connection/statistics",
   404  		strings.NewReader(
   405  			`{
   406  				"consumer_id" : "my-identity",
   407  				"provider_id" : "required-node",
   408  				"hermes_id": "hermes",
   409  				"service_type": "noop"
   410  			}`))
   411  
   412  	g := summonTestGin()
   413  	err := AddRoutesForConnection(&manager, fakeState, &mockProposalRepository{}, mockIdentityRegistryInstance, eventbus.New(), &mockAddressProvider{})(g)
   414  	assert.NoError(t, err)
   415  
   416  	g.ServeHTTP(resp, req)
   417  
   418  	assert.JSONEq(
   419  		t,
   420  		`{
   421  			"bytes_sent": 1,
   422  			"bytes_received": 2,
   423  			"throughput_sent": 1000,
   424  			"throughput_received": 2000,
   425  			"duration": 0,
   426  			"tokens_spent": 10001,
   427  			"spent_tokens": {
   428  				"ether": "0.000000000000010001",
   429  				"human": "0",
   430  				"wei": "10001"
   431  			}
   432  		}`,
   433  		resp.Body.String(),
   434  	)
   435  }
   436  
   437  func TestEndpointReturnsConflictStatusIfConnectionAlreadyExists(t *testing.T) {
   438  	manager := mockConnectionManager{}
   439  	manager.onConnectReturn = connection.ErrAlreadyExists
   440  
   441  	mystAPI := mockRepositoryWithProposal("required-node", "openvpn")
   442  
   443  	req := httptest.NewRequest(
   444  		http.MethodPut,
   445  		"/connection",
   446  		strings.NewReader(
   447  			`{
   448  				"consumer_id" : "my-identity",
   449  				"provider_id" : "required-node",
   450  				"hermes_id" : "hermes"
   451  			}`))
   452  	resp := httptest.NewRecorder()
   453  
   454  	g := summonTestGin()
   455  	err := AddRoutesForConnection(&manager, nil, mystAPI, mockIdentityRegistryInstance, eventbus.New(), &mockAddressProvider{})(g)
   456  	assert.NoError(t, err)
   457  
   458  	g.ServeHTTP(resp, req)
   459  
   460  	assert.Equal(t, http.StatusUnprocessableEntity, resp.Code)
   461  	assert.Equal(t, "err_connection_already_exists", apierror.Parse(resp.Result()).Err.Code)
   462  }
   463  
   464  /*func TestDisconnectReturnsConflictStatusIfConnectionDoesNotExist(t *testing.T) {
   465  	manager := mockConnectionManager{}
   466  	manager.onDisconnectReturn = connection.ErrNoConnection
   467  
   468  	connectionEndpoint := NewConnectionEndpoint(&manager, nil, &mockProposalRepository{}, mockIdentityRegistryInstance, eventbus.New(), &mockAddressProvider{})
   469  
   470  	req := httptest.NewRequest(
   471  		http.MethodDelete,
   472  		"/irrelevant",
   473  		nil,
   474  	)
   475  	resp := httptest.NewRecorder()
   476  
   477  	connectionEndpoint.Kill(&gin.Context{Request: req})
   478  
   479  	assert.Equal(t, http.StatusConflict, resp.Code)
   480  	assert.JSONEq(
   481  		t,
   482  		`{
   483  			"message" : "no connection exists"
   484  		}`,
   485  		resp.Body.String(),
   486  	)
   487  }*/
   488  
   489  func TestConnectReturnsConnectCancelledStatusWhenErrConnectionCancelledIsEncountered(t *testing.T) {
   490  	manager := mockConnectionManager{}
   491  	manager.onConnectReturn = connection.ErrConnectionCancelled
   492  
   493  	mockProposalProvider := mockRepositoryWithProposal("required-node", "openvpn")
   494  	req := httptest.NewRequest(
   495  		http.MethodPut,
   496  		"/connection",
   497  		strings.NewReader(
   498  			`{
   499  				"consumer_id" : "my-identity",
   500  				"provider_id" : "required-node",
   501  				"hermes_id" : "hermes"
   502  			}`))
   503  	resp := httptest.NewRecorder()
   504  
   505  	g := summonTestGin()
   506  	err := AddRoutesForConnection(&manager, nil, mockProposalProvider, mockIdentityRegistryInstance, eventbus.New(), &mockAddressProvider{})(g)
   507  	assert.NoError(t, err)
   508  
   509  	g.ServeHTTP(resp, req)
   510  
   511  	assert.Equal(t, http.StatusUnprocessableEntity, resp.Code)
   512  	assert.Equal(t, "err_connection_cancelled", apierror.Parse(resp.Result()).Err.Code)
   513  }
   514  
   515  func TestConnectReturnsErrorIfNoProposals(t *testing.T) {
   516  	manager := mockConnectionManager{}
   517  	manager.onConnectReturn = connection.ErrConnectionCancelled
   518  
   519  	req := httptest.NewRequest(
   520  		http.MethodPut,
   521  		"/connection",
   522  		strings.NewReader(
   523  			`{
   524  				"consumer_id" : "my-identity",
   525  				"provider_id" : "required-node",
   526  				"hermes_id" : "hermes"
   527  			}`))
   528  	resp := httptest.NewRecorder()
   529  
   530  	g := summonTestGin()
   531  	err := AddRoutesForConnection(&manager, nil, &mockProposalRepository{}, mockIdentityRegistryInstance, eventbus.New(), &mockAddressProvider{})(g)
   532  	assert.NoError(t, err)
   533  
   534  	g.ServeHTTP(resp, req)
   535  
   536  	assert.Equal(t, http.StatusInternalServerError, resp.Code)
   537  }
   538  
   539  var mockIdentityRegistryInstance = &registry.FakeRegistry{RegistrationStatus: registry.Registered}