github.com/prebid/prebid-server/v2@v2.18.0/usersync/chooser_test.go (about)

     1  package usersync
     2  
     3  import (
     4  	"testing"
     5  	"time"
     6  
     7  	"github.com/prebid/prebid-server/v2/config"
     8  	"github.com/prebid/prebid-server/v2/openrtb_ext"
     9  	"github.com/prebid/prebid-server/v2/util/ptrutil"
    10  	"github.com/stretchr/testify/assert"
    11  	"github.com/stretchr/testify/mock"
    12  
    13  	"github.com/prebid/prebid-server/v2/macros"
    14  )
    15  
    16  func TestNewChooser(t *testing.T) {
    17  	testCases := []struct {
    18  		description              string
    19  		bidderSyncerLookup       map[string]Syncer
    20  		bidderInfo               map[string]config.BidderInfo
    21  		expectedBiddersAvailable []string
    22  	}{
    23  		{
    24  			description:              "Nil",
    25  			bidderSyncerLookup:       nil,
    26  			expectedBiddersAvailable: []string{},
    27  		},
    28  		{
    29  			description:              "Empty",
    30  			bidderSyncerLookup:       map[string]Syncer{},
    31  			expectedBiddersAvailable: []string{},
    32  		},
    33  		{
    34  			description:              "One",
    35  			bidderSyncerLookup:       map[string]Syncer{"a": fakeSyncer{}},
    36  			expectedBiddersAvailable: []string{"a"},
    37  		},
    38  		{
    39  			description:              "Many",
    40  			bidderSyncerLookup:       map[string]Syncer{"a": fakeSyncer{}, "b": fakeSyncer{}},
    41  			expectedBiddersAvailable: []string{"a", "b"},
    42  		},
    43  	}
    44  
    45  	for _, test := range testCases {
    46  		chooser, _ := NewChooser(test.bidderSyncerLookup, make(map[string]struct{}), test.bidderInfo).(standardChooser)
    47  		assert.ElementsMatch(t, test.expectedBiddersAvailable, chooser.biddersAvailable, test.description)
    48  	}
    49  }
    50  
    51  func TestChooserChoose(t *testing.T) {
    52  	fakeSyncerA := fakeSyncer{key: "keyA", supportsIFrame: true}
    53  	fakeSyncerB := fakeSyncer{key: "keyB", supportsIFrame: true}
    54  	fakeSyncerC := fakeSyncer{key: "keyC", supportsIFrame: false}
    55  
    56  	duplicateSyncer := fakeSyncer{key: "syncerForDuplicateTest", supportsIFrame: true}
    57  	bidderSyncerLookup := map[string]Syncer{"a": fakeSyncerA, "b": fakeSyncerB, "c": fakeSyncerC, "appnexus": fakeSyncerA, "d": duplicateSyncer, "e": duplicateSyncer}
    58  	biddersKnown := map[string]struct{}{"a": {}, "b": {}, "c": {}}
    59  
    60  	normalizedBidderNamesLookup := func(name string) (openrtb_ext.BidderName, bool) {
    61  		return openrtb_ext.BidderName(name), true
    62  	}
    63  
    64  	syncerChoiceA := SyncerChoice{Bidder: "a", Syncer: fakeSyncerA}
    65  	syncerChoiceB := SyncerChoice{Bidder: "b", Syncer: fakeSyncerB}
    66  
    67  	syncTypeFilter := SyncTypeFilter{
    68  		IFrame:   NewUniformBidderFilter(BidderFilterModeInclude),
    69  		Redirect: NewUniformBidderFilter(BidderFilterModeExclude),
    70  	}
    71  
    72  	cooperativeConfig := Cooperative{Enabled: true}
    73  
    74  	usersyncDisabled := ptrutil.ToPtr(false)
    75  
    76  	testCases := []struct {
    77  		description        string
    78  		givenRequest       Request
    79  		givenChosenBidders []string
    80  		givenCookie        Cookie
    81  		givenBidderInfo    map[string]config.BidderInfo
    82  		bidderNamesLookup  func(name string) (openrtb_ext.BidderName, bool)
    83  		expected           Result
    84  	}{
    85  		{
    86  			description: "Cookie Opt Out",
    87  			givenRequest: Request{
    88  				Privacy: &fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
    89  				Limit:   0,
    90  			},
    91  			givenChosenBidders: []string{"a"},
    92  			givenCookie:        Cookie{optOut: true},
    93  			bidderNamesLookup:  normalizedBidderNamesLookup,
    94  			expected: Result{
    95  				Status:           StatusBlockedByUserOptOut,
    96  				BiddersEvaluated: nil,
    97  				SyncersChosen:    nil,
    98  			},
    99  		},
   100  		{
   101  			description: "GDPR Host Cookie Not Allowed",
   102  			givenRequest: Request{
   103  				Privacy: &fakePrivacy{gdprAllowsHostCookie: false, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   104  				Limit:   0,
   105  			},
   106  			givenChosenBidders: []string{"a"},
   107  			givenCookie:        Cookie{},
   108  			bidderNamesLookup:  normalizedBidderNamesLookup,
   109  			expected: Result{
   110  				Status:           StatusBlockedByPrivacy,
   111  				BiddersEvaluated: nil,
   112  				SyncersChosen:    nil,
   113  			},
   114  		},
   115  		{
   116  			description: "No Bidders",
   117  			givenRequest: Request{
   118  				Privacy: &fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   119  				Limit:   0,
   120  			},
   121  			givenChosenBidders: []string{},
   122  			givenCookie:        Cookie{},
   123  			bidderNamesLookup:  normalizedBidderNamesLookup,
   124  			expected: Result{
   125  				Status:           StatusOK,
   126  				BiddersEvaluated: []BidderEvaluation{},
   127  				SyncersChosen:    []SyncerChoice{},
   128  			},
   129  		},
   130  		{
   131  			description: "One Bidder - Sync",
   132  			givenRequest: Request{
   133  				Privacy: &fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   134  				Limit:   0,
   135  			},
   136  			givenChosenBidders: []string{"a"},
   137  			givenCookie:        Cookie{},
   138  			bidderNamesLookup:  normalizedBidderNamesLookup,
   139  			expected: Result{
   140  				Status:           StatusOK,
   141  				BiddersEvaluated: []BidderEvaluation{{Bidder: "a", SyncerKey: "keyA", Status: StatusOK}},
   142  				SyncersChosen:    []SyncerChoice{syncerChoiceA},
   143  			},
   144  		},
   145  		{
   146  			description: "One Bidder - No Sync",
   147  			givenRequest: Request{
   148  				Privacy: &fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   149  				Limit:   0,
   150  			},
   151  			givenChosenBidders: []string{"c"},
   152  			givenCookie:        Cookie{},
   153  			bidderNamesLookup:  normalizedBidderNamesLookup,
   154  			expected: Result{
   155  				Status:           StatusOK,
   156  				BiddersEvaluated: []BidderEvaluation{{Bidder: "c", SyncerKey: "keyC", Status: StatusTypeNotSupported}},
   157  				SyncersChosen:    []SyncerChoice{},
   158  			},
   159  		},
   160  		{
   161  			description: "One Bidder - No Sync - Unknown",
   162  			givenRequest: Request{
   163  				Privacy: &fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   164  				Limit:   0,
   165  			},
   166  			givenChosenBidders: []string{"unknown"},
   167  			givenCookie:        Cookie{},
   168  			bidderNamesLookup:  normalizedBidderNamesLookup,
   169  			expected: Result{
   170  				Status:           StatusOK,
   171  				BiddersEvaluated: []BidderEvaluation{{Bidder: "unknown", Status: StatusUnknownBidder}},
   172  				SyncersChosen:    []SyncerChoice{},
   173  			},
   174  		},
   175  		{
   176  			description: "Many Bidders - All Sync - Limit Disabled With 0",
   177  			givenRequest: Request{
   178  				Privacy: &fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   179  				Limit:   0,
   180  			},
   181  			givenChosenBidders: []string{"a", "b"},
   182  			givenCookie:        Cookie{},
   183  			bidderNamesLookup:  normalizedBidderNamesLookup,
   184  			expected: Result{
   185  				Status:           StatusOK,
   186  				BiddersEvaluated: []BidderEvaluation{{Bidder: "a", SyncerKey: "keyA", Status: StatusOK}, {Bidder: "b", SyncerKey: "keyB", Status: StatusOK}},
   187  				SyncersChosen:    []SyncerChoice{syncerChoiceA, syncerChoiceB},
   188  			},
   189  		},
   190  		{
   191  			description: "Many Bidders - All Sync - Limit Disabled With Negative Value",
   192  			givenRequest: Request{
   193  				Privacy: &fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   194  				Limit:   -1,
   195  			},
   196  			givenChosenBidders: []string{"a", "b"},
   197  			givenCookie:        Cookie{},
   198  			bidderNamesLookup:  normalizedBidderNamesLookup,
   199  			expected: Result{
   200  				Status:           StatusOK,
   201  				BiddersEvaluated: []BidderEvaluation{{Bidder: "a", SyncerKey: "keyA", Status: StatusOK}, {Bidder: "b", SyncerKey: "keyB", Status: StatusOK}},
   202  				SyncersChosen:    []SyncerChoice{syncerChoiceA, syncerChoiceB},
   203  			},
   204  		},
   205  		{
   206  			description: "Many Bidders - Limited Sync",
   207  			givenRequest: Request{
   208  				Privacy: &fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   209  				Limit:   1,
   210  			},
   211  			givenChosenBidders: []string{"a", "b"},
   212  			givenCookie:        Cookie{},
   213  			bidderNamesLookup:  normalizedBidderNamesLookup,
   214  			expected: Result{
   215  				Status:           StatusOK,
   216  				BiddersEvaluated: []BidderEvaluation{{Bidder: "a", SyncerKey: "keyA", Status: StatusOK}},
   217  				SyncersChosen:    []SyncerChoice{syncerChoiceA},
   218  			},
   219  		},
   220  		{
   221  			description: "Many Bidders - Limited Sync - Disqualified Syncers Don't Count Towards Limit",
   222  			givenRequest: Request{
   223  				Privacy: &fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   224  				Limit:   1,
   225  			},
   226  			givenChosenBidders: []string{"c", "a", "b"},
   227  			givenCookie:        Cookie{},
   228  			bidderNamesLookup:  normalizedBidderNamesLookup,
   229  			expected: Result{
   230  				Status:           StatusOK,
   231  				BiddersEvaluated: []BidderEvaluation{{Bidder: "c", SyncerKey: "keyC", Status: StatusTypeNotSupported}, {Bidder: "a", SyncerKey: "keyA", Status: StatusOK}},
   232  				SyncersChosen:    []SyncerChoice{syncerChoiceA},
   233  			},
   234  		},
   235  		{
   236  			description: "Many Bidders - Some Sync, Some Don't",
   237  			givenRequest: Request{
   238  				Privacy: &fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   239  				Limit:   0,
   240  			},
   241  			givenChosenBidders: []string{"a", "c"},
   242  			givenCookie:        Cookie{},
   243  			bidderNamesLookup:  normalizedBidderNamesLookup,
   244  			expected: Result{
   245  				Status:           StatusOK,
   246  				BiddersEvaluated: []BidderEvaluation{{Bidder: "a", SyncerKey: "keyA", Status: StatusOK}, {Bidder: "c", SyncerKey: "keyC", Status: StatusTypeNotSupported}},
   247  				SyncersChosen:    []SyncerChoice{syncerChoiceA},
   248  			},
   249  		},
   250  		{
   251  			description: "Chosen bidders have duplicate syncer keys, the one that comes first should be labled OK",
   252  			givenRequest: Request{
   253  				Privacy: &fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   254  				Limit:   0,
   255  			},
   256  			givenChosenBidders: []string{"d", "e"},
   257  			givenCookie:        Cookie{},
   258  			bidderNamesLookup:  normalizedBidderNamesLookup,
   259  			expected: Result{
   260  				Status: StatusOK,
   261  				BiddersEvaluated: []BidderEvaluation{
   262  					{Bidder: "d", SyncerKey: "syncerForDuplicateTest", Status: StatusOK},
   263  					{Bidder: "e", SyncerKey: "syncerForDuplicateTest", Status: StatusDuplicate},
   264  				},
   265  				SyncersChosen: []SyncerChoice{{Bidder: "d", Syncer: duplicateSyncer}},
   266  			},
   267  		},
   268  		{
   269  			description: "Chosen bidders have duplicate syncer keys, the one that comes first should be labled OK (reverse)",
   270  			givenRequest: Request{
   271  				Privacy: &fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   272  				Limit:   0,
   273  			},
   274  			givenChosenBidders: []string{"e", "d"},
   275  			givenCookie:        Cookie{},
   276  			bidderNamesLookup:  normalizedBidderNamesLookup,
   277  			expected: Result{
   278  				Status: StatusOK,
   279  				BiddersEvaluated: []BidderEvaluation{
   280  					{Bidder: "e", SyncerKey: "syncerForDuplicateTest", Status: StatusOK},
   281  					{Bidder: "d", SyncerKey: "syncerForDuplicateTest", Status: StatusDuplicate},
   282  				},
   283  				SyncersChosen: []SyncerChoice{{Bidder: "e", Syncer: duplicateSyncer}},
   284  			},
   285  		},
   286  		{
   287  			description: "Same bidder name, no duplicate warning",
   288  			givenRequest: Request{
   289  				Privacy: &fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   290  				Limit:   0,
   291  			},
   292  			givenChosenBidders: []string{"a", "a"},
   293  			givenCookie:        Cookie{},
   294  			bidderNamesLookup:  normalizedBidderNamesLookup,
   295  			expected: Result{
   296  				Status: StatusOK,
   297  				BiddersEvaluated: []BidderEvaluation{
   298  					{Bidder: "a", SyncerKey: fakeSyncerA.key, Status: StatusOK},
   299  				},
   300  				SyncersChosen: []SyncerChoice{{Bidder: "a", Syncer: fakeSyncerA}},
   301  			},
   302  		},
   303  		{
   304  			description: "Unknown Bidder",
   305  			givenRequest: Request{
   306  				Privacy: &fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   307  				Limit:   0,
   308  			},
   309  			givenChosenBidders: []string{"a"},
   310  			givenCookie:        Cookie{},
   311  			bidderNamesLookup: func(name string) (openrtb_ext.BidderName, bool) {
   312  				return openrtb_ext.BidderName(name), false
   313  			},
   314  			expected: Result{
   315  				Status:           StatusOK,
   316  				BiddersEvaluated: []BidderEvaluation{{Bidder: "a", Status: StatusUnknownBidder}},
   317  				SyncersChosen:    []SyncerChoice{},
   318  			},
   319  		},
   320  		{
   321  			description: "Case insensitive bidder name",
   322  			givenRequest: Request{
   323  				Privacy: &fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   324  				Limit:   0,
   325  			},
   326  			givenChosenBidders: []string{"AppNexus"},
   327  			givenCookie:        Cookie{},
   328  			bidderNamesLookup:  openrtb_ext.NormalizeBidderName,
   329  			expected: Result{
   330  				Status:           StatusOK,
   331  				BiddersEvaluated: []BidderEvaluation{{Bidder: "AppNexus", SyncerKey: "keyA", Status: StatusOK}},
   332  				SyncersChosen:    []SyncerChoice{{Bidder: "AppNexus", Syncer: fakeSyncerA}},
   333  			},
   334  		},
   335  		{
   336  			description: "Duplicate bidder name",
   337  			givenRequest: Request{
   338  				Privacy: &fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   339  				Limit:   0,
   340  			},
   341  			givenChosenBidders: []string{"AppNexus", "appNexus"},
   342  			givenCookie:        Cookie{},
   343  			bidderNamesLookup:  openrtb_ext.NormalizeBidderName,
   344  			expected: Result{
   345  				Status:           StatusOK,
   346  				BiddersEvaluated: []BidderEvaluation{{Bidder: "AppNexus", SyncerKey: "keyA", Status: StatusOK}, {Bidder: "appNexus", SyncerKey: "keyA", Status: StatusDuplicate}},
   347  				SyncersChosen:    []SyncerChoice{{Bidder: "AppNexus", Syncer: fakeSyncerA}},
   348  			},
   349  		},
   350  		{
   351  			description: "Disabled Usersync",
   352  			givenRequest: Request{
   353  				Privacy: &fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   354  				Limit:   0,
   355  			},
   356  			givenChosenBidders: []string{"a"},
   357  			givenCookie:        Cookie{},
   358  			givenBidderInfo: map[string]config.BidderInfo{
   359  				"a": {
   360  					Syncer: &config.Syncer{
   361  						Enabled: usersyncDisabled,
   362  					},
   363  				},
   364  			},
   365  			bidderNamesLookup: normalizedBidderNamesLookup,
   366  			expected: Result{
   367  				Status:           StatusOK,
   368  				BiddersEvaluated: []BidderEvaluation{{Bidder: "a", SyncerKey: "keyA", Status: StatusBlockedByDisabledUsersync}},
   369  				SyncersChosen:    []SyncerChoice{},
   370  			},
   371  		},
   372  		{
   373  			description: "Regulation Scope GDPR",
   374  			givenRequest: Request{
   375  				Privacy: &fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true, gdprInScope: true},
   376  				Limit:   0,
   377  			},
   378  			givenChosenBidders: []string{"a"},
   379  			givenCookie:        Cookie{},
   380  			givenBidderInfo: map[string]config.BidderInfo{
   381  				"a": {
   382  					Syncer: &config.Syncer{
   383  						SkipWhen: &config.SkipWhen{
   384  							GDPR: true,
   385  						},
   386  					},
   387  				},
   388  			},
   389  			bidderNamesLookup: normalizedBidderNamesLookup,
   390  			expected: Result{
   391  				Status:           StatusOK,
   392  				BiddersEvaluated: []BidderEvaluation{{Bidder: "a", SyncerKey: "keyA", Status: StatusBlockedByRegulationScope}},
   393  				SyncersChosen:    []SyncerChoice{},
   394  			},
   395  		},
   396  		{
   397  			description: "Regulation Scope GPP",
   398  			givenRequest: Request{
   399  				Privacy: &fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   400  				Limit:   0,
   401  				GPPSID:  "2",
   402  			},
   403  			givenChosenBidders: []string{"a"},
   404  			givenCookie:        Cookie{},
   405  			givenBidderInfo: map[string]config.BidderInfo{
   406  				"a": {
   407  					Syncer: &config.Syncer{
   408  						SkipWhen: &config.SkipWhen{
   409  							GPPSID: []string{"2", "3"},
   410  						},
   411  					},
   412  				},
   413  			},
   414  			bidderNamesLookup: normalizedBidderNamesLookup,
   415  			expected: Result{
   416  				Status:           StatusOK,
   417  				BiddersEvaluated: []BidderEvaluation{{Bidder: "a", SyncerKey: "keyA", Status: StatusBlockedByRegulationScope}},
   418  				SyncersChosen:    []SyncerChoice{},
   419  			},
   420  		},
   421  	}
   422  
   423  	bidders := []string{"anyRequested"}
   424  	biddersAvailable := []string{"anyAvailable"}
   425  	for _, test := range testCases {
   426  		// set request values which don't need to be specified for each test case
   427  		test.givenRequest.Bidders = bidders
   428  		test.givenRequest.SyncTypeFilter = syncTypeFilter
   429  		test.givenRequest.Cooperative = cooperativeConfig
   430  
   431  		mockBidderChooser := &mockBidderChooser{}
   432  		mockBidderChooser.
   433  			On("choose", test.givenRequest.Bidders, biddersAvailable, cooperativeConfig).
   434  			Return(test.givenChosenBidders)
   435  
   436  		if test.givenBidderInfo == nil {
   437  			test.givenBidderInfo = map[string]config.BidderInfo{}
   438  		}
   439  
   440  		chooser := standardChooser{
   441  			bidderSyncerLookup:       bidderSyncerLookup,
   442  			biddersAvailable:         biddersAvailable,
   443  			bidderChooser:            mockBidderChooser,
   444  			normalizeValidBidderName: test.bidderNamesLookup,
   445  			biddersKnown:             biddersKnown,
   446  			bidderInfo:               test.givenBidderInfo,
   447  		}
   448  
   449  		result := chooser.Choose(test.givenRequest, &test.givenCookie)
   450  		assert.Equal(t, test.expected, result, test.description)
   451  	}
   452  }
   453  
   454  func TestChooserEvaluate(t *testing.T) {
   455  	fakeSyncerA := fakeSyncer{key: "keyA", supportsIFrame: true}
   456  	fakeSyncerB := fakeSyncer{key: "keyB", supportsIFrame: false}
   457  
   458  	biddersKnown := map[string]struct{}{"a": {}, "b": {}, "unconfigured": {}}
   459  	bidderSyncerLookup := map[string]Syncer{"a": fakeSyncerA, "b": fakeSyncerB, "appnexus": fakeSyncerA, "seedingAlliance": fakeSyncerA}
   460  
   461  	syncTypeFilter := SyncTypeFilter{
   462  		IFrame:   NewUniformBidderFilter(BidderFilterModeInclude),
   463  		Redirect: NewUniformBidderFilter(BidderFilterModeExclude),
   464  	}
   465  	normalizedBidderNamesLookup := func(name string) (openrtb_ext.BidderName, bool) {
   466  		return openrtb_ext.BidderName(name), true
   467  	}
   468  	cookieNeedsSync := Cookie{}
   469  	cookieAlreadyHasSyncForA := Cookie{uids: map[string]UIDEntry{"keyA": {Expires: time.Now().Add(time.Duration(24) * time.Hour)}}}
   470  	cookieAlreadyHasSyncForB := Cookie{uids: map[string]UIDEntry{"keyB": {Expires: time.Now().Add(time.Duration(24) * time.Hour)}}}
   471  
   472  	usersyncDisabled := ptrutil.ToPtr(false)
   473  
   474  	testCases := []struct {
   475  		description                 string
   476  		givenBidder                 string
   477  		normalisedBidderName        string
   478  		givenSyncersSeen            map[string]struct{}
   479  		givenPrivacy                fakePrivacy
   480  		givenCookie                 Cookie
   481  		givenGPPSID                 string
   482  		givenBidderInfo             map[string]config.BidderInfo
   483  		givenSyncTypeFilter         SyncTypeFilter
   484  		normalizedBidderNamesLookup func(name string) (openrtb_ext.BidderName, bool)
   485  		expectedSyncer              Syncer
   486  		expectedEvaluation          BidderEvaluation
   487  	}{
   488  		{
   489  			description:                 "Valid",
   490  			givenBidder:                 "a",
   491  			normalisedBidderName:        "a",
   492  			givenSyncersSeen:            map[string]struct{}{},
   493  			givenPrivacy:                fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   494  			givenCookie:                 cookieNeedsSync,
   495  			givenSyncTypeFilter:         syncTypeFilter,
   496  			normalizedBidderNamesLookup: normalizedBidderNamesLookup,
   497  			expectedSyncer:              fakeSyncerA,
   498  			expectedEvaluation:          BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusOK},
   499  		},
   500  		{
   501  			description:                 "Unknown Bidder",
   502  			givenBidder:                 "unknown",
   503  			normalisedBidderName:        "",
   504  			givenSyncersSeen:            map[string]struct{}{},
   505  			givenPrivacy:                fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   506  			givenCookie:                 cookieNeedsSync,
   507  			givenSyncTypeFilter:         syncTypeFilter,
   508  			normalizedBidderNamesLookup: normalizedBidderNamesLookup,
   509  			expectedSyncer:              nil,
   510  			expectedEvaluation:          BidderEvaluation{Bidder: "unknown", Status: StatusUnknownBidder},
   511  		},
   512  		{
   513  			description:                 "Duplicate Syncer",
   514  			givenBidder:                 "a",
   515  			normalisedBidderName:        "",
   516  			givenSyncersSeen:            map[string]struct{}{"keyA": {}},
   517  			givenPrivacy:                fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   518  			givenCookie:                 cookieNeedsSync,
   519  			givenSyncTypeFilter:         syncTypeFilter,
   520  			normalizedBidderNamesLookup: normalizedBidderNamesLookup,
   521  			expectedSyncer:              nil,
   522  			expectedEvaluation:          BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusDuplicate},
   523  		},
   524  		{
   525  			description:                 "Incompatible Kind",
   526  			givenBidder:                 "b",
   527  			normalisedBidderName:        "",
   528  			givenSyncersSeen:            map[string]struct{}{},
   529  			givenPrivacy:                fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   530  			givenCookie:                 cookieNeedsSync,
   531  			givenSyncTypeFilter:         syncTypeFilter,
   532  			normalizedBidderNamesLookup: normalizedBidderNamesLookup,
   533  			expectedSyncer:              nil,
   534  			expectedEvaluation:          BidderEvaluation{Bidder: "b", SyncerKey: "keyB", Status: StatusTypeNotSupported},
   535  		},
   536  		{
   537  			description:                 "Already Synced",
   538  			givenBidder:                 "a",
   539  			normalisedBidderName:        "",
   540  			givenSyncersSeen:            map[string]struct{}{},
   541  			givenPrivacy:                fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   542  			givenCookie:                 cookieAlreadyHasSyncForA,
   543  			givenSyncTypeFilter:         syncTypeFilter,
   544  			normalizedBidderNamesLookup: normalizedBidderNamesLookup,
   545  			expectedSyncer:              nil,
   546  			expectedEvaluation:          BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusAlreadySynced},
   547  		},
   548  		{
   549  			description:                 "Different Bidder Already Synced",
   550  			givenBidder:                 "a",
   551  			normalisedBidderName:        "a",
   552  			givenSyncersSeen:            map[string]struct{}{},
   553  			givenPrivacy:                fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   554  			givenCookie:                 cookieAlreadyHasSyncForB,
   555  			givenSyncTypeFilter:         syncTypeFilter,
   556  			normalizedBidderNamesLookup: normalizedBidderNamesLookup,
   557  			expectedSyncer:              fakeSyncerA,
   558  			expectedEvaluation:          BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusOK},
   559  		},
   560  		{
   561  			description:                 "Blocked By GDPR",
   562  			givenBidder:                 "a",
   563  			normalisedBidderName:        "a",
   564  			givenSyncersSeen:            map[string]struct{}{},
   565  			givenPrivacy:                fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: false, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   566  			givenCookie:                 cookieNeedsSync,
   567  			givenSyncTypeFilter:         syncTypeFilter,
   568  			normalizedBidderNamesLookup: normalizedBidderNamesLookup,
   569  			expectedSyncer:              nil,
   570  			expectedEvaluation:          BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusBlockedByPrivacy},
   571  		},
   572  		{
   573  			description:                 "Blocked By activity control",
   574  			givenBidder:                 "a",
   575  			normalisedBidderName:        "",
   576  			givenSyncersSeen:            map[string]struct{}{},
   577  			givenPrivacy:                fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: false},
   578  			givenCookie:                 cookieNeedsSync,
   579  			givenSyncTypeFilter:         syncTypeFilter,
   580  			normalizedBidderNamesLookup: normalizedBidderNamesLookup,
   581  			expectedSyncer:              nil,
   582  			expectedEvaluation:          BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusBlockedByPrivacy},
   583  		},
   584  		{
   585  			description:                 "Case insensitive bidder name",
   586  			givenBidder:                 "AppNexus",
   587  			normalisedBidderName:        "appnexus",
   588  			givenSyncersSeen:            map[string]struct{}{},
   589  			givenPrivacy:                fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   590  			givenCookie:                 cookieNeedsSync,
   591  			givenSyncTypeFilter:         syncTypeFilter,
   592  			normalizedBidderNamesLookup: openrtb_ext.NormalizeBidderName,
   593  			expectedSyncer:              fakeSyncerA,
   594  			expectedEvaluation:          BidderEvaluation{Bidder: "AppNexus", SyncerKey: "keyA", Status: StatusOK},
   595  		},
   596  		{
   597  			description:          "Case insensitivity check for sync type filter",
   598  			givenBidder:          "SeedingAlliance",
   599  			normalisedBidderName: "seedingAlliance",
   600  			givenSyncersSeen:     map[string]struct{}{},
   601  			givenPrivacy:         fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   602  			givenCookie:          cookieNeedsSync,
   603  			givenSyncTypeFilter: SyncTypeFilter{
   604  				IFrame:   NewSpecificBidderFilter([]string{"SeedingAlliance"}, BidderFilterModeInclude),
   605  				Redirect: NewSpecificBidderFilter([]string{"SeedingAlliance"}, BidderFilterModeExclude),
   606  			},
   607  			normalizedBidderNamesLookup: openrtb_ext.NormalizeBidderName,
   608  			expectedSyncer:              fakeSyncerA,
   609  			expectedEvaluation:          BidderEvaluation{Bidder: "SeedingAlliance", SyncerKey: "keyA", Status: StatusOK},
   610  		},
   611  		{
   612  			description:                 "Case Insensitivity Check For Blocked By GDPR",
   613  			givenBidder:                 "AppNexus",
   614  			normalisedBidderName:        "appnexus",
   615  			givenSyncersSeen:            map[string]struct{}{},
   616  			givenPrivacy:                fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: false, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   617  			givenCookie:                 cookieNeedsSync,
   618  			givenSyncTypeFilter:         syncTypeFilter,
   619  			normalizedBidderNamesLookup: openrtb_ext.NormalizeBidderName,
   620  			expectedSyncer:              nil,
   621  			expectedEvaluation:          BidderEvaluation{Bidder: "AppNexus", SyncerKey: "keyA", Status: StatusBlockedByPrivacy},
   622  		},
   623  		{
   624  			description:                 "Unconfigured Bidder",
   625  			givenBidder:                 "unconfigured",
   626  			normalizedBidderNamesLookup: normalizedBidderNamesLookup,
   627  			givenSyncersSeen:            map[string]struct{}{},
   628  			givenPrivacy:                fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   629  			givenCookie:                 cookieNeedsSync,
   630  			expectedSyncer:              nil,
   631  			expectedEvaluation:          BidderEvaluation{Bidder: "unconfigured", Status: StatusUnconfiguredBidder},
   632  		},
   633  		{
   634  			description:          "Disabled Usersync",
   635  			givenBidder:          "a",
   636  			normalisedBidderName: "a",
   637  			givenSyncersSeen:     map[string]struct{}{},
   638  			givenPrivacy:         fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   639  			givenCookie:          cookieNeedsSync,
   640  			givenBidderInfo: map[string]config.BidderInfo{
   641  				"a": {
   642  					Syncer: &config.Syncer{
   643  						Enabled: usersyncDisabled,
   644  					},
   645  				},
   646  			},
   647  			givenSyncTypeFilter:         syncTypeFilter,
   648  			normalizedBidderNamesLookup: normalizedBidderNamesLookup,
   649  			expectedSyncer:              nil,
   650  			expectedEvaluation:          BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusBlockedByDisabledUsersync},
   651  		},
   652  		{
   653  			description:      "Blocked By Regulation Scope - GDPR",
   654  			givenBidder:      "a",
   655  			givenSyncersSeen: map[string]struct{}{},
   656  			givenPrivacy:     fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true, gdprInScope: true},
   657  			givenCookie:      cookieNeedsSync,
   658  			givenBidderInfo: map[string]config.BidderInfo{
   659  				"a": {
   660  					Syncer: &config.Syncer{
   661  						SkipWhen: &config.SkipWhen{
   662  							GDPR: true,
   663  						},
   664  					},
   665  				},
   666  			},
   667  			givenSyncTypeFilter:         syncTypeFilter,
   668  			normalizedBidderNamesLookup: normalizedBidderNamesLookup,
   669  			normalisedBidderName:        "a",
   670  			expectedSyncer:              nil,
   671  			expectedEvaluation:          BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusBlockedByRegulationScope},
   672  		},
   673  		{
   674  			description:      "Blocked By Regulation Scope - GPP",
   675  			givenBidder:      "a",
   676  			givenSyncersSeen: map[string]struct{}{},
   677  			givenPrivacy:     fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   678  			givenCookie:      cookieNeedsSync,
   679  			givenBidderInfo: map[string]config.BidderInfo{
   680  				"a": {
   681  					Syncer: &config.Syncer{
   682  						SkipWhen: &config.SkipWhen{
   683  							GPPSID: []string{"2", "3"},
   684  						},
   685  					},
   686  				},
   687  			},
   688  			givenGPPSID:                 "2",
   689  			givenSyncTypeFilter:         syncTypeFilter,
   690  			normalizedBidderNamesLookup: normalizedBidderNamesLookup,
   691  			normalisedBidderName:        "a",
   692  			expectedSyncer:              nil,
   693  			expectedEvaluation:          BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusBlockedByRegulationScope},
   694  		},
   695  	}
   696  
   697  	for _, test := range testCases {
   698  		t.Run(test.description, func(t *testing.T) {
   699  			chooser, _ := NewChooser(bidderSyncerLookup, biddersKnown, test.givenBidderInfo).(standardChooser)
   700  			chooser.normalizeValidBidderName = test.normalizedBidderNamesLookup
   701  			sync, evaluation := chooser.evaluate(test.givenBidder, test.givenSyncersSeen, test.givenSyncTypeFilter, &test.givenPrivacy, &test.givenCookie, test.givenGPPSID)
   702  
   703  			assert.Equal(t, test.normalisedBidderName, test.givenPrivacy.inputBidderName)
   704  			assert.Equal(t, test.expectedSyncer, sync, test.description+":syncer")
   705  			assert.Equal(t, test.expectedEvaluation, evaluation, test.description+":evaluation")
   706  		})
   707  	}
   708  }
   709  
   710  type mockBidderChooser struct {
   711  	mock.Mock
   712  }
   713  
   714  func (m *mockBidderChooser) choose(requested, available []string, cooperative Cooperative) []string {
   715  	args := m.Called(requested, available, cooperative)
   716  	return args.Get(0).([]string)
   717  }
   718  
   719  type fakeSyncer struct {
   720  	key              string
   721  	supportsIFrame   bool
   722  	supportsRedirect bool
   723  	formatOverride   string
   724  }
   725  
   726  func (s fakeSyncer) Key() string {
   727  	return s.key
   728  }
   729  
   730  func (s fakeSyncer) DefaultResponseFormat() SyncType {
   731  	return SyncTypeIFrame
   732  }
   733  
   734  func (s fakeSyncer) SupportsType(syncTypes []SyncType) bool {
   735  	for _, syncType := range syncTypes {
   736  		if syncType == SyncTypeIFrame && s.supportsIFrame {
   737  			return true
   738  		}
   739  		if syncType == SyncTypeRedirect && s.supportsRedirect {
   740  			return true
   741  		}
   742  	}
   743  	return false
   744  }
   745  
   746  func (fakeSyncer) GetSync([]SyncType, macros.UserSyncPrivacy) (Sync, error) {
   747  	return Sync{}, nil
   748  }
   749  
   750  type fakePrivacy struct {
   751  	gdprAllowsHostCookie  bool
   752  	gdprAllowsBidderSync  bool
   753  	ccpaAllowsBidderSync  bool
   754  	activityAllowUserSync bool
   755  	gdprInScope           bool
   756  	inputBidderName       string
   757  }
   758  
   759  func (p *fakePrivacy) GDPRAllowsHostCookie() bool {
   760  	return p.gdprAllowsHostCookie
   761  }
   762  
   763  func (p *fakePrivacy) GDPRAllowsBidderSync(bidder string) bool {
   764  	p.inputBidderName = bidder
   765  	return p.gdprAllowsBidderSync
   766  }
   767  
   768  func (p *fakePrivacy) CCPAAllowsBidderSync(bidder string) bool {
   769  	p.inputBidderName = bidder
   770  	return p.ccpaAllowsBidderSync
   771  }
   772  
   773  func (p *fakePrivacy) ActivityAllowsUserSync(bidder string) bool {
   774  	return p.activityAllowUserSync
   775  }
   776  
   777  func (p *fakePrivacy) GDPRInScope() bool {
   778  	return p.gdprInScope
   779  }