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 = ®istry.FakeRegistry{RegistrationStatus: registry.Registered}