github.com/prebid/prebid-server@v0.275.0/usersync/chooser_test.go (about)

     1  package usersync
     2  
     3  import (
     4  	"testing"
     5  	"time"
     6  
     7  	"github.com/prebid/prebid-server/macros"
     8  	"github.com/stretchr/testify/assert"
     9  	"github.com/stretchr/testify/mock"
    10  )
    11  
    12  func TestNewChooser(t *testing.T) {
    13  	testCases := []struct {
    14  		description              string
    15  		bidderSyncerLookup       map[string]Syncer
    16  		expectedBiddersAvailable []string
    17  	}{
    18  		{
    19  			description:              "Nil",
    20  			bidderSyncerLookup:       nil,
    21  			expectedBiddersAvailable: []string{},
    22  		},
    23  		{
    24  			description:              "Empty",
    25  			bidderSyncerLookup:       map[string]Syncer{},
    26  			expectedBiddersAvailable: []string{},
    27  		},
    28  		{
    29  			description:              "One",
    30  			bidderSyncerLookup:       map[string]Syncer{"a": fakeSyncer{}},
    31  			expectedBiddersAvailable: []string{"a"},
    32  		},
    33  		{
    34  			description:              "Many",
    35  			bidderSyncerLookup:       map[string]Syncer{"a": fakeSyncer{}, "b": fakeSyncer{}},
    36  			expectedBiddersAvailable: []string{"a", "b"},
    37  		},
    38  	}
    39  
    40  	for _, test := range testCases {
    41  		chooser, _ := NewChooser(test.bidderSyncerLookup).(standardChooser)
    42  		assert.ElementsMatch(t, test.expectedBiddersAvailable, chooser.biddersAvailable, test.description)
    43  	}
    44  }
    45  
    46  func TestChooserChoose(t *testing.T) {
    47  	fakeSyncerA := fakeSyncer{key: "keyA", supportsIFrame: true}
    48  	fakeSyncerB := fakeSyncer{key: "keyB", supportsIFrame: true}
    49  	fakeSyncerC := fakeSyncer{key: "keyC", supportsIFrame: false}
    50  	bidderSyncerLookup := map[string]Syncer{"a": fakeSyncerA, "b": fakeSyncerB, "c": fakeSyncerC}
    51  	syncerChoiceA := SyncerChoice{Bidder: "a", Syncer: fakeSyncerA}
    52  	syncerChoiceB := SyncerChoice{Bidder: "b", Syncer: fakeSyncerB}
    53  	syncTypeFilter := SyncTypeFilter{
    54  		IFrame:   NewUniformBidderFilter(BidderFilterModeInclude),
    55  		Redirect: NewUniformBidderFilter(BidderFilterModeExclude)}
    56  
    57  	cooperativeConfig := Cooperative{Enabled: true}
    58  
    59  	testCases := []struct {
    60  		description        string
    61  		givenRequest       Request
    62  		givenChosenBidders []string
    63  		givenCookie        Cookie
    64  		expected           Result
    65  	}{
    66  		{
    67  			description: "Cookie Opt Out",
    68  			givenRequest: Request{
    69  				Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
    70  				Limit:   0,
    71  			},
    72  			givenChosenBidders: []string{"a"},
    73  			givenCookie:        Cookie{optOut: true},
    74  			expected: Result{
    75  				Status:           StatusBlockedByUserOptOut,
    76  				BiddersEvaluated: nil,
    77  				SyncersChosen:    nil,
    78  			},
    79  		},
    80  		{
    81  			description: "GDPR Host Cookie Not Allowed",
    82  			givenRequest: Request{
    83  				Privacy: fakePrivacy{gdprAllowsHostCookie: false, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
    84  				Limit:   0,
    85  			},
    86  			givenChosenBidders: []string{"a"},
    87  			givenCookie:        Cookie{},
    88  			expected: Result{
    89  				Status:           StatusBlockedByGDPR,
    90  				BiddersEvaluated: nil,
    91  				SyncersChosen:    nil,
    92  			},
    93  		},
    94  		{
    95  			description: "No Bidders",
    96  			givenRequest: Request{
    97  				Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
    98  				Limit:   0,
    99  			},
   100  			givenChosenBidders: []string{},
   101  			givenCookie:        Cookie{},
   102  			expected: Result{
   103  				Status:           StatusOK,
   104  				BiddersEvaluated: []BidderEvaluation{},
   105  				SyncersChosen:    []SyncerChoice{},
   106  			},
   107  		},
   108  		{
   109  			description: "One Bidder - Sync",
   110  			givenRequest: Request{
   111  				Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   112  				Limit:   0,
   113  			},
   114  			givenChosenBidders: []string{"a"},
   115  			givenCookie:        Cookie{},
   116  			expected: Result{
   117  				Status:           StatusOK,
   118  				BiddersEvaluated: []BidderEvaluation{{Bidder: "a", SyncerKey: "keyA", Status: StatusOK}},
   119  				SyncersChosen:    []SyncerChoice{syncerChoiceA},
   120  			},
   121  		},
   122  		{
   123  			description: "One Bidder - No Sync",
   124  			givenRequest: Request{
   125  				Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   126  				Limit:   0,
   127  			},
   128  			givenChosenBidders: []string{"c"},
   129  			givenCookie:        Cookie{},
   130  			expected: Result{
   131  				Status:           StatusOK,
   132  				BiddersEvaluated: []BidderEvaluation{{Bidder: "c", SyncerKey: "keyC", Status: StatusTypeNotSupported}},
   133  				SyncersChosen:    []SyncerChoice{},
   134  			},
   135  		},
   136  		{
   137  			description: "Many Bidders - All Sync - Limit Disabled With 0",
   138  			givenRequest: Request{
   139  				Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   140  				Limit:   0,
   141  			},
   142  			givenChosenBidders: []string{"a", "b"},
   143  			givenCookie:        Cookie{},
   144  			expected: Result{
   145  				Status:           StatusOK,
   146  				BiddersEvaluated: []BidderEvaluation{{Bidder: "a", SyncerKey: "keyA", Status: StatusOK}, {Bidder: "b", SyncerKey: "keyB", Status: StatusOK}},
   147  				SyncersChosen:    []SyncerChoice{syncerChoiceA, syncerChoiceB},
   148  			},
   149  		},
   150  		{
   151  			description: "Many Bidders - All Sync - Limit Disabled With Negative Value",
   152  			givenRequest: Request{
   153  				Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   154  				Limit:   -1,
   155  			},
   156  			givenChosenBidders: []string{"a", "b"},
   157  			givenCookie:        Cookie{},
   158  			expected: Result{
   159  				Status:           StatusOK,
   160  				BiddersEvaluated: []BidderEvaluation{{Bidder: "a", SyncerKey: "keyA", Status: StatusOK}, {Bidder: "b", SyncerKey: "keyB", Status: StatusOK}},
   161  				SyncersChosen:    []SyncerChoice{syncerChoiceA, syncerChoiceB},
   162  			},
   163  		},
   164  		{
   165  			description: "Many Bidders - Limited Sync",
   166  			givenRequest: Request{
   167  				Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   168  				Limit:   1,
   169  			},
   170  			givenChosenBidders: []string{"a", "b"},
   171  			givenCookie:        Cookie{},
   172  			expected: Result{
   173  				Status:           StatusOK,
   174  				BiddersEvaluated: []BidderEvaluation{{Bidder: "a", SyncerKey: "keyA", Status: StatusOK}},
   175  				SyncersChosen:    []SyncerChoice{syncerChoiceA},
   176  			},
   177  		},
   178  		{
   179  			description: "Many Bidders - Limited Sync - Disqualified Syncers Don't Count Towards Limit",
   180  			givenRequest: Request{
   181  				Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   182  				Limit:   1,
   183  			},
   184  			givenChosenBidders: []string{"c", "a", "b"},
   185  			givenCookie:        Cookie{},
   186  			expected: Result{
   187  				Status:           StatusOK,
   188  				BiddersEvaluated: []BidderEvaluation{{Bidder: "c", SyncerKey: "keyC", Status: StatusTypeNotSupported}, {Bidder: "a", SyncerKey: "keyA", Status: StatusOK}},
   189  				SyncersChosen:    []SyncerChoice{syncerChoiceA},
   190  			},
   191  		},
   192  		{
   193  			description: "Many Bidders - Some Sync, Some Don't",
   194  			givenRequest: Request{
   195  				Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   196  				Limit:   0,
   197  			},
   198  			givenChosenBidders: []string{"a", "c"},
   199  			givenCookie:        Cookie{},
   200  			expected: Result{
   201  				Status:           StatusOK,
   202  				BiddersEvaluated: []BidderEvaluation{{Bidder: "a", SyncerKey: "keyA", Status: StatusOK}, {Bidder: "c", SyncerKey: "keyC", Status: StatusTypeNotSupported}},
   203  				SyncersChosen:    []SyncerChoice{syncerChoiceA},
   204  			},
   205  		},
   206  	}
   207  
   208  	bidders := []string{"anyRequested"}
   209  	biddersAvailable := []string{"anyAvailable"}
   210  	for _, test := range testCases {
   211  		// set request values which don't need to be specified for each test case
   212  		test.givenRequest.Bidders = bidders
   213  		test.givenRequest.SyncTypeFilter = syncTypeFilter
   214  		test.givenRequest.Cooperative = cooperativeConfig
   215  
   216  		mockBidderChooser := &mockBidderChooser{}
   217  		mockBidderChooser.
   218  			On("choose", test.givenRequest.Bidders, biddersAvailable, cooperativeConfig).
   219  			Return(test.givenChosenBidders)
   220  
   221  		chooser := standardChooser{
   222  			bidderSyncerLookup: bidderSyncerLookup,
   223  			biddersAvailable:   biddersAvailable,
   224  			bidderChooser:      mockBidderChooser,
   225  		}
   226  
   227  		result := chooser.Choose(test.givenRequest, &test.givenCookie)
   228  		assert.Equal(t, test.expected, result, test.description)
   229  	}
   230  }
   231  
   232  func TestChooserEvaluate(t *testing.T) {
   233  	fakeSyncerA := fakeSyncer{key: "keyA", supportsIFrame: true}
   234  	fakeSyncerB := fakeSyncer{key: "keyB", supportsIFrame: false}
   235  	bidderSyncerLookup := map[string]Syncer{"a": fakeSyncerA, "b": fakeSyncerB}
   236  	syncTypeFilter := SyncTypeFilter{
   237  		IFrame:   NewUniformBidderFilter(BidderFilterModeInclude),
   238  		Redirect: NewUniformBidderFilter(BidderFilterModeExclude)}
   239  
   240  	cookieNeedsSync := Cookie{}
   241  	cookieAlreadyHasSyncForA := Cookie{uids: map[string]UIDEntry{"keyA": {Expires: time.Now().Add(time.Duration(24) * time.Hour)}}}
   242  	cookieAlreadyHasSyncForB := Cookie{uids: map[string]UIDEntry{"keyB": {Expires: time.Now().Add(time.Duration(24) * time.Hour)}}}
   243  
   244  	testCases := []struct {
   245  		description        string
   246  		givenBidder        string
   247  		givenSyncersSeen   map[string]struct{}
   248  		givenPrivacy       Privacy
   249  		givenCookie        Cookie
   250  		expectedSyncer     Syncer
   251  		expectedEvaluation BidderEvaluation
   252  	}{
   253  		{
   254  			description:        "Valid",
   255  			givenBidder:        "a",
   256  			givenSyncersSeen:   map[string]struct{}{},
   257  			givenPrivacy:       fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   258  			givenCookie:        cookieNeedsSync,
   259  			expectedSyncer:     fakeSyncerA,
   260  			expectedEvaluation: BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusOK},
   261  		},
   262  		{
   263  			description:        "Unknown Bidder",
   264  			givenBidder:        "unknown",
   265  			givenSyncersSeen:   map[string]struct{}{},
   266  			givenPrivacy:       fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   267  			givenCookie:        cookieNeedsSync,
   268  			expectedSyncer:     nil,
   269  			expectedEvaluation: BidderEvaluation{Bidder: "unknown", Status: StatusUnknownBidder},
   270  		},
   271  		{
   272  			description:        "Duplicate Syncer",
   273  			givenBidder:        "a",
   274  			givenSyncersSeen:   map[string]struct{}{"keyA": {}},
   275  			givenPrivacy:       fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   276  			givenCookie:        cookieNeedsSync,
   277  			expectedSyncer:     nil,
   278  			expectedEvaluation: BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusDuplicate},
   279  		},
   280  		{
   281  			description:        "Incompatible Kind",
   282  			givenBidder:        "b",
   283  			givenSyncersSeen:   map[string]struct{}{},
   284  			givenPrivacy:       fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   285  			givenCookie:        cookieNeedsSync,
   286  			expectedSyncer:     nil,
   287  			expectedEvaluation: BidderEvaluation{Bidder: "b", SyncerKey: "keyB", Status: StatusTypeNotSupported},
   288  		},
   289  		{
   290  			description:        "Already Synced",
   291  			givenBidder:        "a",
   292  			givenSyncersSeen:   map[string]struct{}{},
   293  			givenPrivacy:       fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   294  			givenCookie:        cookieAlreadyHasSyncForA,
   295  			expectedSyncer:     nil,
   296  			expectedEvaluation: BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusAlreadySynced},
   297  		},
   298  		{
   299  			description:        "Different Bidder Already Synced",
   300  			givenBidder:        "a",
   301  			givenSyncersSeen:   map[string]struct{}{},
   302  			givenPrivacy:       fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   303  			givenCookie:        cookieAlreadyHasSyncForB,
   304  			expectedSyncer:     fakeSyncerA,
   305  			expectedEvaluation: BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusOK},
   306  		},
   307  		{
   308  			description:        "Blocked By GDPR",
   309  			givenBidder:        "a",
   310  			givenSyncersSeen:   map[string]struct{}{},
   311  			givenPrivacy:       fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: false, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
   312  			givenCookie:        cookieNeedsSync,
   313  			expectedSyncer:     nil,
   314  			expectedEvaluation: BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusBlockedByGDPR},
   315  		},
   316  		{
   317  			description:        "Blocked By CCPA",
   318  			givenBidder:        "a",
   319  			givenSyncersSeen:   map[string]struct{}{},
   320  			givenPrivacy:       fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: false, activityAllowUserSync: true},
   321  			givenCookie:        cookieNeedsSync,
   322  			expectedSyncer:     nil,
   323  			expectedEvaluation: BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusBlockedByCCPA},
   324  		},
   325  		{
   326  			description:        "Blocked By activity control",
   327  			givenBidder:        "a",
   328  			givenSyncersSeen:   map[string]struct{}{},
   329  			givenPrivacy:       fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: false},
   330  			givenCookie:        cookieNeedsSync,
   331  			expectedSyncer:     nil,
   332  			expectedEvaluation: BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusBlockedByPrivacy},
   333  		},
   334  	}
   335  
   336  	for _, test := range testCases {
   337  		chooser, _ := NewChooser(bidderSyncerLookup).(standardChooser)
   338  		sync, evaluation := chooser.evaluate(test.givenBidder, test.givenSyncersSeen, syncTypeFilter, test.givenPrivacy, &test.givenCookie)
   339  
   340  		assert.Equal(t, test.expectedSyncer, sync, test.description+":syncer")
   341  		assert.Equal(t, test.expectedEvaluation, evaluation, test.description+":evaluation")
   342  	}
   343  }
   344  
   345  type mockBidderChooser struct {
   346  	mock.Mock
   347  }
   348  
   349  func (m *mockBidderChooser) choose(requested, available []string, cooperative Cooperative) []string {
   350  	args := m.Called(requested, available, cooperative)
   351  	return args.Get(0).([]string)
   352  }
   353  
   354  type fakeSyncer struct {
   355  	key              string
   356  	supportsIFrame   bool
   357  	supportsRedirect bool
   358  }
   359  
   360  func (s fakeSyncer) Key() string {
   361  	return s.key
   362  }
   363  
   364  func (s fakeSyncer) DefaultSyncType() SyncType {
   365  	return SyncTypeIFrame
   366  }
   367  
   368  func (s fakeSyncer) SupportsType(syncTypes []SyncType) bool {
   369  	for _, syncType := range syncTypes {
   370  		if syncType == SyncTypeIFrame && s.supportsIFrame {
   371  			return true
   372  		}
   373  		if syncType == SyncTypeRedirect && s.supportsRedirect {
   374  			return true
   375  		}
   376  	}
   377  	return false
   378  }
   379  
   380  func (fakeSyncer) GetSync([]SyncType, macros.UserSyncPrivacy) (Sync, error) {
   381  	return Sync{}, nil
   382  }
   383  
   384  type fakePrivacy struct {
   385  	gdprAllowsHostCookie  bool
   386  	gdprAllowsBidderSync  bool
   387  	ccpaAllowsBidderSync  bool
   388  	activityAllowUserSync bool
   389  }
   390  
   391  func (p fakePrivacy) GDPRAllowsHostCookie() bool {
   392  	return p.gdprAllowsHostCookie
   393  }
   394  
   395  func (p fakePrivacy) GDPRAllowsBidderSync(bidder string) bool {
   396  	return p.gdprAllowsBidderSync
   397  }
   398  
   399  func (p fakePrivacy) CCPAAllowsBidderSync(bidder string) bool {
   400  	return p.ccpaAllowsBidderSync
   401  }
   402  
   403  func (p fakePrivacy) ActivityAllowsUserSync(bidder string) bool {
   404  	return p.activityAllowUserSync
   405  }