github.com/prebid/prebid-server/v2@v2.18.0/gdpr/vendorlist-fetching_test.go (about)

     1  package gdpr
     2  
     3  import (
     4  	"context"
     5  	"net/http"
     6  	"net/http/httptest"
     7  	"strconv"
     8  	"testing"
     9  
    10  	"github.com/stretchr/testify/assert"
    11  
    12  	"github.com/prebid/go-gdpr/api"
    13  	"github.com/prebid/go-gdpr/consentconstants"
    14  	"github.com/prebid/prebid-server/v2/config"
    15  	"github.com/prebid/prebid-server/v2/util/jsonutil"
    16  )
    17  
    18  func TestFetcherDynamicLoadListExists(t *testing.T) {
    19  	// Loads the first vendor list during initialization by setting the latest vendor list version to 1.
    20  	// All other vendor lists will be dynamically loaded.
    21  
    22  	server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{
    23  		vendorListLatestVersion: 1,
    24  		vendorLists: map[int]map[int]string{
    25  			3: {
    26  				1: vendorList1,
    27  				2: vendorList2,
    28  			},
    29  		},
    30  	})))
    31  	defer server.Close()
    32  
    33  	test := test{
    34  		description: "Dynamic Load - List Exists",
    35  		setup: testSetup{
    36  			specVersion: 3,
    37  			listVersion: 2,
    38  		},
    39  		expected: vendorList2Expected,
    40  	}
    41  
    42  	runTest(t, test, server)
    43  }
    44  
    45  func TestFetcherDynamicLoadListDoesntExist(t *testing.T) {
    46  	// Loads the first vendor list during initialization by setting the latest vendor list version to 1.
    47  	// All other vendor list load attempts will be done dynamically.
    48  
    49  	server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{
    50  		vendorListLatestVersion: 1,
    51  		vendorLists: map[int]map[int]string{
    52  			3: {
    53  				1: vendorList1,
    54  			},
    55  		},
    56  	})))
    57  	defer server.Close()
    58  
    59  	test := test{
    60  		description: "No Fallback - Vendor Doesn't Exist",
    61  		setup: testSetup{
    62  			specVersion: 3,
    63  			listVersion: 2,
    64  		},
    65  		expected: testExpected{
    66  			errorMessage: "gdpr vendor list spec version 3 list version 2 does not exist, or has not been loaded yet. Try again in a few minutes",
    67  		},
    68  	}
    69  
    70  	runTest(t, test, server)
    71  }
    72  
    73  func TestFetcherThrottling(t *testing.T) {
    74  	server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{
    75  		vendorListLatestVersion: 1,
    76  		vendorLists: map[int]map[int]string{
    77  			3: {
    78  				1: MarshalVendorList(vendorList{
    79  					GVLSpecificationVersion: 3,
    80  					VendorListVersion:       1,
    81  					Vendors:                 map[string]*vendor{"12": {ID: 12, Purposes: []int{1}}},
    82  				}),
    83  				2: MarshalVendorList(vendorList{
    84  					GVLSpecificationVersion: 3,
    85  					VendorListVersion:       2,
    86  					Vendors:                 map[string]*vendor{"12": {ID: 12, Purposes: []int{1, 2}}},
    87  				}),
    88  				3: MarshalVendorList(vendorList{
    89  					GVLSpecificationVersion: 3,
    90  					VendorListVersion:       3,
    91  					Vendors:                 map[string]*vendor{"12": {ID: 12, Purposes: []int{1, 2, 3}}},
    92  				}),
    93  			},
    94  		},
    95  	})))
    96  	defer server.Close()
    97  
    98  	fetcher := NewVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server))
    99  
   100  	// Dynamically Load List 2 Successfully
   101  	_, errList1 := fetcher(context.Background(), 3, 2)
   102  	assert.NoError(t, errList1)
   103  
   104  	// Fail To Load List 3 Due To Rate Limiting
   105  	// - The request is rate limited after dynamically list 2.
   106  	_, errList2 := fetcher(context.Background(), 3, 3)
   107  	assert.EqualError(t, errList2, "gdpr vendor list spec version 3 list version 3 does not exist, or has not been loaded yet. Try again in a few minutes")
   108  }
   109  
   110  func TestMalformedVendorlist(t *testing.T) {
   111  	server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{
   112  		vendorListLatestVersion: 1,
   113  		vendorLists: map[int]map[int]string{
   114  			3: {
   115  				1: "malformed",
   116  			},
   117  		},
   118  	})))
   119  	defer server.Close()
   120  
   121  	fetcher := NewVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server))
   122  	_, err := fetcher(context.Background(), 3, 1)
   123  
   124  	// Fetching should fail since vendor list could not be unmarshalled.
   125  	assert.Error(t, err)
   126  }
   127  
   128  func TestServerUrlInvalid(t *testing.T) {
   129  	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
   130  	server.Close()
   131  
   132  	invalidURLGenerator := func(uint16, uint16) string { return " http://invalid-url-has-leading-whitespace" }
   133  
   134  	fetcher := NewVendorListFetcher(context.Background(), testConfig(), server.Client(), invalidURLGenerator)
   135  	_, err := fetcher(context.Background(), 3, 1)
   136  
   137  	assert.EqualError(t, err, "gdpr vendor list spec version 3 list version 1 does not exist, or has not been loaded yet. Try again in a few minutes")
   138  }
   139  
   140  func TestServerUnavailable(t *testing.T) {
   141  	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
   142  	server.Close()
   143  
   144  	fetcher := NewVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server))
   145  	_, err := fetcher(context.Background(), 3, 1)
   146  
   147  	assert.EqualError(t, err, "gdpr vendor list spec version 3 list version 1 does not exist, or has not been loaded yet. Try again in a few minutes")
   148  }
   149  
   150  func TestVendorListURLMaker(t *testing.T) {
   151  	testCases := []struct {
   152  		description string
   153  		specVersion uint16
   154  		listVersion uint16
   155  		expectedURL string
   156  	}{
   157  		{
   158  			description: "Spec version 2 latest list",
   159  			specVersion: 2,
   160  			listVersion: 0,
   161  			expectedURL: "https://vendor-list.consensu.org/v2/vendor-list.json",
   162  		},
   163  		{
   164  			description: "Spec version 2 specific list",
   165  			specVersion: 2,
   166  			listVersion: 42,
   167  			expectedURL: "https://vendor-list.consensu.org/v2/archives/vendor-list-v42.json",
   168  		},
   169  		{
   170  			description: "Spec version 3 latest list",
   171  			specVersion: 3,
   172  			listVersion: 0,
   173  			expectedURL: "https://vendor-list.consensu.org/v3/vendor-list.json",
   174  		},
   175  		{
   176  			description: "Spec version 3 specific list",
   177  			specVersion: 3,
   178  			listVersion: 42,
   179  			expectedURL: "https://vendor-list.consensu.org/v3/archives/vendor-list-v42.json",
   180  		},
   181  	}
   182  
   183  	for _, test := range testCases {
   184  		result := VendorListURLMaker(test.specVersion, test.listVersion)
   185  		assert.Equal(t, test.expectedURL, result)
   186  	}
   187  }
   188  
   189  type versionInfo struct {
   190  	specVersion uint16
   191  	listVersion uint16
   192  }
   193  type saver []versionInfo
   194  
   195  func (s *saver) saveVendorLists(specVersion uint16, listVersion uint16, gvl api.VendorList) {
   196  	vi := versionInfo{
   197  		specVersion: specVersion,
   198  		listVersion: listVersion,
   199  	}
   200  	*s = append(*s, vi)
   201  }
   202  
   203  func TestPreloadCache(t *testing.T) {
   204  	server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{
   205  		vendorListLatestVersion: 3,
   206  		vendorLists: map[int]map[int]string{
   207  			1: {
   208  				1: MarshalVendorList(vendorList{
   209  					GVLSpecificationVersion: 1, VendorListVersion: 1,
   210  				}),
   211  				2: MarshalVendorList(vendorList{
   212  					GVLSpecificationVersion: 1, VendorListVersion: 2,
   213  				}),
   214  				3: MarshalVendorList(vendorList{
   215  					GVLSpecificationVersion: 1, VendorListVersion: 3,
   216  				}),
   217  			},
   218  			2: {
   219  				1: MarshalVendorList(vendorList{
   220  					GVLSpecificationVersion: 2, VendorListVersion: 1,
   221  				}),
   222  				2: MarshalVendorList(vendorList{
   223  					GVLSpecificationVersion: 2, VendorListVersion: 2,
   224  				}),
   225  				3: MarshalVendorList(vendorList{
   226  					GVLSpecificationVersion: 2, VendorListVersion: 3,
   227  				}),
   228  			},
   229  			3: {
   230  				1: MarshalVendorList(vendorList{
   231  					GVLSpecificationVersion: 3, VendorListVersion: 1,
   232  				}),
   233  				2: MarshalVendorList(vendorList{
   234  					GVLSpecificationVersion: 3, VendorListVersion: 2,
   235  				}),
   236  				3: MarshalVendorList(vendorList{
   237  					GVLSpecificationVersion: 3, VendorListVersion: 3,
   238  				}),
   239  			},
   240  			4: {
   241  				1: MarshalVendorList(vendorList{
   242  					GVLSpecificationVersion: 4, VendorListVersion: 1,
   243  				}),
   244  				2: MarshalVendorList(vendorList{
   245  					GVLSpecificationVersion: 4, VendorListVersion: 2,
   246  				}),
   247  				3: MarshalVendorList(vendorList{
   248  					GVLSpecificationVersion: 4, VendorListVersion: 3,
   249  				}),
   250  			},
   251  		},
   252  	})))
   253  	defer server.Close()
   254  
   255  	s := make(saver, 0, 5)
   256  	preloadCache(context.Background(), server.Client(), testURLMaker(server), s.saveVendorLists)
   257  
   258  	expectedLoadedVersions := []versionInfo{
   259  		{specVersion: 2, listVersion: 2},
   260  		{specVersion: 2, listVersion: 3},
   261  		{specVersion: 3, listVersion: 1},
   262  		{specVersion: 3, listVersion: 2},
   263  		{specVersion: 3, listVersion: 3},
   264  	}
   265  
   266  	assert.ElementsMatch(t, expectedLoadedVersions, s)
   267  }
   268  
   269  var vendorList1 = MarshalVendorList(vendorList{
   270  	GVLSpecificationVersion: 3,
   271  	VendorListVersion:       1,
   272  	Vendors:                 map[string]*vendor{"12": {ID: 12, Purposes: []int{2}}},
   273  })
   274  
   275  var vendorList2 = MarshalVendorList(vendorList{
   276  	GVLSpecificationVersion: 3,
   277  	VendorListVersion:       2,
   278  	Vendors:                 map[string]*vendor{"12": {ID: 12, Purposes: []int{2, 3}}},
   279  })
   280  
   281  var vendorList2Expected = testExpected{
   282  	vendorListVersion: 2,
   283  	vendorID:          12,
   284  	vendorPurposes:    map[int]bool{1: false, 2: true, 3: true},
   285  }
   286  
   287  var vendorListFallbackExpected = testExpected{
   288  	vendorListVersion: 215, // Values from hardcoded fallback file.
   289  	vendorID:          12,
   290  	vendorPurposes:    map[int]bool{1: true, 2: false, 3: true},
   291  }
   292  
   293  type vendorList struct {
   294  	GVLSpecificationVersion uint16             `json:"gvlSpecificationVersion"`
   295  	VendorListVersion       uint16             `json:"vendorListVersion"`
   296  	Vendors                 map[string]*vendor `json:"vendors"`
   297  }
   298  
   299  type vendor struct {
   300  	ID               uint16 `json:"id"`
   301  	Purposes         []int  `json:"purposes"`
   302  	LegIntPurposes   []int  `json:"legIntPurposes"`
   303  	FlexiblePurposes []int  `json:"flexiblePurposes"`
   304  	SpecialFeatures  []int  `json:"specialFeatures"`
   305  }
   306  
   307  func MarshalVendorList(vendorList vendorList) string {
   308  	json, _ := jsonutil.Marshal(vendorList)
   309  	return string(json)
   310  }
   311  
   312  type serverSettings struct {
   313  	vendorListLatestVersion int
   314  	vendorLists             map[int]map[int]string
   315  }
   316  
   317  // mockServer returns a handler which returns the given response for each global vendor list version.
   318  // The latestVersion param can be used to mock "updates" which occur after PBS has been turned on.
   319  // For example, if latestVersion is 3, but the responses map has data at "4", the server will return
   320  // version "3" when asked for the latest version.
   321  //
   322  // This will help test lazy-fetches for versions which aren't there on app startup.
   323  //
   324  // If the "version" query param doesn't exist, it returns a 400.
   325  //
   326  // If the "version" query param points to a version which doesn't exist, it returns a 403.
   327  // Don't ask why... that's just what the official page is doing. See https://vendor-list.consensu.org/v-9999/vendorlist.json
   328  func mockServer(settings serverSettings) func(http.ResponseWriter, *http.Request) {
   329  	return func(w http.ResponseWriter, req *http.Request) {
   330  		specVersion := req.URL.Query().Get("specversion")
   331  		specVersionInt, err := strconv.Atoi(specVersion)
   332  		if err != nil {
   333  			w.WriteHeader(http.StatusBadRequest)
   334  			w.Write([]byte("Request had invalid spec version: " + specVersion))
   335  			return
   336  		}
   337  		listVersion := req.URL.Query().Get("listversion")
   338  		listVersionInt, err := strconv.Atoi(listVersion)
   339  		if err != nil {
   340  			w.WriteHeader(http.StatusBadRequest)
   341  			w.Write([]byte("Request had invalid version: " + listVersion))
   342  			return
   343  		}
   344  		if listVersionInt == 0 {
   345  			listVersionInt = settings.vendorListLatestVersion
   346  		}
   347  		specVersionVendorLists, ok := settings.vendorLists[specVersionInt]
   348  		if !ok {
   349  			w.WriteHeader(http.StatusForbidden)
   350  			w.Write([]byte("Version not found: spec version " + specVersion + " list version " + listVersion))
   351  			return
   352  		}
   353  		response, ok := specVersionVendorLists[listVersionInt]
   354  		if !ok {
   355  			w.WriteHeader(http.StatusForbidden)
   356  			w.Write([]byte("Version not found: " + listVersion))
   357  			return
   358  		}
   359  		w.Write([]byte(response))
   360  	}
   361  }
   362  
   363  type test struct {
   364  	description string
   365  	setup       testSetup
   366  	expected    testExpected
   367  }
   368  
   369  type testSetup struct {
   370  	specVersion uint16
   371  	listVersion uint16
   372  }
   373  
   374  type testExpected struct {
   375  	errorMessage      string
   376  	vendorListVersion uint16
   377  	vendorID          uint16
   378  	vendorPurposes    map[int]bool
   379  }
   380  
   381  func runTest(t *testing.T, test test, server *httptest.Server) {
   382  	config := testConfig()
   383  	fetcher := NewVendorListFetcher(context.Background(), config, server.Client(), testURLMaker(server))
   384  	vendorList, err := fetcher(context.Background(), test.setup.specVersion, test.setup.listVersion)
   385  
   386  	if test.expected.errorMessage != "" {
   387  		assert.EqualError(t, err, test.expected.errorMessage, test.description+":error")
   388  	} else {
   389  		assert.NoError(t, err, test.description+":vendorlist")
   390  		assert.Equal(t, test.expected.vendorListVersion, vendorList.Version(), test.description+":vendorlistid")
   391  		vendor := vendorList.Vendor(test.expected.vendorID)
   392  		for id, expected := range test.expected.vendorPurposes {
   393  			result := vendor.Purpose(consentconstants.Purpose(id))
   394  			assert.Equalf(t, expected, result, "%s:vendor-%d:purpose-%d", test.description, vendorList.Version(), id)
   395  		}
   396  	}
   397  }
   398  
   399  func testURLMaker(server *httptest.Server) func(uint16, uint16) string {
   400  	url := server.URL
   401  	return func(specVersion, listVersion uint16) string {
   402  		return url + "?specversion=" + strconv.Itoa(int(specVersion)) + "&listversion=" + strconv.Itoa(int(listVersion))
   403  	}
   404  }
   405  
   406  func testConfig() config.GDPR {
   407  	return config.GDPR{
   408  		Timeouts: config.GDPRTimeouts{
   409  			InitVendorlistFetch:   60 * 1000,
   410  			ActiveVendorlistFetch: 1000 * 5,
   411  		},
   412  	}
   413  }