github.com/mysteriumnetwork/node@v0.0.0-20240516044423-365054f76801/tequilapi/endpoints/service_test.go (about) 1 /* 2 * Copyright (C) 2019 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 "encoding/json" 22 "errors" 23 "fmt" 24 "math/big" 25 "net/http" 26 "net/http/httptest" 27 "strings" 28 "testing" 29 30 "github.com/mysteriumnetwork/go-rest/apierror" 31 "github.com/mysteriumnetwork/node/core/service" 32 "github.com/mysteriumnetwork/node/core/service/servicestate" 33 "github.com/mysteriumnetwork/node/identity" 34 "github.com/mysteriumnetwork/node/market" 35 "github.com/mysteriumnetwork/node/services" 36 "github.com/stretchr/testify/assert" 37 ) 38 39 var ( 40 mockServiceID = service.ID("6ba7b810-9dad-11d1-80b4-00c04fd430c8") 41 mockAccessPolicyServiceID = service.ID("6ba7b810-9dad-11d1-80b4-00c04fd430c9") 42 mockProviderID = identity.FromAddress("0xproviderid") 43 mockServiceType = "testprotocol" 44 mockServiceOptions = fancyServiceOptions{ 45 Foo: "bar", 46 } 47 mockAccessPolicyEndpoint = "https://some.domain/api/v1/lists/" 48 mockProposal = market.NewProposal(mockProviderID.Address, mockServiceType, market.NewProposalOpts{ 49 Location: &TestLocation, 50 Quality: &mockQuality, 51 }) 52 ap = []market.AccessPolicy{ 53 { 54 ID: "verified-traffic", 55 Source: fmt.Sprintf("%v%v", mockAccessPolicyEndpoint, "verified-traffic"), 56 }, 57 { 58 ID: "0x0000000000000001", 59 Source: fmt.Sprintf("%v%v", mockAccessPolicyEndpoint, "0x0000000000000001"), 60 }, 61 { 62 ID: "dvpn-traffic", 63 Source: fmt.Sprintf("%v%v", mockAccessPolicyEndpoint, "dvpn-traffic"), 64 }, 65 { 66 ID: "12312312332132", 67 Source: fmt.Sprintf("%v%v", mockAccessPolicyEndpoint, "12312312332132"), 68 }, 69 } 70 serviceTypeWithAccessPolicy = "mockAccessPolicyService" 71 mockProposalWithAccessPolicy = market.NewProposal(mockProviderID.Address, serviceTypeWithAccessPolicy, market.NewProposalOpts{ 72 Location: &TestLocation, 73 Quality: &mockQuality, 74 AccessPolicies: ap, 75 }) 76 mockServiceRunning = service.NewInstance(mockProviderID, mockServiceType, mockServiceOptions, mockProposal, servicestate.Running, nil, nil, nil) 77 mockServiceStopped = service.NewInstance(mockProviderID, mockServiceType, mockServiceOptions, mockProposal, servicestate.NotRunning, nil, nil, nil) 78 mockServiceRunningWithAccessPolicy = service.NewInstance(mockProviderID, serviceTypeWithAccessPolicy, mockServiceOptions, mockProposalWithAccessPolicy, servicestate.Running, nil, nil, nil) 79 ) 80 81 type fancyServiceOptions struct { 82 Foo string `json:"foo"` 83 } 84 85 type mockServiceManager struct{} 86 87 func (sm *mockServiceManager) Start(_ identity.Identity, serviceType string, _ []string, _ service.Options) (service.ID, error) { 88 if serviceType == serviceTypeWithAccessPolicy { 89 return mockAccessPolicyServiceID, nil 90 } 91 return mockServiceID, nil 92 } 93 func (sm *mockServiceManager) Stop(id service.ID) error { return nil } 94 func (sm *mockServiceManager) Service(id service.ID) *service.Instance { 95 if id == "6ba7b810-9dad-11d1-80b4-00c04fd430c8" { 96 return mockServiceRunning 97 } 98 if id == mockAccessPolicyServiceID { 99 return mockServiceRunningWithAccessPolicy 100 } 101 return nil 102 } 103 func (sm *mockServiceManager) List(includeAll bool) []*service.Instance { 104 return []*service.Instance{ 105 mockServiceStopped, 106 } 107 } 108 func (sm *mockServiceManager) ListAll() []*service.Instance { 109 return []*service.Instance{mockServiceStopped} 110 } 111 func (sm *mockServiceManager) Kill() error { return nil } 112 113 var fakeOptionsParser = map[string]services.ServiceOptionsParser{ 114 "testprotocol": func(opts *json.RawMessage) (service.Options, error) { 115 return nil, nil 116 }, 117 serviceTypeWithAccessPolicy: func(opts *json.RawMessage) (service.Options, error) { 118 return nil, nil 119 }, 120 "errorprotocol": func(opts *json.RawMessage) (service.Options, error) { 121 return nil, errors.New("error") 122 }, 123 } 124 125 type mockTequilaApiClient struct{} 126 127 func (c *mockTequilaApiClient) Post(path string, payload interface{}) (*http.Response, error) { 128 return nil, nil 129 } 130 131 func Test_AddRoutesForServiceAddsRoutes(t *testing.T) { 132 router := summonTestGin() 133 err := AddRoutesForService(&mockServiceManager{}, fakeOptionsParser, &mockProposalRepository{ 134 priceToAdd: market.Price{ 135 PricePerHour: big.NewInt(500_000_000_000_000_000), 136 PricePerGiB: big.NewInt(1_000_000_000_000_000_000), 137 }, 138 }, nil)(router) 139 assert.NoError(t, err) 140 tests := []struct { 141 method string 142 path string 143 body string 144 expectedStatus int 145 expectedJSON string 146 }{ 147 { 148 http.MethodGet, 149 "/services", 150 "", 151 http.StatusOK, 152 `[{ 153 "options": {"foo": "bar"}, 154 "provider_id": "0xproviderid", 155 "type": "testprotocol", 156 "status": "NotRunning" 157 }]`, 158 }, 159 { 160 http.MethodPost, 161 "/services?ignore_user_config=true", 162 `{"provider_id": "node1", "type": "testprotocol"}`, 163 http.StatusCreated, 164 `{ 165 "id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", 166 "provider_id": "0xproviderid", 167 "type": "testprotocol", 168 "options": {"foo": "bar"}, 169 "status": "Running", 170 "proposal": { 171 "format": "service-proposal/v3", 172 "compatibility": 2, 173 "provider_id": "0xproviderid", 174 "service_type": "testprotocol", 175 "location": { 176 "asn": 123, 177 "country": "Lithuania", 178 "city": "Vilnius" 179 }, 180 "quality": { 181 "quality": 2.0, 182 "latency": 50, 183 "bandwidth": 10, 184 "uptime": 20 185 }, 186 "price": { 187 "currency": "MYST", 188 "per_gib": 1000000000000000000, 189 "per_gib_tokens": { 190 "ether": "1", 191 "human": "1", 192 "wei": "1000000000000000000" 193 }, 194 "per_hour": 500000000000000000, 195 "per_hour_tokens": { 196 "ether": "0.5", 197 "human": "0.5", 198 "wei": "500000000000000000" 199 } 200 } 201 } 202 }`, 203 }, 204 { 205 http.MethodGet, 206 "/services/6ba7b810-9dad-11d1-80b4-00c04fd430c8", 207 "", 208 http.StatusOK, 209 `{ 210 "id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", 211 "provider_id": "0xproviderid", 212 "type": "testprotocol", 213 "options": {"foo": "bar"}, 214 "status": "Running", 215 "proposal": { 216 "format": "service-proposal/v3", 217 "compatibility": 2, 218 "provider_id": "0xproviderid", 219 "service_type": "testprotocol", 220 "location": { 221 "asn": 123, 222 "country": "Lithuania", 223 "city": "Vilnius" 224 }, 225 "quality": { 226 "quality": 2.0, 227 "latency": 50, 228 "bandwidth": 10, 229 "uptime": 20 230 }, 231 "price": { 232 "currency": "MYST", 233 "per_gib": 1000000000000000000, 234 "per_gib_tokens": { 235 "ether": "1", 236 "human": "1", 237 "wei": "1000000000000000000" 238 }, 239 "per_hour": 500000000000000000, 240 "per_hour_tokens": { 241 "ether": "0.5", 242 "human": "0.5", 243 "wei": "500000000000000000" 244 } 245 } 246 } 247 }`, 248 }, 249 { 250 http.MethodDelete, "/services/6ba7b810-9dad-11d1-80b4-00c04fd430c8?ignore_user_config=true", "", 251 http.StatusAccepted, "", 252 }, 253 { 254 http.MethodDelete, "/services/00000000-9dad-11d1-80b4-00c04fd43000", "", 255 http.StatusNotFound, `{ "error": {"code":"not_found", "message":"Service not found"}, "path":"/services/00000000-9dad-11d1-80b4-00c04fd43000", "status":404 }`, 256 }, 257 } 258 259 for _, test := range tests { 260 req := httptest.NewRequest(test.method, test.path, strings.NewReader(test.body)) 261 resp := httptest.NewRecorder() 262 router.ServeHTTP(resp, req) 263 264 assert.Equal(t, test.expectedStatus, resp.Code) 265 if test.expectedJSON != "" { 266 assert.JSONEq(t, test.expectedJSON, resp.Body.String()) 267 } else { 268 assert.Equal(t, "", resp.Body.String()) 269 } 270 } 271 } 272 273 func Test_ServiceStartInvalidType(t *testing.T) { 274 path := "/services" 275 req := httptest.NewRequest( 276 http.MethodPost, 277 path, 278 strings.NewReader(`{ 279 "type": "openvpn", 280 "provider_id": "0x9edf75f870d87d2d1a69f0d950a99984ae955ee0", 281 "options": {} 282 }`), 283 ) 284 resp := httptest.NewRecorder() 285 286 g := summonTestGin() 287 err := AddRoutesForService(&mockServiceManager{}, fakeOptionsParser, &mockProposalRepository{}, nil)(g) 288 assert.NoError(t, err) 289 290 g.ServeHTTP(resp, req) 291 292 assert.Equal(t, http.StatusBadRequest, resp.Code) 293 apiErr := apierror.Parse(resp.Result()) 294 assert.Equal(t, "validation_failed", apiErr.Err.Code) 295 assert.Contains(t, apiErr.Err.Fields, "type") 296 assert.Equal(t, "invalid_value", apiErr.Err.Fields["type"].Code) 297 } 298 299 func Test_ServiceStart_InvalidType(t *testing.T) { 300 req := httptest.NewRequest( 301 http.MethodPost, 302 "/services", 303 strings.NewReader(`{ 304 "type": "openvpn", 305 "provider_id": "0x9edf75f870d87d2d1a69f0d950a99984ae955ee0", 306 "options": {} 307 }`), 308 ) 309 resp := httptest.NewRecorder() 310 311 g := summonTestGin() 312 err := AddRoutesForService(&mockServiceManager{}, fakeOptionsParser, &mockProposalRepository{}, nil)(g) 313 assert.NoError(t, err) 314 315 g.ServeHTTP(resp, req) 316 317 assert.Equal(t, http.StatusBadRequest, resp.Code) 318 apiErr := apierror.Parse(resp.Result()) 319 assert.Equal(t, "validation_failed", apiErr.Err.Code) 320 assert.Contains(t, apiErr.Err.Fields, "type") 321 assert.Equal(t, "invalid_value", apiErr.Err.Fields["type"].Code) 322 } 323 324 func Test_ServiceStart_InvalidOptions(t *testing.T) { 325 req := httptest.NewRequest( 326 http.MethodPost, 327 "/services", 328 strings.NewReader(`{ 329 "type": "errorprotocol", 330 "provider_id": "0x9edf75f870d87d2d1a69f0d950a99984ae955ee0", 331 "options": {} 332 }`), 333 ) 334 resp := httptest.NewRecorder() 335 336 g := summonTestGin() 337 err := AddRoutesForService(&mockServiceManager{}, fakeOptionsParser, &mockProposalRepository{}, nil)(g) 338 assert.NoError(t, err) 339 340 g.ServeHTTP(resp, req) 341 342 assert.Equal(t, http.StatusBadRequest, resp.Code) 343 apiErr := apierror.Parse(resp.Result()) 344 assert.Equal(t, "validation_failed", apiErr.Err.Code) 345 assert.Contains(t, apiErr.Err.Fields, "options") 346 assert.Equal(t, "invalid_value", apiErr.Err.Fields["options"].Code) 347 } 348 349 func Test_ServiceStartAlreadyRunning(t *testing.T) { 350 req := httptest.NewRequest( 351 http.MethodPost, 352 "/services", 353 strings.NewReader(`{ 354 "type": "testprotocol", 355 "provider_id": "0xproviderid", 356 "options": {} 357 }`), 358 ) 359 resp := httptest.NewRecorder() 360 361 g := summonTestGin() 362 err := AddRoutesForService(&mockServiceManager{}, fakeOptionsParser, &mockProposalRepository{}, nil)(g) 363 assert.NoError(t, err) 364 365 g.ServeHTTP(resp, req) 366 367 assert.Equal(t, http.StatusUnprocessableEntity, resp.Code) 368 assert.Equal(t, "err_service_running", apierror.Parse(resp.Result()).Err.Code) 369 } 370 371 func Test_ServiceStatus_NotFoundIsReturnedWhenNotStarted(t *testing.T) { 372 req := httptest.NewRequest(http.MethodGet, "/services/1", nil) 373 resp := httptest.NewRecorder() 374 375 g := summonTestGin() 376 err := AddRoutesForService(&mockServiceManager{}, fakeOptionsParser, &mockProposalRepository{}, nil)(g) 377 assert.NoError(t, err) 378 379 g.ServeHTTP(resp, req) 380 381 assert.Equal(t, http.StatusNotFound, resp.Code) 382 } 383 384 func Test_ServiceGetReturnsServiceInfo(t *testing.T) { 385 req := httptest.NewRequest(http.MethodGet, "/services/6ba7b810-9dad-11d1-80b4-00c04fd430c8", nil) 386 resp := httptest.NewRecorder() 387 388 g := summonTestGin() 389 err := AddRoutesForService( 390 &mockServiceManager{}, 391 fakeOptionsParser, 392 &mockProposalRepository{ 393 priceToAdd: market.Price{ 394 PricePerHour: big.NewInt(500_000_000_000_000_000), 395 PricePerGiB: big.NewInt(1_000_000_000_000_000_000), 396 }, 397 }, 398 nil, 399 )(g) 400 assert.NoError(t, err) 401 402 g.ServeHTTP(resp, req) 403 404 assert.Equal(t, http.StatusOK, resp.Code) 405 assert.JSONEq( 406 t, 407 `{ 408 "id":"6ba7b810-9dad-11d1-80b4-00c04fd430c8", 409 "provider_id": "0xproviderid", 410 "type": "testprotocol", 411 "options": {"foo": "bar"}, 412 "status": "Running", 413 "proposal": { 414 "format": "service-proposal/v3", 415 "compatibility": 2, 416 "provider_id": "0xproviderid", 417 "service_type": "testprotocol", 418 "location": { 419 "asn": 123, 420 "country": "Lithuania", 421 "city": "Vilnius" 422 }, 423 "quality": { 424 "quality": 2.0, 425 "latency": 50, 426 "bandwidth": 10, 427 "uptime": 20 428 }, 429 "price": { 430 "currency": "MYST", 431 "per_gib": 1000000000000000000, 432 "per_gib_tokens": { 433 "ether": "1", 434 "human": "1", 435 "wei": "1000000000000000000" 436 }, 437 "per_hour": 500000000000000000, 438 "per_hour_tokens": { 439 "ether": "0.5", 440 "human": "0.5", 441 "wei": "500000000000000000" 442 } 443 } 444 } 445 }`, 446 resp.Body.String(), 447 ) 448 } 449 func Test_ServiceCreate_Returns400ErrorIfRequestBodyIsNotJSON(t *testing.T) { 450 req := httptest.NewRequest(http.MethodPost, "/services", strings.NewReader("a")) 451 resp := httptest.NewRecorder() 452 453 g := summonTestGin() 454 err := AddRoutesForService(&mockServiceManager{}, fakeOptionsParser, &mockProposalRepository{}, nil)(g) 455 assert.NoError(t, err) 456 457 g.ServeHTTP(resp, req) 458 459 assert.Equal(t, http.StatusBadRequest, resp.Code) 460 assert.Equal(t, "parse_failed", apierror.Parse(resp.Result()).Err.Code) 461 } 462 463 func Test_ServiceCreate_Returns422ErrorIfRequestBodyIsMissingFieldValues(t *testing.T) { 464 req := httptest.NewRequest(http.MethodPost, "/services", strings.NewReader("{}")) 465 resp := httptest.NewRecorder() 466 467 g := summonTestGin() 468 err := AddRoutesForService(&mockServiceManager{}, fakeOptionsParser, &mockProposalRepository{}, nil)(g) 469 assert.NoError(t, err) 470 471 g.ServeHTTP(resp, req) 472 473 assert.Equal(t, http.StatusBadRequest, resp.Code) 474 apiErr := apierror.Parse(resp.Result()) 475 assert.Equal(t, "validation_failed", apiErr.Err.Code) 476 assert.Contains(t, apiErr.Err.Fields, "provider_id") 477 assert.Equal(t, "required", apiErr.Err.Fields["provider_id"].Code) 478 assert.Contains(t, apiErr.Err.Fields, "type") 479 assert.Equal(t, "required", apiErr.Err.Fields["type"].Code) 480 } 481 482 func Test_ServiceStart_WithAccessPolicy(t *testing.T) { 483 req := httptest.NewRequest( 484 http.MethodPost, 485 "/services?ignore_user_config=true", 486 strings.NewReader(`{ 487 "type": "mockAccessPolicyService", 488 "provider_id": "0x9edf75f870d87d2d1a69f0d950a99984ae955ee0", 489 "access_policies": { 490 "ids": ["verified-traffic", "dvpn-traffic", "12312312332132", "0x0000000000000001"] 491 } 492 }`), 493 ) 494 resp := httptest.NewRecorder() 495 496 g := summonTestGin() 497 err := AddRoutesForService(&mockServiceManager{}, fakeOptionsParser, &mockProposalRepository{ 498 priceToAdd: market.Price{ 499 PricePerHour: big.NewInt(500_000_000_000_000_000), 500 PricePerGiB: big.NewInt(1_000_000_000_000_000_000), 501 }, 502 }, nil)(g) 503 assert.NoError(t, err) 504 505 g.ServeHTTP(resp, req) 506 507 assert.Equal(t, http.StatusCreated, resp.Code) 508 assert.JSONEq( 509 t, 510 `{ 511 "id": "6ba7b810-9dad-11d1-80b4-00c04fd430c9", 512 "provider_id": "0xproviderid", 513 "type": "mockAccessPolicyService", 514 "options": {"foo": "bar"}, 515 "status": "Running", 516 "proposal": { 517 "format": "service-proposal/v3", 518 "compatibility": 2, 519 "provider_id": "0xproviderid", 520 "service_type": "mockAccessPolicyService", 521 "location": { 522 "asn": 123, 523 "country": "Lithuania", 524 "city": "Vilnius" 525 }, 526 "quality": { 527 "quality": 2.0, 528 "latency": 50, 529 "bandwidth": 10, 530 "uptime": 20 531 }, 532 "price": { 533 "currency": "MYST", 534 "per_gib": 1000000000000000000, 535 "per_gib_tokens": { 536 "ether": "1", 537 "human": "1", 538 "wei": "1000000000000000000" 539 }, 540 "per_hour": 500000000000000000, 541 "per_hour_tokens": { 542 "ether": "0.5", 543 "human": "0.5", 544 "wei": "500000000000000000" 545 } 546 }, 547 "access_policies": [ 548 { 549 "id":"verified-traffic", 550 "source": "https://some.domain/api/v1/lists/verified-traffic" 551 }, 552 { 553 "id":"0x0000000000000001", 554 "source": "https://some.domain/api/v1/lists/0x0000000000000001" 555 }, 556 { 557 "id":"dvpn-traffic", 558 "source": "https://some.domain/api/v1/lists/dvpn-traffic" 559 }, 560 { 561 "id":"12312312332132", 562 "source": "https://some.domain/api/v1/lists/12312312332132" 563 } 564 ] 565 } 566 }`, 567 resp.Body.String(), 568 ) 569 } 570 571 func Test_ServiceStart_ReturnsBadRequest_WithUnknownParams(t *testing.T) { 572 req := httptest.NewRequest( 573 http.MethodPost, 574 "/services", 575 strings.NewReader(`{ 576 "type": "mockAccessPolicyService", 577 "provider_id": "0x9edf75f870d87d2d1a69f0d950a99984ae955ee0", 578 "access_policy": { 579 "ids": ["verified-traffic", "dvpn-traffic", "12312312332132", "0x0000000000000001"] 580 } 581 }`), 582 ) 583 resp := httptest.NewRecorder() 584 585 g := summonTestGin() 586 err := AddRoutesForService(&mockServiceManager{}, fakeOptionsParser, &mockProposalRepository{}, nil)(g) 587 assert.NoError(t, err) 588 589 g.ServeHTTP(resp, req) 590 591 assert.Equal(t, http.StatusBadRequest, resp.Code) 592 assert.Equal(t, "parse_failed", apierror.Parse(resp.Result()).Err.Code) 593 }