github.com/mysteriumnetwork/node@v0.0.0-20240516044423-365054f76801/tequilapi/endpoints/proposals_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  	"errors"
    22  	"fmt"
    23  	"math/big"
    24  	"net/http"
    25  	"net/http/httptest"
    26  	"net/url"
    27  	"testing"
    28  
    29  	"github.com/gin-gonic/gin"
    30  	"github.com/stretchr/testify/assert"
    31  	"golang.org/x/net/context"
    32  
    33  	"github.com/mysteriumnetwork/node/core/discovery/proposal"
    34  	"github.com/mysteriumnetwork/node/core/location/locationstate"
    35  	"github.com/mysteriumnetwork/node/market"
    36  	"github.com/mysteriumnetwork/node/mocks"
    37  	"github.com/mysteriumnetwork/node/nat"
    38  )
    39  
    40  var TestLocation = market.Location{ASN: 123, Country: "Lithuania", City: "Vilnius"}
    41  
    42  var (
    43  	priceHourMax = big.NewInt(50000)
    44  	priceGiBMax  = big.NewInt(7000000)
    45  	mockQuality  = mocks.Quality()
    46  )
    47  
    48  var serviceProposals = []proposal.PricedServiceProposal{
    49  	{
    50  		ServiceProposal: market.NewProposal("0xProviderId", "testprotocol", market.NewProposalOpts{
    51  			Location: &TestLocation,
    52  			Quality:  &mockQuality,
    53  		}),
    54  		Price: market.Price{
    55  			PricePerHour: big.NewInt(500_000_000_000_000_000),
    56  			PricePerGiB:  big.NewInt(1_000_000_000_000_000_000),
    57  		},
    58  	},
    59  	{
    60  		ServiceProposal: market.NewProposal("other_provider", "testprotocol", market.NewProposalOpts{
    61  			Location: &TestLocation,
    62  			Quality:  &mockQuality,
    63  		}),
    64  		Price: market.Price{
    65  			PricePerHour: big.NewInt(500_000_000_000_000_000),
    66  			PricePerGiB:  big.NewInt(1_000_000_000_000_000_000),
    67  		},
    68  	},
    69  }
    70  
    71  type mockNATProber struct {
    72  	returnRes nat.NATType
    73  	returnErr error
    74  }
    75  
    76  func (m *mockNATProber) Probe(_ context.Context) (nat.NATType, error) {
    77  	return m.returnRes, m.returnErr
    78  }
    79  
    80  var mockedNATProber = &mockNATProber{"none", nil}
    81  
    82  type mockResolver struct{}
    83  
    84  func (r *mockResolver) DetectLocation() (locationstate.Location, error) {
    85  	return locationstate.Location{}, nil
    86  }
    87  
    88  func (r *mockResolver) DetectProxyLocation(_ int) (locationstate.Location, error) {
    89  	return r.DetectLocation()
    90  }
    91  
    92  type mockPricer struct {
    93  	priceToReturn market.Price
    94  }
    95  
    96  func (mpip *mockPricer) GetCurrentPrice(nodeType string, country string, serviceType string) (market.Price, error) {
    97  	return mpip.priceToReturn, nil
    98  }
    99  
   100  func TestProposalsEndpointListByNodeId(t *testing.T) {
   101  	repository := &mockProposalRepository{
   102  		// we assume that underling component does correct filtering
   103  		proposals: []proposal.PricedServiceProposal{serviceProposals[0]},
   104  	}
   105  
   106  	path := "/proposals"
   107  	req, err := http.NewRequest(
   108  		http.MethodGet,
   109  		path,
   110  		nil,
   111  	)
   112  	assert.Nil(t, err)
   113  
   114  	query := req.URL.Query()
   115  	query.Set("provider_id", "0xProviderId")
   116  	req.URL.RawQuery = query.Encode()
   117  
   118  	resp := httptest.NewRecorder()
   119  	endpoint := NewProposalsEndpoint(repository, nil, nil, &mockFilterPresetRepository{}, mockedNATProber)
   120  	g := gin.Default()
   121  	g.GET(path, endpoint.List)
   122  	g.ServeHTTP(resp, req)
   123  
   124  	assert.JSONEq(
   125  		t,
   126  		`{
   127              "proposals": [
   128                  {
   129                      "format": "service-proposal/v3",
   130                      "compatibility": 2,
   131                      "provider_id": "0xProviderId",
   132                      "service_type": "testprotocol",
   133                      "location": {
   134  						"asn": 123,
   135  						"country": "Lithuania",
   136  						"city": "Vilnius"
   137  					},
   138                      "quality": {
   139                        "quality": 2.0,
   140                        "latency": 50,
   141                        "bandwidth": 10,
   142                        "uptime": 20
   143                      },
   144  					"price": {
   145  					  "currency": "MYST",
   146  					  "per_gib": 1000000000000000000,
   147  					  "per_gib_tokens": {
   148  						"ether": "1",
   149  						"human": "1",
   150  						"wei": "1000000000000000000"
   151  					  },
   152  					  "per_hour": 500000000000000000,
   153  					  "per_hour_tokens": {
   154  						"ether": "0.5",
   155  						"human": "0.5",
   156  						"wei": "500000000000000000"
   157  					  }
   158  					}
   159                  }
   160              ]
   161          }`,
   162  		resp.Body.String(),
   163  	)
   164  
   165  	assert.EqualValues(t, &proposal.Filter{
   166  		ProviderID:         "0xProviderId",
   167  		ExcludeUnsupported: true,
   168  		CompatibilityMin:   2,
   169  	}, repository.recordedFilter)
   170  }
   171  
   172  func TestProposalsEndpointAcceptsAccessPolicyParams(t *testing.T) {
   173  	repository := &mockProposalRepository{
   174  		proposals: []proposal.PricedServiceProposal{serviceProposals[0]},
   175  	}
   176  	path := "/proposals"
   177  	req, err := http.NewRequest(
   178  		http.MethodGet,
   179  		path,
   180  		nil,
   181  	)
   182  	assert.Nil(t, err)
   183  
   184  	query := req.URL.Query()
   185  	query.Set("access_policy", "accessPolicy")
   186  	query.Set("access_policy_source", "accessPolicySource")
   187  	req.URL.RawQuery = query.Encode()
   188  
   189  	resp := httptest.NewRecorder()
   190  	endpoint := NewProposalsEndpoint(repository, nil, nil, &mockFilterPresetRepository{}, mockedNATProber)
   191  
   192  	g := gin.Default()
   193  	g.GET(path, endpoint.List)
   194  	g.ServeHTTP(resp, req)
   195  
   196  	assert.JSONEq(
   197  		t,
   198  		`{
   199              "proposals": [
   200                  {
   201                      "format": "service-proposal/v3",
   202                      "compatibility": 2,
   203                      "provider_id": "0xProviderId",
   204                      "service_type": "testprotocol",
   205                      "location": {
   206  						"asn": 123,
   207  						"country": "Lithuania",
   208  						"city": "Vilnius"
   209  					},
   210                      "quality": {
   211                        "quality": 2.0,
   212                        "latency": 50,
   213                        "bandwidth": 10,
   214                        "uptime": 20
   215                      },
   216  					"price": {
   217  					  "currency": "MYST",
   218  					  "per_gib": 1000000000000000000,
   219  					  "per_gib_tokens": {
   220  						"ether": "1",
   221  						"human": "1",
   222  						"wei": "1000000000000000000"
   223  					  },
   224  					  "per_hour": 500000000000000000,
   225  					  "per_hour_tokens": {
   226  						"ether": "0.5",
   227  						"human": "0.5",
   228  						"wei": "500000000000000000"
   229  					  }
   230  					}
   231                  }
   232              ]
   233          }`,
   234  		resp.Body.String(),
   235  	)
   236  	assert.Equal(t,
   237  		&proposal.Filter{
   238  			AccessPolicy:       "accessPolicy",
   239  			AccessPolicySource: "accessPolicySource",
   240  			ExcludeUnsupported: true,
   241  			CompatibilityMin:   2,
   242  		},
   243  		repository.recordedFilter,
   244  	)
   245  }
   246  
   247  func TestCurrentPrices(t *testing.T) {
   248  	// given
   249  	repository := &mockProposalRepository{
   250  		proposals: serviceProposals,
   251  	}
   252  	presetRepository := &mockFilterPresetRepository{
   253  		presets: proposal.FilterPresets{Entries: []proposal.FilterPreset{
   254  			{
   255  				ID:     0,
   256  				Name:   "",
   257  				IPType: "",
   258  			},
   259  		}},
   260  	}
   261  	endpoint := NewProposalsEndpoint(repository, &mockPricer{
   262  		priceToReturn: market.Price{
   263  			PricePerHour: big.NewInt(123_000_000_000_000_000),
   264  			PricePerGiB:  big.NewInt(456_000_000_000_000_000),
   265  		},
   266  	}, &mockResolver{}, presetRepository, mockedNATProber)
   267  
   268  	path := "/prices/current"
   269  	req, err := http.NewRequest(
   270  		http.MethodGet,
   271  		path,
   272  		nil,
   273  	)
   274  	assert.Nil(t, err)
   275  
   276  	g := gin.Default()
   277  	g.GET(path, endpoint.CurrentPrice)
   278  	resp := httptest.NewRecorder()
   279  	g.ServeHTTP(resp, req)
   280  
   281  	assert.JSONEq(
   282  		t,
   283  		`
   284  				{
   285  				  "service_type": "wireguard",
   286  				  "price_per_hour": 123000000000000000,
   287  				  "price_per_hour_tokens": {
   288  					"wei": "123000000000000000",
   289  					"ether": "0.123",
   290  					"human": "0.123"
   291  				  },
   292  				  "price_per_gib": 456000000000000000,
   293  				  "price_per_gib_tokens": {
   294  					"wei": "456000000000000000",
   295  					"ether": "0.456",
   296  					"human": "0.456"
   297  				  }
   298  				}
   299  				`,
   300  		resp.Body.String(),
   301  	)
   302  }
   303  
   304  func TestProposalsEndpointFilterByPresetID(t *testing.T) {
   305  	repository := &mockProposalRepository{
   306  		proposals: serviceProposals,
   307  	}
   308  
   309  	path := "/proposals"
   310  	req, err := http.NewRequest(
   311  		http.MethodGet,
   312  		path,
   313  		nil,
   314  	)
   315  	assert.Nil(t, err)
   316  
   317  	resp := httptest.NewRecorder()
   318  	presetRepository := &mockFilterPresetRepository{
   319  		presets: proposal.FilterPresets{Entries: []proposal.FilterPreset{
   320  			{
   321  				ID:     0,
   322  				Name:   "",
   323  				IPType: "",
   324  			},
   325  		}},
   326  	}
   327  	endpoint := NewProposalsEndpoint(repository, nil, nil, presetRepository, mockedNATProber)
   328  	g := gin.Default()
   329  	g.GET(path, endpoint.List)
   330  	g.ServeHTTP(resp, req)
   331  
   332  	assert.JSONEq(
   333  		t,
   334  		`{
   335              "proposals": [
   336                  {
   337                      "format": "service-proposal/v3",
   338                      "compatibility": 2,
   339                      "provider_id": "0xProviderId",
   340                      "service_type": "testprotocol",
   341                      "location": {
   342  						"asn": 123,
   343  						"country": "Lithuania",
   344  						"city": "Vilnius"
   345  					},
   346                      "quality": {
   347                        "quality": 2.0,
   348                        "latency": 50,
   349                        "bandwidth": 10,
   350                        "uptime": 20
   351                      },
   352  					"price": {
   353  					  "currency": "MYST",
   354  					  "per_gib": 1000000000000000000,
   355  					  "per_gib_tokens": {
   356  						"ether": "1",
   357  						"human": "1",
   358  						"wei": "1000000000000000000"
   359  					  },
   360  					  "per_hour": 500000000000000000,
   361  					  "per_hour_tokens": {
   362  						"ether": "0.5",
   363  						"human": "0.5",
   364  						"wei": "500000000000000000"
   365  					  }
   366  					}
   367                  },
   368                  {
   369                      "format": "service-proposal/v3",
   370                      "compatibility": 2,
   371                      "provider_id": "other_provider",
   372                      "service_type": "testprotocol",
   373                      "location": {
   374  						"asn": 123,
   375  						"country": "Lithuania",
   376  						"city": "Vilnius"
   377  					},
   378                      "quality": {
   379                        "quality": 2.0,
   380                        "latency": 50,
   381                        "bandwidth": 10,
   382                        "uptime": 20
   383                      },
   384  					"price": {
   385  					  "currency": "MYST",
   386  					  "per_gib": 1000000000000000000,
   387  					  "per_gib_tokens": {
   388  						"ether": "1",
   389  						"human": "1",
   390  						"wei": "1000000000000000000"
   391  					  },
   392  					  "per_hour": 500000000000000000,
   393  					  "per_hour_tokens": {
   394  						"ether": "0.5",
   395  						"human": "0.5",
   396  						"wei": "500000000000000000"
   397  					  }
   398  					}
   399                  }
   400              ]
   401          }`,
   402  		resp.Body.String(),
   403  	)
   404  }
   405  
   406  type mockProposalRepository struct {
   407  	proposals      []proposal.PricedServiceProposal
   408  	recordedFilter *proposal.Filter
   409  	priceToAdd     market.Price
   410  }
   411  
   412  func (m *mockProposalRepository) Proposal(_ market.ProposalID) (*proposal.PricedServiceProposal, error) {
   413  	if len(m.proposals) == 0 {
   414  		return nil, nil
   415  	}
   416  	return &m.proposals[0], nil
   417  }
   418  
   419  func (m *mockProposalRepository) Proposals(filter *proposal.Filter) ([]proposal.PricedServiceProposal, error) {
   420  	m.recordedFilter = filter
   421  	return m.proposals, nil
   422  }
   423  
   424  func (m *mockProposalRepository) Countries(filter *proposal.Filter) (map[string]int, error) {
   425  	m.recordedFilter = filter
   426  	return nil, nil
   427  }
   428  
   429  func (m *mockProposalRepository) EnrichProposalWithPrice(in market.ServiceProposal) (proposal.PricedServiceProposal, error) {
   430  	return proposal.PricedServiceProposal{
   431  		Price:           m.priceToAdd,
   432  		ServiceProposal: in,
   433  	}, nil
   434  }
   435  
   436  type mockFilterPresetRepository struct {
   437  	presets proposal.FilterPresets
   438  }
   439  
   440  func (m *mockFilterPresetRepository) List() (*proposal.FilterPresets, error) {
   441  	return &m.presets, nil
   442  }
   443  
   444  func (m *mockFilterPresetRepository) Get(id int) (*proposal.FilterPreset, error) {
   445  	for _, p := range m.presets.Entries {
   446  		if p.ID == id {
   447  			return &p, nil
   448  		}
   449  	}
   450  	return nil, errors.New("preset not found")
   451  }
   452  
   453  func setPricingBounds(v url.Values) {
   454  	v.Add("price_hour_max", fmt.Sprintf("%v", priceHourMax))
   455  	v.Add("price_gib_max", fmt.Sprintf("%v", priceGiBMax))
   456  }