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  }