github.com/prebid/prebid-server/v2@v2.18.0/endpoints/cookie_sync_test.go (about)

     1  package endpoints
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"net/http/httptest"
    11  	"strconv"
    12  	"strings"
    13  	"testing"
    14  	"testing/iotest"
    15  	"time"
    16  
    17  	"github.com/prebid/prebid-server/v2/analytics"
    18  	"github.com/prebid/prebid-server/v2/config"
    19  	"github.com/prebid/prebid-server/v2/errortypes"
    20  	"github.com/prebid/prebid-server/v2/gdpr"
    21  	"github.com/prebid/prebid-server/v2/macros"
    22  	"github.com/prebid/prebid-server/v2/metrics"
    23  	"github.com/prebid/prebid-server/v2/openrtb_ext"
    24  	"github.com/prebid/prebid-server/v2/privacy"
    25  	"github.com/prebid/prebid-server/v2/privacy/ccpa"
    26  	"github.com/prebid/prebid-server/v2/usersync"
    27  	"github.com/prebid/prebid-server/v2/util/ptrutil"
    28  	"github.com/stretchr/testify/assert"
    29  	"github.com/stretchr/testify/mock"
    30  )
    31  
    32  // fakeTime implements the Time interface
    33  type fakeTime struct {
    34  	time time.Time
    35  }
    36  
    37  func (ft *fakeTime) Now() time.Time {
    38  	return ft.time
    39  }
    40  
    41  func TestNewCookieSyncEndpoint(t *testing.T) {
    42  	var (
    43  		syncersByBidder  = map[string]usersync.Syncer{"a": &MockSyncer{}}
    44  		gdprPermsBuilder = fakePermissionsBuilder{
    45  			permissions: &fakePermissions{},
    46  		}.Builder
    47  		tcf2ConfigBuilder = fakeTCF2ConfigBuilder{
    48  			cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
    49  		}.Builder
    50  		configUserSync    = config.UserSync{Cooperative: config.UserSyncCooperative{EnabledByDefault: true}}
    51  		configHostCookie  = config.HostCookie{Family: "foo"}
    52  		configGDPR        = config.GDPR{HostVendorID: 42}
    53  		configCCPAEnforce = true
    54  		metrics           = metrics.MetricsEngineMock{}
    55  		analytics         = MockAnalyticsRunner{}
    56  		fetcher           = FakeAccountsFetcher{}
    57  		bidders           = map[string]openrtb_ext.BidderName{"bidderA": openrtb_ext.BidderName("bidderA"), "bidderB": openrtb_ext.BidderName("bidderB")}
    58  		bidderInfo        = map[string]config.BidderInfo{"bidderA": {}, "bidderB": {}}
    59  		biddersKnown      = map[string]struct{}{"bidderA": {}, "bidderB": {}}
    60  	)
    61  
    62  	endpoint := NewCookieSyncEndpoint(
    63  		syncersByBidder,
    64  		&config.Configuration{
    65  			UserSync:    configUserSync,
    66  			HostCookie:  configHostCookie,
    67  			GDPR:        configGDPR,
    68  			CCPA:        config.CCPA{Enforce: configCCPAEnforce},
    69  			BidderInfos: bidderInfo,
    70  		},
    71  		gdprPermsBuilder,
    72  		tcf2ConfigBuilder,
    73  		&metrics,
    74  		&analytics,
    75  		&fetcher,
    76  		bidders,
    77  	)
    78  	result := endpoint.(*cookieSyncEndpoint)
    79  
    80  	expected := &cookieSyncEndpoint{
    81  		chooser: usersync.NewChooser(syncersByBidder, biddersKnown, bidderInfo),
    82  		config: &config.Configuration{
    83  			UserSync:    configUserSync,
    84  			HostCookie:  configHostCookie,
    85  			GDPR:        configGDPR,
    86  			CCPA:        config.CCPA{Enforce: configCCPAEnforce},
    87  			BidderInfos: bidderInfo,
    88  		},
    89  		privacyConfig: usersyncPrivacyConfig{
    90  			gdprConfig:             configGDPR,
    91  			gdprPermissionsBuilder: gdprPermsBuilder,
    92  			tcf2ConfigBuilder:      tcf2ConfigBuilder,
    93  			ccpaEnforce:            configCCPAEnforce,
    94  			bidderHashSet:          map[string]struct{}{"bidderA": {}, "bidderB": {}},
    95  		},
    96  		metrics:         &metrics,
    97  		pbsAnalytics:    &analytics,
    98  		accountsFetcher: &fetcher,
    99  	}
   100  
   101  	assert.IsType(t, &cookieSyncEndpoint{}, endpoint)
   102  
   103  	assert.Equal(t, expected.config, result.config)
   104  	assert.ObjectsAreEqualValues(expected.chooser, result.chooser)
   105  	assert.Equal(t, expected.metrics, result.metrics)
   106  	assert.Equal(t, expected.pbsAnalytics, result.pbsAnalytics)
   107  	assert.Equal(t, expected.accountsFetcher, result.accountsFetcher)
   108  
   109  	assert.Equal(t, expected.privacyConfig.gdprConfig, result.privacyConfig.gdprConfig)
   110  	assert.Equal(t, expected.privacyConfig.ccpaEnforce, result.privacyConfig.ccpaEnforce)
   111  	assert.Equal(t, expected.privacyConfig.bidderHashSet, result.privacyConfig.bidderHashSet)
   112  }
   113  
   114  func TestCookieSyncHandle(t *testing.T) {
   115  	syncTypeExpected := []usersync.SyncType{usersync.SyncTypeIFrame, usersync.SyncTypeRedirect}
   116  	sync := usersync.Sync{URL: "aURL", Type: usersync.SyncTypeRedirect, SupportCORS: true}
   117  	syncer := MockSyncer{}
   118  	syncer.On("GetSync", syncTypeExpected, macros.UserSyncPrivacy{}).Return(sync, nil).Maybe()
   119  
   120  	cookieWithSyncs := usersync.NewCookie()
   121  	cookieWithSyncs.Sync("foo", "anyID")
   122  
   123  	testCases := []struct {
   124  		description                     string
   125  		givenCookie                     *usersync.Cookie
   126  		givenBody                       io.Reader
   127  		givenChooserResult              usersync.Result
   128  		givenAccountData                map[string]json.RawMessage
   129  		expectedStatusCode              int
   130  		expectedBody                    string
   131  		setMetricsExpectations          func(*metrics.MetricsEngineMock)
   132  		setAnalyticsExpectations        func(*MockAnalyticsRunner)
   133  		expectedCookieDeprecationHeader bool
   134  	}{
   135  		{
   136  			description: "Request With Cookie",
   137  			givenCookie: cookieWithSyncs,
   138  			givenBody:   strings.NewReader(`{}`),
   139  			givenChooserResult: usersync.Result{
   140  				Status:           usersync.StatusOK,
   141  				BiddersEvaluated: []usersync.BidderEvaluation{{Bidder: "a", SyncerKey: "aSyncer", Status: usersync.StatusOK}},
   142  				SyncersChosen:    []usersync.SyncerChoice{{Bidder: "a", Syncer: &syncer}},
   143  			},
   144  			expectedStatusCode: 200,
   145  			expectedBody: `{"status":"ok","bidder_status":[` +
   146  				`{"bidder":"a","no_cookie":true,"usersync":{"url":"aURL","type":"redirect","supportCORS":true}}` +
   147  				`]}` + "\n",
   148  			setMetricsExpectations: func(m *metrics.MetricsEngineMock) {
   149  				m.On("RecordCookieSync", metrics.CookieSyncOK).Once()
   150  				m.On("RecordSyncerRequest", "aSyncer", metrics.SyncerCookieSyncOK).Once()
   151  			},
   152  			setAnalyticsExpectations: func(a *MockAnalyticsRunner) {
   153  				expected := analytics.CookieSyncObject{
   154  					Status: 200,
   155  					Errors: nil,
   156  					BidderStatus: []*analytics.CookieSyncBidder{
   157  						{
   158  							BidderCode:   "a",
   159  							NoCookie:     true,
   160  							UsersyncInfo: &analytics.UsersyncInfo{URL: "aURL", Type: "redirect", SupportCORS: true},
   161  						},
   162  					},
   163  				}
   164  				a.On("LogCookieSyncObject", &expected).Once()
   165  			},
   166  		},
   167  		{
   168  			description: "Request Without Cookie",
   169  			givenCookie: nil,
   170  			givenBody:   strings.NewReader(`{}`),
   171  			givenChooserResult: usersync.Result{
   172  				Status:           usersync.StatusOK,
   173  				BiddersEvaluated: []usersync.BidderEvaluation{{Bidder: "a", SyncerKey: "aSyncer", Status: usersync.StatusOK}},
   174  				SyncersChosen:    []usersync.SyncerChoice{{Bidder: "a", Syncer: &syncer}},
   175  			},
   176  			expectedStatusCode: 200,
   177  			expectedBody: `{"status":"no_cookie","bidder_status":[` +
   178  				`{"bidder":"a","no_cookie":true,"usersync":{"url":"aURL","type":"redirect","supportCORS":true}}` +
   179  				`]}` + "\n",
   180  			setMetricsExpectations: func(m *metrics.MetricsEngineMock) {
   181  				m.On("RecordCookieSync", metrics.CookieSyncOK).Once()
   182  				m.On("RecordSyncerRequest", "aSyncer", metrics.SyncerCookieSyncOK).Once()
   183  			},
   184  			setAnalyticsExpectations: func(a *MockAnalyticsRunner) {
   185  				expected := analytics.CookieSyncObject{
   186  					Status: 200,
   187  					Errors: nil,
   188  					BidderStatus: []*analytics.CookieSyncBidder{
   189  						{
   190  							BidderCode:   "a",
   191  							NoCookie:     true,
   192  							UsersyncInfo: &analytics.UsersyncInfo{URL: "aURL", Type: "redirect", SupportCORS: true},
   193  						},
   194  					},
   195  				}
   196  				a.On("LogCookieSyncObject", &expected).Once()
   197  			},
   198  		},
   199  		{
   200  			description: "Malformed Request",
   201  			givenCookie: cookieWithSyncs,
   202  			givenBody:   strings.NewReader(`malformed`),
   203  			givenChooserResult: usersync.Result{
   204  				Status:           usersync.StatusOK,
   205  				BiddersEvaluated: []usersync.BidderEvaluation{{Bidder: "a", SyncerKey: "aSyncer", Status: usersync.StatusOK}},
   206  				SyncersChosen:    []usersync.SyncerChoice{{Bidder: "a", Syncer: &syncer}},
   207  			},
   208  			expectedStatusCode: 400,
   209  			expectedBody:       `JSON parsing failed: expect { or n, but found m` + "\n",
   210  			setMetricsExpectations: func(m *metrics.MetricsEngineMock) {
   211  				m.On("RecordCookieSync", metrics.CookieSyncBadRequest).Once()
   212  			},
   213  			setAnalyticsExpectations: func(a *MockAnalyticsRunner) {
   214  				expected := analytics.CookieSyncObject{
   215  					Status:       400,
   216  					Errors:       []error{errors.New("JSON parsing failed: expect { or n, but found m")},
   217  					BidderStatus: []*analytics.CookieSyncBidder{},
   218  				}
   219  				a.On("LogCookieSyncObject", &expected).Once()
   220  			},
   221  		},
   222  		{
   223  			description: "Request Blocked By Opt Out",
   224  			givenCookie: cookieWithSyncs,
   225  			givenBody:   strings.NewReader(`{}`),
   226  			givenChooserResult: usersync.Result{
   227  				Status:           usersync.StatusBlockedByUserOptOut,
   228  				BiddersEvaluated: []usersync.BidderEvaluation{{Bidder: "a", SyncerKey: "aSyncer", Status: usersync.StatusOK}},
   229  				SyncersChosen:    []usersync.SyncerChoice{{Bidder: "a", Syncer: &syncer}},
   230  			},
   231  			expectedStatusCode: 401,
   232  			expectedBody:       `User has opted out` + "\n",
   233  			setMetricsExpectations: func(m *metrics.MetricsEngineMock) {
   234  				m.On("RecordCookieSync", metrics.CookieSyncOptOut).Once()
   235  			},
   236  			setAnalyticsExpectations: func(a *MockAnalyticsRunner) {
   237  				expected := analytics.CookieSyncObject{
   238  					Status:       401,
   239  					Errors:       []error{errors.New("User has opted out")},
   240  					BidderStatus: []*analytics.CookieSyncBidder{},
   241  				}
   242  				a.On("LogCookieSyncObject", &expected).Once()
   243  			},
   244  		},
   245  		{
   246  			description: "Request Blocked By GDPR Host Cookie Restriction",
   247  			givenCookie: cookieWithSyncs,
   248  			givenBody:   strings.NewReader(`{}`),
   249  			givenChooserResult: usersync.Result{
   250  				Status:           usersync.StatusBlockedByPrivacy,
   251  				BiddersEvaluated: []usersync.BidderEvaluation{{Bidder: "a", SyncerKey: "aSyncer", Status: usersync.StatusOK}},
   252  				SyncersChosen:    []usersync.SyncerChoice{{Bidder: "a", Syncer: &syncer}},
   253  			},
   254  			expectedStatusCode: 200,
   255  			expectedBody:       `{"status":"ok","bidder_status":[]}` + "\n",
   256  			setMetricsExpectations: func(m *metrics.MetricsEngineMock) {
   257  				m.On("RecordCookieSync", metrics.CookieSyncGDPRHostCookieBlocked).Once()
   258  			},
   259  			setAnalyticsExpectations: func(a *MockAnalyticsRunner) {
   260  				expected := analytics.CookieSyncObject{
   261  					Status:       200,
   262  					Errors:       nil,
   263  					BidderStatus: []*analytics.CookieSyncBidder{},
   264  				}
   265  				a.On("LogCookieSyncObject", &expected).Once()
   266  			},
   267  		},
   268  		{
   269  			description: "Debug Check",
   270  			givenCookie: cookieWithSyncs,
   271  			givenBody:   strings.NewReader(`{"debug": true}`),
   272  			givenChooserResult: usersync.Result{
   273  				Status:           usersync.StatusOK,
   274  				BiddersEvaluated: []usersync.BidderEvaluation{{Bidder: "a", SyncerKey: "aSyncer", Status: usersync.StatusAlreadySynced}},
   275  				SyncersChosen:    []usersync.SyncerChoice{{Bidder: "a", Syncer: &syncer}},
   276  			},
   277  			expectedStatusCode: 200,
   278  			expectedBody: `{"status":"ok","bidder_status":[` +
   279  				`{"bidder":"a","no_cookie":true,"usersync":{"url":"aURL","type":"redirect","supportCORS":true}}` +
   280  				`],"debug":[{"bidder":"a","error":"Already in sync"}]}` + "\n",
   281  			setMetricsExpectations: func(m *metrics.MetricsEngineMock) {
   282  				m.On("RecordCookieSync", metrics.CookieSyncOK).Once()
   283  				m.On("RecordSyncerRequest", "aSyncer", metrics.SyncerCookieSyncAlreadySynced).Once()
   284  			},
   285  			setAnalyticsExpectations: func(a *MockAnalyticsRunner) {
   286  				expected := analytics.CookieSyncObject{
   287  					Status: 200,
   288  					Errors: nil,
   289  					BidderStatus: []*analytics.CookieSyncBidder{
   290  						{
   291  							BidderCode:   "a",
   292  							NoCookie:     true,
   293  							UsersyncInfo: &analytics.UsersyncInfo{URL: "aURL", Type: "redirect", SupportCORS: true},
   294  						},
   295  					},
   296  				}
   297  				a.On("LogCookieSyncObject", &expected).Once()
   298  			},
   299  		},
   300  		{
   301  			description: "CookieDeprecation-Set",
   302  			givenCookie: cookieWithSyncs,
   303  			givenBody:   strings.NewReader(`{"account": "testAccount"}`),
   304  			givenChooserResult: usersync.Result{
   305  				Status:           usersync.StatusOK,
   306  				BiddersEvaluated: []usersync.BidderEvaluation{{Bidder: "a", SyncerKey: "aSyncer", Status: usersync.StatusAlreadySynced}},
   307  				SyncersChosen:    []usersync.SyncerChoice{{Bidder: "a", Syncer: &syncer}},
   308  			},
   309  			givenAccountData: map[string]json.RawMessage{
   310  				"testAccount": json.RawMessage(`{"id":"1","privacy":{"privacysandbox":{"cookiedeprecation":{"enabled":true,"ttlsec":86400}}}}`),
   311  			},
   312  			expectedStatusCode:              200,
   313  			expectedCookieDeprecationHeader: true,
   314  			expectedBody: `{"status":"ok","bidder_status":[` +
   315  				`{"bidder":"a","no_cookie":true,"usersync":{"url":"aURL","type":"redirect","supportCORS":true}}` +
   316  				`]}` + "\n",
   317  			setMetricsExpectations: func(m *metrics.MetricsEngineMock) {
   318  				m.On("RecordCookieSync", metrics.CookieSyncOK).Once()
   319  				m.On("RecordSyncerRequest", "aSyncer", metrics.SyncerCookieSyncAlreadySynced).Once()
   320  			},
   321  			setAnalyticsExpectations: func(a *MockAnalyticsRunner) {
   322  				expected := analytics.CookieSyncObject{
   323  					Status: 200,
   324  					Errors: nil,
   325  					BidderStatus: []*analytics.CookieSyncBidder{
   326  						{
   327  							BidderCode:   "a",
   328  							NoCookie:     true,
   329  							UsersyncInfo: &analytics.UsersyncInfo{URL: "aURL", Type: "redirect", SupportCORS: true},
   330  						},
   331  					},
   332  				}
   333  				a.On("LogCookieSyncObject", &expected).Once()
   334  			},
   335  		},
   336  	}
   337  
   338  	for _, test := range testCases {
   339  		mockMetrics := metrics.MetricsEngineMock{}
   340  		test.setMetricsExpectations(&mockMetrics)
   341  
   342  		mockAnalytics := MockAnalyticsRunner{}
   343  		test.setAnalyticsExpectations(&mockAnalytics)
   344  
   345  		fakeAccountFetcher := FakeAccountsFetcher{
   346  			AccountData: test.givenAccountData,
   347  		}
   348  
   349  		gdprPermsBuilder := fakePermissionsBuilder{
   350  			permissions: &fakePermissions{},
   351  		}.Builder
   352  		tcf2ConfigBuilder := fakeTCF2ConfigBuilder{
   353  			cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
   354  		}.Builder
   355  
   356  		request := httptest.NewRequest("POST", "/cookiesync", test.givenBody)
   357  		if test.givenCookie != nil {
   358  			httpCookie, err := ToHTTPCookie(test.givenCookie)
   359  			assert.NoError(t, err)
   360  			request.AddCookie(httpCookie)
   361  		}
   362  
   363  		writer := httptest.NewRecorder()
   364  
   365  		endpoint := cookieSyncEndpoint{
   366  			chooser: FakeChooser{Result: test.givenChooserResult},
   367  			config: &config.Configuration{
   368  				AccountDefaults: config.Account{Disabled: false},
   369  			},
   370  			privacyConfig: usersyncPrivacyConfig{
   371  				gdprConfig: config.GDPR{
   372  					Enabled:      true,
   373  					DefaultValue: "0",
   374  				},
   375  				gdprPermissionsBuilder: gdprPermsBuilder,
   376  				tcf2ConfigBuilder:      tcf2ConfigBuilder,
   377  				ccpaEnforce:            true,
   378  			},
   379  			metrics:         &mockMetrics,
   380  			pbsAnalytics:    &mockAnalytics,
   381  			accountsFetcher: &fakeAccountFetcher,
   382  			time:            &fakeTime{time: time.Date(2024, 2, 22, 9, 42, 4, 13, time.UTC)},
   383  		}
   384  		assert.NoError(t, endpoint.config.MarshalAccountDefaults())
   385  
   386  		endpoint.Handle(writer, request, nil)
   387  
   388  		assert.Equal(t, test.expectedStatusCode, writer.Code, test.description+":status_code")
   389  		assert.Equal(t, test.expectedBody, writer.Body.String(), test.description+":body")
   390  
   391  		gotCookie := writer.Header().Get("Set-Cookie")
   392  		if test.expectedCookieDeprecationHeader {
   393  			wantCookieTTL := endpoint.time.Now().Add(time.Second * time.Duration(86400)).UTC().Format(http.TimeFormat)
   394  			wantCookie := fmt.Sprintf("receive-cookie-deprecation=1; Path=/; Expires=%v; HttpOnly; Secure; SameSite=None; Partitioned;", wantCookieTTL)
   395  			assert.Equal(t, wantCookie, gotCookie, test.description)
   396  		} else {
   397  			assert.Empty(t, gotCookie, test.description)
   398  		}
   399  
   400  		mockMetrics.AssertExpectations(t)
   401  		mockAnalytics.AssertExpectations(t)
   402  	}
   403  }
   404  
   405  func TestExtractGDPRSignal(t *testing.T) {
   406  	type testInput struct {
   407  		requestGDPR *int
   408  		gppSID      []int8
   409  	}
   410  	type testOutput struct {
   411  		gdprSignal gdpr.Signal
   412  		gdprString string
   413  		err        error
   414  	}
   415  	testCases := []struct {
   416  		desc     string
   417  		in       testInput
   418  		expected testOutput
   419  	}{
   420  		{
   421  			desc: "SectionTCFEU2 is listed in GPP_SID array, expect SignalYes and nil error",
   422  			in: testInput{
   423  				requestGDPR: nil,
   424  				gppSID:      []int8{2},
   425  			},
   426  			expected: testOutput{
   427  				gdprSignal: gdpr.SignalYes,
   428  				gdprString: strconv.Itoa(int(gdpr.SignalYes)),
   429  				err:        nil,
   430  			},
   431  		},
   432  		{
   433  			desc: "SectionTCFEU2 is not listed in GPP_SID array, expect SignalNo and nil error",
   434  			in: testInput{
   435  				requestGDPR: nil,
   436  				gppSID:      []int8{6},
   437  			},
   438  			expected: testOutput{
   439  				gdprSignal: gdpr.SignalNo,
   440  				gdprString: strconv.Itoa(int(gdpr.SignalNo)),
   441  				err:        nil,
   442  			},
   443  		},
   444  		{
   445  			desc: "Empty GPP_SID array and nil requestGDPR value, expect SignalAmbiguous and nil error",
   446  			in: testInput{
   447  				requestGDPR: nil,
   448  				gppSID:      []int8{},
   449  			},
   450  			expected: testOutput{
   451  				gdprSignal: gdpr.SignalAmbiguous,
   452  				gdprString: "",
   453  				err:        nil,
   454  			},
   455  		},
   456  		{
   457  			desc: "Empty GPP_SID array and non-nil requestGDPR value that could not be successfully parsed, expect SignalAmbiguous and parse error",
   458  			in: testInput{
   459  				requestGDPR: ptrutil.ToPtr(2),
   460  				gppSID:      nil,
   461  			},
   462  			expected: testOutput{
   463  				gdprSignal: gdpr.SignalAmbiguous,
   464  				gdprString: "2",
   465  				err:        &errortypes.BadInput{Message: "GDPR signal should be integer 0 or 1"},
   466  			},
   467  		},
   468  		{
   469  			desc: "Empty GPP_SID array and non-nil requestGDPR value that could be successfully parsed, expect SignalYes and nil error",
   470  			in: testInput{
   471  				requestGDPR: ptrutil.ToPtr(1),
   472  				gppSID:      nil,
   473  			},
   474  			expected: testOutput{
   475  				gdprSignal: gdpr.SignalYes,
   476  				gdprString: "1",
   477  				err:        nil,
   478  			},
   479  		},
   480  	}
   481  	for _, tc := range testCases {
   482  		// run
   483  		outSignal, outGdprStr, outErr := extractGDPRSignal(tc.in.requestGDPR, tc.in.gppSID)
   484  		// assertions
   485  		assert.Equal(t, tc.expected.gdprSignal, outSignal, tc.desc)
   486  		assert.Equal(t, tc.expected.gdprString, outGdprStr, tc.desc)
   487  		assert.Equal(t, tc.expected.err, outErr, tc.desc)
   488  	}
   489  }
   490  
   491  func TestExtractPrivacyPolicies(t *testing.T) {
   492  	type testInput struct {
   493  		request                  cookieSyncRequest
   494  		usersyncDefaultGDPRValue string
   495  	}
   496  	type testOutput struct {
   497  		macros     macros.UserSyncPrivacy
   498  		gdprSignal gdpr.Signal
   499  		policies   privacy.Policies
   500  		err        error
   501  	}
   502  	testCases := []struct {
   503  		desc     string
   504  		in       testInput
   505  		expected testOutput
   506  	}{
   507  		{
   508  			desc: "request GPP string is malformed, expect empty policies, signal No and error",
   509  			in: testInput{
   510  				request: cookieSyncRequest{GPP: "malformedGPPString"},
   511  			},
   512  			expected: testOutput{
   513  				macros:     macros.UserSyncPrivacy{},
   514  				gdprSignal: gdpr.SignalNo,
   515  				policies:   privacy.Policies{},
   516  				err:        errors.New("error parsing GPP header, header must have type=3"),
   517  			},
   518  		},
   519  		{
   520  			desc: "Malformed GPPSid string",
   521  			in: testInput{
   522  				request: cookieSyncRequest{
   523  					GPP:       "DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN",
   524  					GPPSID:    "malformed",
   525  					USPrivacy: "1YYY",
   526  				},
   527  			},
   528  			expected: testOutput{
   529  				macros:     macros.UserSyncPrivacy{},
   530  				gdprSignal: gdpr.SignalNo,
   531  				policies:   privacy.Policies{},
   532  				err:        &strconv.NumError{Func: "ParseInt", Num: "malformed", Err: strconv.ErrSyntax},
   533  			},
   534  		},
   535  		{
   536  			desc: "request USPrivacy string is different from the one in the GPP string, expect empty policies, signalNo and error",
   537  			in: testInput{
   538  				request: cookieSyncRequest{
   539  					GPP:       "DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN",
   540  					GPPSID:    "6",
   541  					USPrivacy: "1YYY",
   542  				},
   543  			},
   544  			expected: testOutput{
   545  				macros:     macros.UserSyncPrivacy{},
   546  				gdprSignal: gdpr.SignalNo,
   547  				policies:   privacy.Policies{},
   548  				err:        errors.New("request.us_privacy consent does not match uspv1"),
   549  			},
   550  		},
   551  		{
   552  			desc: "no issues extracting privacy policies from request GPP and request GPPSid strings",
   553  			in: testInput{
   554  				request: cookieSyncRequest{
   555  					GPP:    "DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN",
   556  					GPPSID: "6",
   557  				},
   558  			},
   559  			expected: testOutput{
   560  				macros: macros.UserSyncPrivacy{
   561  					GDPR:        "0",
   562  					GDPRConsent: "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
   563  					USPrivacy:   "1YNN",
   564  					GPP:         "DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN",
   565  					GPPSID:      "6",
   566  				},
   567  				gdprSignal: gdpr.SignalNo,
   568  				policies:   privacy.Policies{GPPSID: []int8{6}},
   569  				err:        nil,
   570  			},
   571  		},
   572  	}
   573  	for _, tc := range testCases {
   574  		outMacros, outSignal, outPolicies, outErr := extractPrivacyPolicies(tc.in.request, tc.in.usersyncDefaultGDPRValue)
   575  
   576  		assert.Equal(t, tc.expected.macros, outMacros, tc.desc)
   577  		assert.Equal(t, tc.expected.gdprSignal, outSignal, tc.desc)
   578  		assert.Equal(t, tc.expected.policies, outPolicies, tc.desc)
   579  		assert.Equal(t, tc.expected.err, outErr, tc.desc)
   580  	}
   581  }
   582  
   583  func TestCookieSyncParseRequest(t *testing.T) {
   584  	expectedCCPAParsedPolicy, _ := ccpa.Policy{Consent: "1NYN"}.Parse(map[string]struct{}{})
   585  	emptyActivityPoliciesRequest := privacy.NewRequestFromPolicies(privacy.Policies{})
   586  
   587  	testCases := []struct {
   588  		description          string
   589  		givenConfig          config.UserSync
   590  		givenBody            io.Reader
   591  		givenGDPRConfig      config.GDPR
   592  		givenCCPAEnabled     bool
   593  		givenAccountRequired bool
   594  		expectedError        string
   595  		expectedPrivacy      macros.UserSyncPrivacy
   596  		expectedRequest      usersync.Request
   597  	}{
   598  
   599  		{
   600  			description: "Complete Request - includes GPP string with EU TCF V2",
   601  			givenBody: strings.NewReader(`{` +
   602  				`"bidders":["a", "b"],` +
   603  				`"gdpr":1,` +
   604  				`"gdpr_consent":"anyGDPRConsent",` +
   605  				`"us_privacy":"1NYN",` +
   606  				`"gpp":"DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",` +
   607  				`"gpp_sid":"2",` +
   608  				`"limit":42,` +
   609  				`"coopSync":true,` +
   610  				`"filterSettings":{"iframe":{"bidders":"*","filter":"include"}, "image":{"bidders":["b"],"filter":"exclude"}}` +
   611  				`}`),
   612  			givenGDPRConfig:  config.GDPR{Enabled: true, DefaultValue: "0"},
   613  			givenCCPAEnabled: true,
   614  			givenConfig: config.UserSync{
   615  				PriorityGroups: [][]string{{"a", "b", "c"}},
   616  				Cooperative: config.UserSyncCooperative{
   617  					EnabledByDefault: false,
   618  				},
   619  			},
   620  			expectedPrivacy: macros.UserSyncPrivacy{
   621  				GDPR:        "1",
   622  				GDPRConsent: "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
   623  				USPrivacy:   "1NYN",
   624  				GPP:         "DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
   625  				GPPSID:      "2",
   626  			},
   627  			expectedRequest: usersync.Request{
   628  				Bidders: []string{"a", "b"},
   629  				Cooperative: usersync.Cooperative{
   630  					Enabled:        true,
   631  					PriorityGroups: [][]string{{"a", "b", "c"}},
   632  				},
   633  				Limit: 42,
   634  				Privacy: usersyncPrivacy{
   635  					gdprPermissions:  &fakePermissions{},
   636  					ccpaParsedPolicy: expectedCCPAParsedPolicy,
   637  					activityRequest:  privacy.NewRequestFromPolicies(privacy.Policies{GPPSID: []int8{2}}),
   638  					gdprSignal:       1,
   639  				},
   640  				SyncTypeFilter: usersync.SyncTypeFilter{
   641  					IFrame:   usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
   642  					Redirect: usersync.NewSpecificBidderFilter([]string{"b"}, usersync.BidderFilterModeExclude),
   643  				},
   644  				GPPSID: "2",
   645  			},
   646  		},
   647  		{
   648  			description: "Complete Request - Legacy Fields Only",
   649  			givenBody: strings.NewReader(`{` +
   650  				`"bidders":["a", "b"],` +
   651  				`"gdpr":1,` +
   652  				`"gdpr_consent":"anyGDPRConsent",` +
   653  				`"us_privacy":"1NYN",` +
   654  				`"limit":42` +
   655  				`}`),
   656  			givenGDPRConfig:  config.GDPR{Enabled: true, DefaultValue: "0"},
   657  			givenCCPAEnabled: true,
   658  			givenConfig: config.UserSync{
   659  				PriorityGroups: [][]string{{"a", "b", "c"}},
   660  				Cooperative: config.UserSyncCooperative{
   661  					EnabledByDefault: false,
   662  				},
   663  			},
   664  			expectedPrivacy: macros.UserSyncPrivacy{
   665  				GDPR:        "1",
   666  				GDPRConsent: "anyGDPRConsent",
   667  				USPrivacy:   "1NYN",
   668  			},
   669  			expectedRequest: usersync.Request{
   670  				Bidders: []string{"a", "b"},
   671  				Cooperative: usersync.Cooperative{
   672  					Enabled:        false,
   673  					PriorityGroups: [][]string{{"a", "b", "c"}},
   674  				},
   675  				Limit: 42,
   676  				Privacy: usersyncPrivacy{
   677  					gdprPermissions:  &fakePermissions{},
   678  					ccpaParsedPolicy: expectedCCPAParsedPolicy,
   679  					activityRequest:  emptyActivityPoliciesRequest,
   680  					gdprSignal:       1,
   681  				},
   682  				SyncTypeFilter: usersync.SyncTypeFilter{
   683  					IFrame:   usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
   684  					Redirect: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
   685  				},
   686  			},
   687  		},
   688  		{
   689  			description:      "Empty Request",
   690  			givenBody:        strings.NewReader(`{}`),
   691  			givenGDPRConfig:  config.GDPR{Enabled: true, DefaultValue: "0"},
   692  			givenCCPAEnabled: true,
   693  			expectedPrivacy:  macros.UserSyncPrivacy{},
   694  			expectedRequest: usersync.Request{
   695  				Privacy: usersyncPrivacy{
   696  					gdprPermissions: &fakePermissions{},
   697  					activityRequest: emptyActivityPoliciesRequest,
   698  					gdprSignal:      -1,
   699  				},
   700  				SyncTypeFilter: usersync.SyncTypeFilter{
   701  					IFrame:   usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
   702  					Redirect: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
   703  				},
   704  			},
   705  		},
   706  		{
   707  			description:      "Cooperative Unspecified - Default True",
   708  			givenBody:        strings.NewReader(`{}`),
   709  			givenGDPRConfig:  config.GDPR{Enabled: true, DefaultValue: "0"},
   710  			givenCCPAEnabled: true,
   711  			givenConfig: config.UserSync{
   712  				PriorityGroups: [][]string{{"a", "b", "c"}},
   713  				Cooperative: config.UserSyncCooperative{
   714  					EnabledByDefault: true,
   715  				},
   716  			},
   717  			expectedPrivacy: macros.UserSyncPrivacy{},
   718  			expectedRequest: usersync.Request{
   719  				Cooperative: usersync.Cooperative{
   720  					Enabled:        true,
   721  					PriorityGroups: [][]string{{"a", "b", "c"}},
   722  				},
   723  				Privacy: usersyncPrivacy{
   724  					gdprPermissions: &fakePermissions{},
   725  					activityRequest: emptyActivityPoliciesRequest,
   726  					gdprSignal:      -1,
   727  				},
   728  				SyncTypeFilter: usersync.SyncTypeFilter{
   729  					IFrame:   usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
   730  					Redirect: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
   731  				},
   732  			},
   733  		},
   734  		{
   735  			description:      "Cooperative Unspecified - Default False",
   736  			givenBody:        strings.NewReader(`{}`),
   737  			givenGDPRConfig:  config.GDPR{Enabled: true, DefaultValue: "0"},
   738  			givenCCPAEnabled: true,
   739  			givenConfig: config.UserSync{
   740  				PriorityGroups: [][]string{{"a", "b", "c"}},
   741  				Cooperative: config.UserSyncCooperative{
   742  					EnabledByDefault: false,
   743  				},
   744  			},
   745  			expectedPrivacy: macros.UserSyncPrivacy{},
   746  			expectedRequest: usersync.Request{
   747  				Cooperative: usersync.Cooperative{
   748  					Enabled:        false,
   749  					PriorityGroups: [][]string{{"a", "b", "c"}},
   750  				},
   751  				Privacy: usersyncPrivacy{
   752  					gdprPermissions: &fakePermissions{},
   753  					activityRequest: emptyActivityPoliciesRequest,
   754  					gdprSignal:      -1,
   755  				},
   756  				SyncTypeFilter: usersync.SyncTypeFilter{
   757  					IFrame:   usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
   758  					Redirect: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
   759  				},
   760  			},
   761  		},
   762  		{
   763  			description:      "Cooperative False - Default True",
   764  			givenBody:        strings.NewReader(`{"coopSync":false}`),
   765  			givenGDPRConfig:  config.GDPR{Enabled: true, DefaultValue: "0"},
   766  			givenCCPAEnabled: true,
   767  			givenConfig: config.UserSync{
   768  				PriorityGroups: [][]string{{"a", "b", "c"}},
   769  				Cooperative: config.UserSyncCooperative{
   770  					EnabledByDefault: true,
   771  				},
   772  			},
   773  			expectedPrivacy: macros.UserSyncPrivacy{},
   774  			expectedRequest: usersync.Request{
   775  				Cooperative: usersync.Cooperative{
   776  					Enabled:        false,
   777  					PriorityGroups: [][]string{{"a", "b", "c"}},
   778  				},
   779  				Privacy: usersyncPrivacy{
   780  					gdprPermissions: &fakePermissions{},
   781  					activityRequest: emptyActivityPoliciesRequest,
   782  					gdprSignal:      -1,
   783  				},
   784  				SyncTypeFilter: usersync.SyncTypeFilter{
   785  					IFrame:   usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
   786  					Redirect: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
   787  				},
   788  			},
   789  		},
   790  		{
   791  			description:      "Cooperative False - Default False",
   792  			givenBody:        strings.NewReader(`{"coopSync":false}`),
   793  			givenGDPRConfig:  config.GDPR{Enabled: true, DefaultValue: "0"},
   794  			givenCCPAEnabled: true,
   795  			givenConfig: config.UserSync{
   796  				PriorityGroups: [][]string{{"a", "b", "c"}},
   797  				Cooperative: config.UserSyncCooperative{
   798  					EnabledByDefault: false,
   799  				},
   800  			},
   801  			expectedPrivacy: macros.UserSyncPrivacy{},
   802  			expectedRequest: usersync.Request{
   803  				Cooperative: usersync.Cooperative{
   804  					Enabled:        false,
   805  					PriorityGroups: [][]string{{"a", "b", "c"}},
   806  				},
   807  				Privacy: usersyncPrivacy{
   808  					gdprPermissions: &fakePermissions{},
   809  					activityRequest: emptyActivityPoliciesRequest,
   810  					gdprSignal:      -1,
   811  				},
   812  				SyncTypeFilter: usersync.SyncTypeFilter{
   813  					IFrame:   usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
   814  					Redirect: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
   815  				},
   816  			},
   817  		},
   818  		{
   819  			description:      "Cooperative True - Default True",
   820  			givenBody:        strings.NewReader(`{"coopSync":true}`),
   821  			givenGDPRConfig:  config.GDPR{Enabled: true, DefaultValue: "0"},
   822  			givenCCPAEnabled: true,
   823  			givenConfig: config.UserSync{
   824  				PriorityGroups: [][]string{{"a", "b", "c"}},
   825  				Cooperative: config.UserSyncCooperative{
   826  					EnabledByDefault: true,
   827  				},
   828  			},
   829  			expectedPrivacy: macros.UserSyncPrivacy{},
   830  			expectedRequest: usersync.Request{
   831  				Cooperative: usersync.Cooperative{
   832  					Enabled:        true,
   833  					PriorityGroups: [][]string{{"a", "b", "c"}},
   834  				},
   835  				Privacy: usersyncPrivacy{
   836  					gdprPermissions: &fakePermissions{},
   837  					activityRequest: emptyActivityPoliciesRequest,
   838  					gdprSignal:      -1,
   839  				},
   840  				SyncTypeFilter: usersync.SyncTypeFilter{
   841  					IFrame:   usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
   842  					Redirect: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
   843  				},
   844  			},
   845  		},
   846  		{
   847  			description:      "Cooperative True - Default False",
   848  			givenBody:        strings.NewReader(`{"coopSync":true}`),
   849  			givenGDPRConfig:  config.GDPR{Enabled: true, DefaultValue: "0"},
   850  			givenCCPAEnabled: true,
   851  			givenConfig: config.UserSync{
   852  				PriorityGroups: [][]string{{"a", "b", "c"}},
   853  				Cooperative: config.UserSyncCooperative{
   854  					EnabledByDefault: false,
   855  				},
   856  			},
   857  			expectedPrivacy: macros.UserSyncPrivacy{},
   858  			expectedRequest: usersync.Request{
   859  				Cooperative: usersync.Cooperative{
   860  					Enabled:        true,
   861  					PriorityGroups: [][]string{{"a", "b", "c"}},
   862  				},
   863  				Privacy: usersyncPrivacy{
   864  					gdprPermissions: &fakePermissions{},
   865  					activityRequest: emptyActivityPoliciesRequest,
   866  					gdprSignal:      -1,
   867  				},
   868  				SyncTypeFilter: usersync.SyncTypeFilter{
   869  					IFrame:   usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
   870  					Redirect: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
   871  				},
   872  			},
   873  		},
   874  		{
   875  			description:      "CCPA Consent Invalid",
   876  			givenBody:        strings.NewReader(`{"us_privacy":"invalid"}`),
   877  			givenGDPRConfig:  config.GDPR{Enabled: true, DefaultValue: "0"},
   878  			givenCCPAEnabled: true,
   879  			expectedPrivacy:  macros.UserSyncPrivacy{},
   880  			expectedRequest: usersync.Request{
   881  				Privacy: usersyncPrivacy{
   882  					gdprPermissions: &fakePermissions{},
   883  					activityRequest: emptyActivityPoliciesRequest,
   884  					gdprSignal:      -1,
   885  				},
   886  				SyncTypeFilter: usersync.SyncTypeFilter{
   887  					IFrame:   usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
   888  					Redirect: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
   889  				},
   890  			},
   891  		},
   892  		{
   893  			description:      "CCPA Disabled",
   894  			givenBody:        strings.NewReader(`{"us_privacy":"1NYN"}`),
   895  			givenGDPRConfig:  config.GDPR{Enabled: true, DefaultValue: "0"},
   896  			givenCCPAEnabled: false,
   897  			expectedPrivacy: macros.UserSyncPrivacy{
   898  				USPrivacy: "1NYN",
   899  			},
   900  			expectedRequest: usersync.Request{
   901  				Privacy: usersyncPrivacy{
   902  					gdprPermissions: &fakePermissions{},
   903  					activityRequest: emptyActivityPoliciesRequest,
   904  					gdprSignal:      -1,
   905  				},
   906  				SyncTypeFilter: usersync.SyncTypeFilter{
   907  					IFrame:   usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
   908  					Redirect: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
   909  				},
   910  			},
   911  		},
   912  		{
   913  			description:      "Invalid JSON",
   914  			givenBody:        strings.NewReader(`malformed`),
   915  			givenGDPRConfig:  config.GDPR{Enabled: true, DefaultValue: "0"},
   916  			givenCCPAEnabled: true,
   917  			expectedError:    "JSON parsing failed: expect { or n, but found m",
   918  		},
   919  		{
   920  			description:      "Invalid Type Filter",
   921  			givenBody:        strings.NewReader(`{"filterSettings":{"iframe":{"bidders":"invalid","filter":"exclude"}}}`),
   922  			givenGDPRConfig:  config.GDPR{Enabled: true, DefaultValue: "0"},
   923  			givenCCPAEnabled: true,
   924  			expectedError:    "error parsing filtersettings.iframe: invalid bidders value `invalid`. must either be '*' or a string array",
   925  		},
   926  		{
   927  			description:      "Invalid GDPR Signal",
   928  			givenBody:        strings.NewReader(`{"gdpr":5}`),
   929  			givenGDPRConfig:  config.GDPR{Enabled: true, DefaultValue: "0"},
   930  			givenCCPAEnabled: true,
   931  			expectedError:    "GDPR signal should be integer 0 or 1",
   932  		},
   933  		{
   934  			description:      "Missing GDPR Consent - Explicit Signal 0",
   935  			givenBody:        strings.NewReader(`{"gdpr":0}`),
   936  			givenGDPRConfig:  config.GDPR{Enabled: true, DefaultValue: "0"},
   937  			givenCCPAEnabled: true,
   938  			expectedPrivacy: macros.UserSyncPrivacy{
   939  				GDPR: "0",
   940  			},
   941  			expectedRequest: usersync.Request{
   942  				Privacy: usersyncPrivacy{
   943  					gdprPermissions: &fakePermissions{},
   944  					activityRequest: emptyActivityPoliciesRequest,
   945  					gdprSignal:      0,
   946  				},
   947  				SyncTypeFilter: usersync.SyncTypeFilter{
   948  					IFrame:   usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
   949  					Redirect: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
   950  				},
   951  			},
   952  		},
   953  		{
   954  			description:      "Missing GDPR Consent - Explicit Signal 1",
   955  			givenBody:        strings.NewReader(`{"gdpr":1}`),
   956  			givenGDPRConfig:  config.GDPR{Enabled: true, DefaultValue: "0"},
   957  			givenCCPAEnabled: true,
   958  			expectedError:    "gdpr_consent is required if gdpr=1",
   959  		},
   960  		{
   961  			description:      "Missing GDPR Consent - Ambiguous Signal - Default Value 0",
   962  			givenBody:        strings.NewReader(`{}`),
   963  			givenGDPRConfig:  config.GDPR{Enabled: true, DefaultValue: "0"},
   964  			givenCCPAEnabled: true,
   965  			expectedPrivacy: macros.UserSyncPrivacy{
   966  				GDPR: "",
   967  			},
   968  			expectedRequest: usersync.Request{
   969  				Privacy: usersyncPrivacy{
   970  					gdprPermissions: &fakePermissions{},
   971  					activityRequest: emptyActivityPoliciesRequest,
   972  					gdprSignal:      -1,
   973  				},
   974  				SyncTypeFilter: usersync.SyncTypeFilter{
   975  					IFrame:   usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
   976  					Redirect: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
   977  				},
   978  			},
   979  		},
   980  		{
   981  			description:      "Missing GDPR Consent - Ambiguous Signal - Default Value 1",
   982  			givenBody:        strings.NewReader(`{}`),
   983  			givenGDPRConfig:  config.GDPR{Enabled: true, DefaultValue: "1"},
   984  			givenCCPAEnabled: true,
   985  			expectedError:    "gdpr_consent is required. gdpr is not specified and is assumed to be 1 by the server. set gdpr=0 to exempt this request",
   986  		},
   987  		{
   988  			description:      "HTTP Read Error",
   989  			givenBody:        iotest.ErrReader(errors.New("anyError")),
   990  			givenGDPRConfig:  config.GDPR{Enabled: true, DefaultValue: "0"},
   991  			givenCCPAEnabled: true,
   992  			expectedError:    "Failed to read request body",
   993  		},
   994  		{
   995  			description: "Account Defaults - Max Limit + Default Coop",
   996  			givenBody: strings.NewReader(`{` +
   997  				`"bidders":["a", "b"],` +
   998  				`"limit":42,` +
   999  				`"account":"TestAccount"` +
  1000  				`}`),
  1001  			givenGDPRConfig:  config.GDPR{Enabled: true, DefaultValue: "0"},
  1002  			givenCCPAEnabled: true,
  1003  			givenConfig: config.UserSync{
  1004  				PriorityGroups: [][]string{{"a", "b", "c"}},
  1005  				Cooperative:    config.UserSyncCooperative{},
  1006  			},
  1007  			expectedPrivacy: macros.UserSyncPrivacy{},
  1008  			expectedRequest: usersync.Request{
  1009  				Bidders: []string{"a", "b"},
  1010  				Cooperative: usersync.Cooperative{
  1011  					Enabled:        true,
  1012  					PriorityGroups: [][]string{{"a", "b", "c"}},
  1013  				},
  1014  				Limit: 30,
  1015  				Privacy: usersyncPrivacy{
  1016  					gdprPermissions: &fakePermissions{},
  1017  					activityRequest: emptyActivityPoliciesRequest,
  1018  					gdprSignal:      -1,
  1019  				},
  1020  				SyncTypeFilter: usersync.SyncTypeFilter{
  1021  					IFrame:   usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
  1022  					Redirect: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
  1023  				},
  1024  			},
  1025  		},
  1026  		{
  1027  			description: "Account Defaults - DefaultLimit",
  1028  			givenBody: strings.NewReader(`{` +
  1029  				`"bidders":["a", "b"],` +
  1030  				`"account":"TestAccount"` +
  1031  				`}`),
  1032  			givenGDPRConfig:  config.GDPR{Enabled: true, DefaultValue: "0"},
  1033  			givenCCPAEnabled: true,
  1034  			givenConfig: config.UserSync{
  1035  				PriorityGroups: [][]string{{"a", "b", "c"}},
  1036  				Cooperative: config.UserSyncCooperative{
  1037  					EnabledByDefault: false,
  1038  				},
  1039  			},
  1040  			expectedPrivacy: macros.UserSyncPrivacy{},
  1041  			expectedRequest: usersync.Request{
  1042  				Bidders: []string{"a", "b"},
  1043  				Cooperative: usersync.Cooperative{
  1044  					Enabled:        true,
  1045  					PriorityGroups: [][]string{{"a", "b", "c"}},
  1046  				},
  1047  				Limit: 20,
  1048  				Privacy: usersyncPrivacy{
  1049  					gdprPermissions: &fakePermissions{},
  1050  					activityRequest: emptyActivityPoliciesRequest,
  1051  					gdprSignal:      -1,
  1052  				},
  1053  				SyncTypeFilter: usersync.SyncTypeFilter{
  1054  					IFrame:   usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
  1055  					Redirect: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
  1056  				},
  1057  			},
  1058  		},
  1059  		{
  1060  			description: "Account Defaults - Error",
  1061  			givenBody: strings.NewReader(`{` +
  1062  				`"bidders":["a", "b"],` +
  1063  				`"account":"DisabledAccount"` +
  1064  				`}`),
  1065  			givenGDPRConfig:  config.GDPR{Enabled: true, DefaultValue: "0"},
  1066  			givenCCPAEnabled: true,
  1067  			givenConfig: config.UserSync{
  1068  				PriorityGroups: [][]string{{"a", "b", "c"}},
  1069  				Cooperative: config.UserSyncCooperative{
  1070  					EnabledByDefault: false,
  1071  				},
  1072  			},
  1073  			expectedPrivacy: macros.UserSyncPrivacy{},
  1074  			expectedRequest: usersync.Request{
  1075  				Bidders: []string{"a", "b"},
  1076  				Cooperative: usersync.Cooperative{
  1077  					Enabled:        true,
  1078  					PriorityGroups: [][]string{{"a", "b", "c"}},
  1079  				},
  1080  				Limit: 20,
  1081  				Privacy: usersyncPrivacy{
  1082  					gdprPermissions: &fakePermissions{},
  1083  					activityRequest: emptyActivityPoliciesRequest,
  1084  					gdprSignal:      0,
  1085  				},
  1086  				SyncTypeFilter: usersync.SyncTypeFilter{
  1087  					IFrame:   usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
  1088  					Redirect: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
  1089  				},
  1090  			},
  1091  			expectedError:        errCookieSyncAccountBlocked.Error(),
  1092  			givenAccountRequired: true,
  1093  		},
  1094  	}
  1095  
  1096  	for _, test := range testCases {
  1097  		httpRequest := httptest.NewRequest("POST", "/cookiesync", test.givenBody)
  1098  
  1099  		gdprPermsBuilder := fakePermissionsBuilder{
  1100  			permissions: &fakePermissions{},
  1101  		}.Builder
  1102  		tcf2ConfigBuilder := fakeTCF2ConfigBuilder{
  1103  			cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
  1104  		}.Builder
  1105  
  1106  		endpoint := cookieSyncEndpoint{
  1107  			config: &config.Configuration{
  1108  				UserSync:        test.givenConfig,
  1109  				AccountRequired: test.givenAccountRequired,
  1110  			},
  1111  			privacyConfig: usersyncPrivacyConfig{
  1112  				gdprConfig:             test.givenGDPRConfig,
  1113  				gdprPermissionsBuilder: gdprPermsBuilder,
  1114  				tcf2ConfigBuilder:      tcf2ConfigBuilder,
  1115  				ccpaEnforce:            test.givenCCPAEnabled,
  1116  			},
  1117  			accountsFetcher: FakeAccountsFetcher{AccountData: map[string]json.RawMessage{
  1118  				"TestAccount":                   json.RawMessage(`{"cookie_sync": {"default_limit": 20, "max_limit": 30, "default_coop_sync": true}}`),
  1119  				"DisabledAccount":               json.RawMessage(`{"disabled":true}`),
  1120  				"ValidAccountInvalidActivities": json.RawMessage(`{"privacy":{"allowactivities":{"syncUser":{"rules":[{"condition":{"componentName": ["bidderA.bidderB.bidderC"]}}]}}}}`),
  1121  			}},
  1122  		}
  1123  		assert.NoError(t, endpoint.config.MarshalAccountDefaults())
  1124  		request, privacyPolicies, _, err := endpoint.parseRequest(httpRequest)
  1125  
  1126  		if test.expectedError == "" {
  1127  			assert.NoError(t, err, test.description+":err")
  1128  			assert.Equal(t, test.expectedRequest, request, test.description+":request")
  1129  			assert.Equal(t, test.expectedPrivacy, privacyPolicies, test.description+":privacy")
  1130  		} else {
  1131  			assert.EqualError(t, err, test.expectedError, test.description+":err")
  1132  			assert.Empty(t, request, test.description+":request")
  1133  			assert.Empty(t, privacyPolicies, test.description+":privacy")
  1134  		}
  1135  	}
  1136  }
  1137  
  1138  func TestSetLimit(t *testing.T) {
  1139  	intNegative1 := -1
  1140  	int20 := 20
  1141  	int30 := 30
  1142  	int40 := 40
  1143  
  1144  	testCases := []struct {
  1145  		description     string
  1146  		givenRequest    cookieSyncRequest
  1147  		givenAccount    *config.Account
  1148  		expectedRequest cookieSyncRequest
  1149  	}{
  1150  		{
  1151  			description: "Default Limit is Applied (request limit = 0)",
  1152  			givenRequest: cookieSyncRequest{
  1153  				Limit: 0,
  1154  			},
  1155  			givenAccount: &config.Account{
  1156  				CookieSync: config.CookieSync{
  1157  					DefaultLimit: &int20,
  1158  				},
  1159  			},
  1160  			expectedRequest: cookieSyncRequest{
  1161  				Limit: 20,
  1162  			},
  1163  		},
  1164  		{
  1165  			description: "Default Limit is Not Applied (default limit not set)",
  1166  			givenRequest: cookieSyncRequest{
  1167  				Limit: 0,
  1168  			},
  1169  			givenAccount: &config.Account{
  1170  				CookieSync: config.CookieSync{
  1171  					DefaultLimit: nil,
  1172  				},
  1173  			},
  1174  			expectedRequest: cookieSyncRequest{
  1175  				Limit: 0,
  1176  			},
  1177  		},
  1178  		{
  1179  			description: "Default Limit is Not Applied (request limit > 0)",
  1180  			givenRequest: cookieSyncRequest{
  1181  				Limit: 10,
  1182  			},
  1183  			givenAccount: &config.Account{
  1184  				CookieSync: config.CookieSync{
  1185  					DefaultLimit: &int20,
  1186  				},
  1187  			},
  1188  			expectedRequest: cookieSyncRequest{
  1189  				Limit: 10,
  1190  			},
  1191  		},
  1192  		{
  1193  			description: "Max Limit is Applied (request limit <= 0)",
  1194  			givenRequest: cookieSyncRequest{
  1195  				Limit: 0,
  1196  			},
  1197  			givenAccount: &config.Account{
  1198  				CookieSync: config.CookieSync{
  1199  					MaxLimit: &int30,
  1200  				},
  1201  			},
  1202  			expectedRequest: cookieSyncRequest{
  1203  				Limit: 30,
  1204  			},
  1205  		},
  1206  		{
  1207  			description: "Max Limit is Applied (0 < max < limit)",
  1208  			givenRequest: cookieSyncRequest{
  1209  				Limit: 40,
  1210  			},
  1211  			givenAccount: &config.Account{
  1212  				CookieSync: config.CookieSync{
  1213  					MaxLimit: &int30,
  1214  				},
  1215  			},
  1216  			expectedRequest: cookieSyncRequest{
  1217  				Limit: 30,
  1218  			},
  1219  		},
  1220  		{
  1221  			description: "Max Limit is Not Applied (max not set)",
  1222  			givenRequest: cookieSyncRequest{
  1223  				Limit: 10,
  1224  			},
  1225  			givenAccount: &config.Account{
  1226  				CookieSync: config.CookieSync{
  1227  					MaxLimit: nil,
  1228  				},
  1229  			},
  1230  			expectedRequest: cookieSyncRequest{
  1231  				Limit: 10,
  1232  			},
  1233  		},
  1234  		{
  1235  			description: "Max Limit is Not Applied (0 < limit < max)",
  1236  			givenRequest: cookieSyncRequest{
  1237  				Limit: 10,
  1238  			},
  1239  			givenAccount: &config.Account{
  1240  				CookieSync: config.CookieSync{
  1241  					MaxLimit: &int30,
  1242  				},
  1243  			},
  1244  			expectedRequest: cookieSyncRequest{
  1245  				Limit: 10,
  1246  			},
  1247  		},
  1248  		{
  1249  			description: "Max Limit is Applied After applying the default",
  1250  			givenRequest: cookieSyncRequest{
  1251  				Limit: 0,
  1252  			},
  1253  			givenAccount: &config.Account{
  1254  				CookieSync: config.CookieSync{
  1255  					DefaultLimit: &int40,
  1256  					MaxLimit:     &int30,
  1257  				},
  1258  			},
  1259  			expectedRequest: cookieSyncRequest{
  1260  				Limit: 30,
  1261  			},
  1262  		},
  1263  		{
  1264  			description: "Negative Value Check",
  1265  			givenRequest: cookieSyncRequest{
  1266  				Limit: 0,
  1267  			},
  1268  			givenAccount: &config.Account{
  1269  				CookieSync: config.CookieSync{
  1270  					DefaultLimit: &intNegative1,
  1271  					MaxLimit:     &intNegative1,
  1272  				},
  1273  			},
  1274  			expectedRequest: cookieSyncRequest{
  1275  				Limit: 0,
  1276  			},
  1277  		},
  1278  	}
  1279  
  1280  	for _, test := range testCases {
  1281  		endpoint := cookieSyncEndpoint{}
  1282  		request := endpoint.setLimit(test.givenRequest, test.givenAccount.CookieSync)
  1283  		assert.Equal(t, test.expectedRequest, request, test.description)
  1284  	}
  1285  }
  1286  
  1287  func TestSetCooperativeSync(t *testing.T) {
  1288  	coopSyncFalse := false
  1289  	coopSyncTrue := true
  1290  
  1291  	testCases := []struct {
  1292  		description     string
  1293  		givenRequest    cookieSyncRequest
  1294  		givenAccount    *config.Account
  1295  		expectedRequest cookieSyncRequest
  1296  	}{
  1297  		{
  1298  			description: "Request coop sync unmodified - request sync nil & default sync nil",
  1299  			givenRequest: cookieSyncRequest{
  1300  				CooperativeSync: nil,
  1301  			},
  1302  			givenAccount: &config.Account{
  1303  				CookieSync: config.CookieSync{
  1304  					DefaultCoopSync: nil,
  1305  				},
  1306  			},
  1307  			expectedRequest: cookieSyncRequest{
  1308  				CooperativeSync: nil,
  1309  			},
  1310  		},
  1311  		{
  1312  			description: "Request coop sync set to default - request sync nil & default sync not nil",
  1313  			givenRequest: cookieSyncRequest{
  1314  				CooperativeSync: nil,
  1315  			},
  1316  			givenAccount: &config.Account{
  1317  				CookieSync: config.CookieSync{
  1318  					DefaultCoopSync: &coopSyncTrue,
  1319  				},
  1320  			},
  1321  			expectedRequest: cookieSyncRequest{
  1322  				CooperativeSync: &coopSyncTrue,
  1323  			},
  1324  		},
  1325  		{
  1326  			description: "Request coop sync unmodified - request sync not nil & default sync nil",
  1327  			givenRequest: cookieSyncRequest{
  1328  				CooperativeSync: &coopSyncTrue,
  1329  			},
  1330  			givenAccount: &config.Account{
  1331  				CookieSync: config.CookieSync{
  1332  					DefaultCoopSync: nil,
  1333  				},
  1334  			},
  1335  			expectedRequest: cookieSyncRequest{
  1336  				CooperativeSync: &coopSyncTrue,
  1337  			},
  1338  		},
  1339  		{
  1340  			description: "Request coop sync unmodified - request sync not nil & default sync not nil",
  1341  			givenRequest: cookieSyncRequest{
  1342  				CooperativeSync: &coopSyncFalse,
  1343  			},
  1344  			givenAccount: &config.Account{
  1345  				CookieSync: config.CookieSync{
  1346  					DefaultCoopSync: &coopSyncTrue,
  1347  				},
  1348  			},
  1349  			expectedRequest: cookieSyncRequest{
  1350  				CooperativeSync: &coopSyncFalse,
  1351  			},
  1352  		},
  1353  	}
  1354  
  1355  	for _, test := range testCases {
  1356  		endpoint := cookieSyncEndpoint{}
  1357  		request := endpoint.setCooperativeSync(test.givenRequest, test.givenAccount.CookieSync)
  1358  		assert.Equal(t, test.expectedRequest, request, test.description)
  1359  	}
  1360  }
  1361  
  1362  func TestWriteParseRequestErrorMetrics(t *testing.T) {
  1363  	err := errors.New("anyError")
  1364  
  1365  	mockAnalytics := MockAnalyticsRunner{}
  1366  	mockAnalytics.On("LogCookieSyncObject", mock.Anything)
  1367  	writer := httptest.NewRecorder()
  1368  
  1369  	endpoint := cookieSyncEndpoint{pbsAnalytics: &mockAnalytics}
  1370  	endpoint.handleError(writer, err, 418)
  1371  
  1372  	assert.Equal(t, writer.Code, 418)
  1373  	assert.Equal(t, writer.Body.String(), "anyError\n")
  1374  	mockAnalytics.AssertCalled(t, "LogCookieSyncObject", &analytics.CookieSyncObject{
  1375  		Status:       418,
  1376  		Errors:       []error{err},
  1377  		BidderStatus: []*analytics.CookieSyncBidder{},
  1378  	})
  1379  }
  1380  
  1381  func TestCookieSyncWriteParseRequestErrorMetrics(t *testing.T) {
  1382  	testCases := []struct {
  1383  		description     string
  1384  		err             error
  1385  		setExpectations func(*metrics.MetricsEngineMock)
  1386  	}{
  1387  		{
  1388  			description: "Account Blocked",
  1389  			err:         errCookieSyncAccountBlocked,
  1390  			setExpectations: func(m *metrics.MetricsEngineMock) {
  1391  				m.On("RecordCookieSync", metrics.CookieSyncAccountBlocked).Once()
  1392  			},
  1393  		},
  1394  		{
  1395  			description: "Account Invalid",
  1396  			err:         errCookieSyncAccountInvalid,
  1397  			setExpectations: func(m *metrics.MetricsEngineMock) {
  1398  				m.On("RecordCookieSync", metrics.CookieSyncAccountInvalid).Once()
  1399  			},
  1400  		},
  1401  		{
  1402  			description: "Account Malformed",
  1403  			err:         errCookieSyncAccountConfigMalformed,
  1404  			setExpectations: func(m *metrics.MetricsEngineMock) {
  1405  				m.On("RecordCookieSync", metrics.CookieSyncAccountConfigMalformed).Once()
  1406  			},
  1407  		},
  1408  		{
  1409  			description: "No Special Case",
  1410  			err:         errors.New("any error"),
  1411  			setExpectations: func(m *metrics.MetricsEngineMock) {
  1412  				m.On("RecordCookieSync", metrics.CookieSyncBadRequest).Once()
  1413  			},
  1414  		},
  1415  	}
  1416  
  1417  	for _, test := range testCases {
  1418  		mockMetrics := metrics.MetricsEngineMock{}
  1419  		test.setExpectations(&mockMetrics)
  1420  
  1421  		endpoint := &cookieSyncEndpoint{metrics: &mockMetrics}
  1422  		endpoint.writeParseRequestErrorMetrics(test.err)
  1423  
  1424  		mockMetrics.AssertExpectations(t)
  1425  	}
  1426  }
  1427  
  1428  func TestParseTypeFilter(t *testing.T) {
  1429  	testCases := []struct {
  1430  		description    string
  1431  		given          *cookieSyncRequestFilterSettings
  1432  		expectedError  string
  1433  		expectedFilter usersync.SyncTypeFilter
  1434  	}{
  1435  		{
  1436  			description: "Nil",
  1437  			given:       nil,
  1438  			expectedFilter: usersync.SyncTypeFilter{
  1439  				IFrame:   usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
  1440  				Redirect: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
  1441  			},
  1442  		},
  1443  		{
  1444  			description: "Nil Object",
  1445  			given:       &cookieSyncRequestFilterSettings{},
  1446  			expectedFilter: usersync.SyncTypeFilter{
  1447  				IFrame:   usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
  1448  				Redirect: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
  1449  			},
  1450  		},
  1451  		{
  1452  			description: "Given IFrame Only",
  1453  			given: &cookieSyncRequestFilterSettings{
  1454  				IFrame: &cookieSyncRequestFilter{Bidders: []interface{}{"a"}, Mode: "exclude"},
  1455  			},
  1456  			expectedFilter: usersync.SyncTypeFilter{
  1457  				IFrame:   usersync.NewSpecificBidderFilter([]string{"a"}, usersync.BidderFilterModeExclude),
  1458  				Redirect: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
  1459  			},
  1460  		},
  1461  		{
  1462  			description: "Given Redirect Only",
  1463  			given: &cookieSyncRequestFilterSettings{
  1464  				Redirect: &cookieSyncRequestFilter{Bidders: []interface{}{"b"}, Mode: "exclude"},
  1465  			},
  1466  			expectedFilter: usersync.SyncTypeFilter{
  1467  				IFrame:   usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
  1468  				Redirect: usersync.NewSpecificBidderFilter([]string{"b"}, usersync.BidderFilterModeExclude),
  1469  			},
  1470  		},
  1471  		{
  1472  			description: "Given Both",
  1473  			given: &cookieSyncRequestFilterSettings{
  1474  				IFrame:   &cookieSyncRequestFilter{Bidders: []interface{}{"a"}, Mode: "exclude"},
  1475  				Redirect: &cookieSyncRequestFilter{Bidders: []interface{}{"b"}, Mode: "exclude"},
  1476  			},
  1477  			expectedFilter: usersync.SyncTypeFilter{
  1478  				IFrame:   usersync.NewSpecificBidderFilter([]string{"a"}, usersync.BidderFilterModeExclude),
  1479  				Redirect: usersync.NewSpecificBidderFilter([]string{"b"}, usersync.BidderFilterModeExclude),
  1480  			},
  1481  		},
  1482  		{
  1483  			description: "IFrame Error",
  1484  			given: &cookieSyncRequestFilterSettings{
  1485  				IFrame:   &cookieSyncRequestFilter{Bidders: 42, Mode: "exclude"},
  1486  				Redirect: &cookieSyncRequestFilter{Bidders: []interface{}{"b"}, Mode: "exclude"},
  1487  			},
  1488  			expectedError: "error parsing filtersettings.iframe: invalid bidders type. must either be a string '*' or a string array of bidders",
  1489  		},
  1490  		{
  1491  			description: "Redirect Error",
  1492  			given: &cookieSyncRequestFilterSettings{
  1493  				IFrame:   &cookieSyncRequestFilter{Bidders: []interface{}{"a"}, Mode: "exclude"},
  1494  				Redirect: &cookieSyncRequestFilter{Bidders: 42, Mode: "exclude"},
  1495  			},
  1496  			expectedError: "error parsing filtersettings.image: invalid bidders type. must either be a string '*' or a string array of bidders",
  1497  		},
  1498  	}
  1499  
  1500  	for _, test := range testCases {
  1501  		result, err := parseTypeFilter(test.given)
  1502  
  1503  		if test.expectedError == "" {
  1504  			assert.NoError(t, err, test.description+":err")
  1505  			assert.Equal(t, test.expectedFilter, result, test.description+":result")
  1506  		} else {
  1507  			assert.EqualError(t, err, test.expectedError, test.description+":err")
  1508  			assert.Empty(t, result, test.description+":result")
  1509  		}
  1510  	}
  1511  }
  1512  
  1513  func TestParseBidderFilter(t *testing.T) {
  1514  	testCases := []struct {
  1515  		description    string
  1516  		given          *cookieSyncRequestFilter
  1517  		expectedError  string
  1518  		expectedFilter usersync.BidderFilter
  1519  	}{
  1520  		{
  1521  			description:    "Nil",
  1522  			given:          nil,
  1523  			expectedFilter: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
  1524  		},
  1525  		{
  1526  			description:    "All Bidders - Include",
  1527  			given:          &cookieSyncRequestFilter{Bidders: "*", Mode: "include"},
  1528  			expectedFilter: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
  1529  		},
  1530  		{
  1531  			description:    "All Bidders - Exclude",
  1532  			given:          &cookieSyncRequestFilter{Bidders: "*", Mode: "exclude"},
  1533  			expectedFilter: usersync.NewUniformBidderFilter(usersync.BidderFilterModeExclude),
  1534  		},
  1535  		{
  1536  			description:   "All Bidders - Invalid Mode",
  1537  			given:         &cookieSyncRequestFilter{Bidders: "*", Mode: "invalid"},
  1538  			expectedError: "invalid filter value 'invalid'. must be either 'include' or 'exclude'",
  1539  		},
  1540  		{
  1541  			description:   "All Bidders - Unexpected Bidders Value",
  1542  			given:         &cookieSyncRequestFilter{Bidders: "invalid", Mode: "include"},
  1543  			expectedError: "invalid bidders value `invalid`. must either be '*' or a string array",
  1544  		},
  1545  		{
  1546  			description:    "Specific Bidders - Include",
  1547  			given:          &cookieSyncRequestFilter{Bidders: []interface{}{"a", "b"}, Mode: "include"},
  1548  			expectedFilter: usersync.NewSpecificBidderFilter([]string{"a", "b"}, usersync.BidderFilterModeInclude),
  1549  		},
  1550  		{
  1551  			description:    "Specific Bidders - Exclude",
  1552  			given:          &cookieSyncRequestFilter{Bidders: []interface{}{"a", "b"}, Mode: "exclude"},
  1553  			expectedFilter: usersync.NewSpecificBidderFilter([]string{"a", "b"}, usersync.BidderFilterModeExclude),
  1554  		},
  1555  		{
  1556  			description:   "Specific Bidders - Invalid Mode",
  1557  			given:         &cookieSyncRequestFilter{Bidders: []interface{}{"a", "b"}, Mode: "invalid"},
  1558  			expectedError: "invalid filter value 'invalid'. must be either 'include' or 'exclude'",
  1559  		},
  1560  		{
  1561  			description:   "Invalid Bidders Type",
  1562  			given:         &cookieSyncRequestFilter{Bidders: 42, Mode: "include"},
  1563  			expectedError: "invalid bidders type. must either be a string '*' or a string array of bidders",
  1564  		},
  1565  		{
  1566  			description:   "Invalid Bidders Type Of Array Element",
  1567  			given:         &cookieSyncRequestFilter{Bidders: []interface{}{"a", 42}, Mode: "include"},
  1568  			expectedError: "invalid bidders type. must either be a string '*' or a string array of bidders",
  1569  		},
  1570  	}
  1571  
  1572  	for _, test := range testCases {
  1573  		result, err := parseBidderFilter(test.given)
  1574  
  1575  		if test.expectedError == "" {
  1576  			assert.NoError(t, err, test.description+":err")
  1577  			assert.Equal(t, test.expectedFilter, result, test.description+":result")
  1578  		} else {
  1579  			assert.EqualError(t, err, test.expectedError, test.description+":err")
  1580  			assert.Nil(t, result, test.description+":result")
  1581  		}
  1582  	}
  1583  }
  1584  
  1585  func TestCookieSyncHandleError(t *testing.T) {
  1586  	err := errors.New("anyError")
  1587  
  1588  	mockAnalytics := MockAnalyticsRunner{}
  1589  	mockAnalytics.On("LogCookieSyncObject", mock.Anything)
  1590  	writer := httptest.NewRecorder()
  1591  
  1592  	endpoint := cookieSyncEndpoint{pbsAnalytics: &mockAnalytics}
  1593  	endpoint.handleError(writer, err, 418)
  1594  
  1595  	assert.Equal(t, writer.Code, 418)
  1596  	assert.Equal(t, writer.Body.String(), "anyError\n")
  1597  	mockAnalytics.AssertCalled(t, "LogCookieSyncObject", &analytics.CookieSyncObject{
  1598  		Status:       418,
  1599  		Errors:       []error{err},
  1600  		BidderStatus: []*analytics.CookieSyncBidder{},
  1601  	})
  1602  }
  1603  
  1604  func TestCookieSyncWriteBidderMetrics(t *testing.T) {
  1605  	testCases := []struct {
  1606  		description     string
  1607  		given           []usersync.BidderEvaluation
  1608  		setExpectations func(*metrics.MetricsEngineMock)
  1609  	}{
  1610  		{
  1611  			description: "None",
  1612  			given:       []usersync.BidderEvaluation{},
  1613  			setExpectations: func(m *metrics.MetricsEngineMock) {
  1614  			},
  1615  		},
  1616  		{
  1617  			description: "One - OK",
  1618  			given:       []usersync.BidderEvaluation{{Bidder: "a", SyncerKey: "aSyncer", Status: usersync.StatusOK}},
  1619  			setExpectations: func(m *metrics.MetricsEngineMock) {
  1620  				m.On("RecordSyncerRequest", "aSyncer", metrics.SyncerCookieSyncOK).Once()
  1621  			},
  1622  		},
  1623  		{
  1624  			description: "One - Blocked By GDPR",
  1625  			given:       []usersync.BidderEvaluation{{Bidder: "a", SyncerKey: "aSyncer", Status: usersync.StatusBlockedByPrivacy}},
  1626  			setExpectations: func(m *metrics.MetricsEngineMock) {
  1627  				m.On("RecordSyncerRequest", "aSyncer", metrics.SyncerCookieSyncPrivacyBlocked).Once()
  1628  			},
  1629  		},
  1630  		{
  1631  			description: "One - Blocked By CCPA",
  1632  			given:       []usersync.BidderEvaluation{{Bidder: "a", SyncerKey: "aSyncer", Status: usersync.StatusBlockedByPrivacy}},
  1633  			setExpectations: func(m *metrics.MetricsEngineMock) {
  1634  				m.On("RecordSyncerRequest", "aSyncer", metrics.SyncerCookieSyncPrivacyBlocked).Once()
  1635  			},
  1636  		},
  1637  		{
  1638  			description: "One - Already Synced",
  1639  			given:       []usersync.BidderEvaluation{{Bidder: "a", SyncerKey: "aSyncer", Status: usersync.StatusAlreadySynced}},
  1640  			setExpectations: func(m *metrics.MetricsEngineMock) {
  1641  				m.On("RecordSyncerRequest", "aSyncer", metrics.SyncerCookieSyncAlreadySynced).Once()
  1642  			},
  1643  		},
  1644  		{
  1645  			description: "One - Type Not Supported",
  1646  			given:       []usersync.BidderEvaluation{{Bidder: "a", SyncerKey: "aSyncer", Status: usersync.StatusTypeNotSupported}},
  1647  			setExpectations: func(m *metrics.MetricsEngineMock) {
  1648  				m.On("RecordSyncerRequest", "aSyncer", metrics.SyncerCookieSyncTypeNotSupported).Once()
  1649  			},
  1650  		},
  1651  		{
  1652  			description: "Many",
  1653  			given: []usersync.BidderEvaluation{
  1654  				{Bidder: "a", SyncerKey: "aSyncer", Status: usersync.StatusOK},
  1655  				{Bidder: "b", SyncerKey: "bSyncer", Status: usersync.StatusAlreadySynced},
  1656  			},
  1657  			setExpectations: func(m *metrics.MetricsEngineMock) {
  1658  				m.On("RecordSyncerRequest", "aSyncer", metrics.SyncerCookieSyncOK).Once()
  1659  				m.On("RecordSyncerRequest", "bSyncer", metrics.SyncerCookieSyncAlreadySynced).Once()
  1660  			},
  1661  		},
  1662  	}
  1663  
  1664  	for _, test := range testCases {
  1665  		mockMetrics := metrics.MetricsEngineMock{}
  1666  		test.setExpectations(&mockMetrics)
  1667  
  1668  		endpoint := &cookieSyncEndpoint{metrics: &mockMetrics}
  1669  		endpoint.writeSyncerMetrics(test.given)
  1670  
  1671  		mockMetrics.AssertExpectations(t)
  1672  	}
  1673  }
  1674  
  1675  func TestCookieSyncHandleResponse(t *testing.T) {
  1676  	syncTypeFilter := usersync.SyncTypeFilter{
  1677  		IFrame:   usersync.NewUniformBidderFilter(usersync.BidderFilterModeExclude),
  1678  		Redirect: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
  1679  	}
  1680  	syncTypeExpected := []usersync.SyncType{usersync.SyncTypeRedirect}
  1681  	privacyMacros := macros.UserSyncPrivacy{USPrivacy: "anyConsent"}
  1682  
  1683  	// The & in the URL is necessary to test proper JSON encoding.
  1684  	syncA := usersync.Sync{URL: "https://syncA.com/sync?a=1&b=2", Type: usersync.SyncTypeRedirect, SupportCORS: true}
  1685  	syncerA := MockSyncer{}
  1686  	syncerA.On("GetSync", syncTypeExpected, privacyMacros).Return(syncA, nil).Maybe()
  1687  
  1688  	// The & in the URL is necessary to test proper JSON encoding.
  1689  	syncB := usersync.Sync{URL: "https://syncB.com/sync?a=1&b=2", Type: usersync.SyncTypeRedirect, SupportCORS: false}
  1690  	syncerB := MockSyncer{}
  1691  	syncerB.On("GetSync", syncTypeExpected, privacyMacros).Return(syncB, nil).Maybe()
  1692  
  1693  	syncWithError := usersync.Sync{}
  1694  	syncerWithError := MockSyncer{}
  1695  	syncerWithError.On("GetSync", syncTypeExpected, privacyMacros).Return(syncWithError, errors.New("anyError")).Maybe()
  1696  
  1697  	bidderEvalForDebug := []usersync.BidderEvaluation{
  1698  		{Bidder: "Bidder1", Status: usersync.StatusAlreadySynced},
  1699  		{Bidder: "Bidder2", Status: usersync.StatusUnknownBidder},
  1700  		{Bidder: "Bidder3", Status: usersync.StatusUnconfiguredBidder},
  1701  		{Bidder: "Bidder4", Status: usersync.StatusBlockedByPrivacy},
  1702  		{Bidder: "Bidder5", Status: usersync.StatusTypeNotSupported},
  1703  		{Bidder: "Bidder6", Status: usersync.StatusBlockedByUserOptOut},
  1704  		{Bidder: "Bidder7", Status: usersync.StatusBlockedByDisabledUsersync},
  1705  		{Bidder: "BidderA", Status: usersync.StatusDuplicate, SyncerKey: "syncerB"},
  1706  	}
  1707  
  1708  	testCases := []struct {
  1709  		description         string
  1710  		givenCookieHasSyncs bool
  1711  		givenSyncersChosen  []usersync.SyncerChoice
  1712  		givenDebug          bool
  1713  		expectedJSON        string
  1714  		expectedAnalytics   analytics.CookieSyncObject
  1715  	}{
  1716  		{
  1717  			description:         "None",
  1718  			givenCookieHasSyncs: true,
  1719  			givenSyncersChosen:  []usersync.SyncerChoice{},
  1720  			expectedJSON:        `{"status":"ok","bidder_status":[]}` + "\n",
  1721  			expectedAnalytics:   analytics.CookieSyncObject{Status: 200, BidderStatus: []*analytics.CookieSyncBidder{}},
  1722  		},
  1723  		{
  1724  			description:         "One",
  1725  			givenCookieHasSyncs: true,
  1726  			givenSyncersChosen:  []usersync.SyncerChoice{{Bidder: "foo", Syncer: &syncerA}},
  1727  			expectedJSON: `{"status":"ok","bidder_status":[` +
  1728  				`{"bidder":"foo","no_cookie":true,"usersync":{"url":"https://syncA.com/sync?a=1&b=2","type":"redirect","supportCORS":true}}` +
  1729  				`]}` + "\n",
  1730  			expectedAnalytics: analytics.CookieSyncObject{
  1731  				Status: 200,
  1732  				BidderStatus: []*analytics.CookieSyncBidder{
  1733  					{
  1734  						BidderCode:   "foo",
  1735  						NoCookie:     true,
  1736  						UsersyncInfo: &analytics.UsersyncInfo{URL: "https://syncA.com/sync?a=1&b=2", Type: "redirect", SupportCORS: true},
  1737  					},
  1738  				},
  1739  			},
  1740  		},
  1741  		{
  1742  			description:         "Many",
  1743  			givenCookieHasSyncs: true,
  1744  			givenSyncersChosen:  []usersync.SyncerChoice{{Bidder: "foo", Syncer: &syncerA}, {Bidder: "bar", Syncer: &syncerB}},
  1745  			expectedJSON: `{"status":"ok","bidder_status":[` +
  1746  				`{"bidder":"foo","no_cookie":true,"usersync":{"url":"https://syncA.com/sync?a=1&b=2","type":"redirect","supportCORS":true}},` +
  1747  				`{"bidder":"bar","no_cookie":true,"usersync":{"url":"https://syncB.com/sync?a=1&b=2","type":"redirect"}}` +
  1748  				`]}` + "\n",
  1749  			expectedAnalytics: analytics.CookieSyncObject{
  1750  				Status: 200,
  1751  				BidderStatus: []*analytics.CookieSyncBidder{
  1752  					{
  1753  						BidderCode:   "foo",
  1754  						NoCookie:     true,
  1755  						UsersyncInfo: &analytics.UsersyncInfo{URL: "https://syncA.com/sync?a=1&b=2", Type: "redirect", SupportCORS: true},
  1756  					},
  1757  					{
  1758  						BidderCode:   "bar",
  1759  						NoCookie:     true,
  1760  						UsersyncInfo: &analytics.UsersyncInfo{URL: "https://syncB.com/sync?a=1&b=2", Type: "redirect", SupportCORS: false},
  1761  					},
  1762  				},
  1763  			},
  1764  		},
  1765  		{
  1766  			description:         "Many With One GetSync Error",
  1767  			givenCookieHasSyncs: true,
  1768  			givenSyncersChosen:  []usersync.SyncerChoice{{Bidder: "foo", Syncer: &syncerWithError}, {Bidder: "bar", Syncer: &syncerB}},
  1769  			expectedJSON: `{"status":"ok","bidder_status":[` +
  1770  				`{"bidder":"bar","no_cookie":true,"usersync":{"url":"https://syncB.com/sync?a=1&b=2","type":"redirect"}}` +
  1771  				`]}` + "\n",
  1772  			expectedAnalytics: analytics.CookieSyncObject{
  1773  				Status: 200,
  1774  				BidderStatus: []*analytics.CookieSyncBidder{
  1775  					{
  1776  						BidderCode:   "bar",
  1777  						NoCookie:     true,
  1778  						UsersyncInfo: &analytics.UsersyncInfo{URL: "https://syncB.com/sync?a=1&b=2", Type: "redirect", SupportCORS: false},
  1779  					},
  1780  				},
  1781  			},
  1782  		},
  1783  		{
  1784  			description:         "No Existing Syncs",
  1785  			givenCookieHasSyncs: false,
  1786  			givenSyncersChosen:  []usersync.SyncerChoice{},
  1787  			expectedJSON:        `{"status":"no_cookie","bidder_status":[]}` + "\n",
  1788  			expectedAnalytics:   analytics.CookieSyncObject{Status: 200, BidderStatus: []*analytics.CookieSyncBidder{}},
  1789  		},
  1790  		{
  1791  			description:         "Debug is true, should see all rejected bidder eval statuses in response",
  1792  			givenCookieHasSyncs: true,
  1793  			givenDebug:          true,
  1794  			givenSyncersChosen:  []usersync.SyncerChoice{},
  1795  			expectedJSON:        `{"status":"ok","bidder_status":[],"debug":[{"bidder":"Bidder1","error":"Already in sync"},{"bidder":"Bidder2","error":"Unsupported bidder"},{"bidder":"Bidder3","error":"No sync config"},{"bidder":"Bidder4","error":"Rejected by privacy"},{"bidder":"Bidder5","error":"Type not supported"},{"bidder":"Bidder6","error":"Status blocked by user opt out"},{"bidder":"Bidder7","error":"Sync disabled by config"},{"bidder":"BidderA","error":"Duplicate bidder synced as syncerB"}]}` + "\n",
  1796  			expectedAnalytics:   analytics.CookieSyncObject{Status: 200, BidderStatus: []*analytics.CookieSyncBidder{}},
  1797  		},
  1798  	}
  1799  
  1800  	for _, test := range testCases {
  1801  		mockAnalytics := MockAnalyticsRunner{}
  1802  		mockAnalytics.On("LogCookieSyncObject", &test.expectedAnalytics).Once()
  1803  
  1804  		cookie := usersync.NewCookie()
  1805  		if test.givenCookieHasSyncs {
  1806  			if err := cookie.Sync("foo", "anyID"); err != nil {
  1807  				assert.FailNow(t, test.description+":set_cookie")
  1808  			}
  1809  		}
  1810  
  1811  		writer := httptest.NewRecorder()
  1812  		endpoint := cookieSyncEndpoint{pbsAnalytics: &mockAnalytics}
  1813  
  1814  		var bidderEval []usersync.BidderEvaluation
  1815  		if test.givenDebug {
  1816  			bidderEval = bidderEvalForDebug
  1817  		} else {
  1818  			bidderEval = []usersync.BidderEvaluation{}
  1819  		}
  1820  		endpoint.handleResponse(writer, syncTypeFilter, cookie, privacyMacros, test.givenSyncersChosen, bidderEval, test.givenDebug)
  1821  
  1822  		if assert.Equal(t, writer.Code, http.StatusOK, test.description+":http_status") {
  1823  			assert.Equal(t, writer.Header().Get("Content-Type"), "application/json; charset=utf-8", test.description+":http_header")
  1824  			assert.Equal(t, test.expectedJSON, writer.Body.String(), test.description+":http_response")
  1825  		}
  1826  		mockAnalytics.AssertExpectations(t)
  1827  	}
  1828  }
  1829  
  1830  func TestMapBidderStatusToAnalytics(t *testing.T) {
  1831  	testCases := []struct {
  1832  		description string
  1833  		given       []cookieSyncResponseBidder
  1834  		expected    []*analytics.CookieSyncBidder
  1835  	}{
  1836  		{
  1837  			description: "None",
  1838  			given:       []cookieSyncResponseBidder{},
  1839  			expected:    []*analytics.CookieSyncBidder{},
  1840  		},
  1841  		{
  1842  			description: "One",
  1843  			given: []cookieSyncResponseBidder{
  1844  				{
  1845  					BidderCode:   "a",
  1846  					NoCookie:     true,
  1847  					UsersyncInfo: cookieSyncResponseSync{URL: "aURL", Type: "aType", SupportCORS: false},
  1848  				},
  1849  			},
  1850  			expected: []*analytics.CookieSyncBidder{
  1851  				{
  1852  					BidderCode:   "a",
  1853  					NoCookie:     true,
  1854  					UsersyncInfo: &analytics.UsersyncInfo{URL: "aURL", Type: "aType", SupportCORS: false},
  1855  				},
  1856  			},
  1857  		},
  1858  		{
  1859  			description: "Many",
  1860  			given: []cookieSyncResponseBidder{
  1861  				{
  1862  					BidderCode:   "a",
  1863  					NoCookie:     true,
  1864  					UsersyncInfo: cookieSyncResponseSync{URL: "aURL", Type: "aType", SupportCORS: false},
  1865  				},
  1866  				{
  1867  					BidderCode:   "b",
  1868  					NoCookie:     false,
  1869  					UsersyncInfo: cookieSyncResponseSync{URL: "bURL", Type: "bType", SupportCORS: true},
  1870  				},
  1871  			},
  1872  			expected: []*analytics.CookieSyncBidder{
  1873  				{
  1874  					BidderCode:   "a",
  1875  					NoCookie:     true,
  1876  					UsersyncInfo: &analytics.UsersyncInfo{URL: "aURL", Type: "aType", SupportCORS: false},
  1877  				},
  1878  				{
  1879  					BidderCode:   "b",
  1880  					NoCookie:     false,
  1881  					UsersyncInfo: &analytics.UsersyncInfo{URL: "bURL", Type: "bType", SupportCORS: true},
  1882  				},
  1883  			},
  1884  		},
  1885  	}
  1886  
  1887  	for _, test := range testCases {
  1888  		result := mapBidderStatusToAnalytics(test.given)
  1889  		assert.ElementsMatch(t, test.expected, result, test.description)
  1890  	}
  1891  }
  1892  
  1893  func TestUsersyncPrivacyGDPRAllowsHostCookie(t *testing.T) {
  1894  	testCases := []struct {
  1895  		description   string
  1896  		givenResponse bool
  1897  		givenError    error
  1898  		expected      bool
  1899  	}{
  1900  		{
  1901  			description:   "Allowed - No Error",
  1902  			givenResponse: true,
  1903  			givenError:    nil,
  1904  			expected:      true,
  1905  		},
  1906  		{
  1907  			description:   "Allowed - Error",
  1908  			givenResponse: true,
  1909  			givenError:    errors.New("anyError"),
  1910  			expected:      false,
  1911  		},
  1912  		{
  1913  			description:   "Not Allowed - No Error",
  1914  			givenResponse: false,
  1915  			givenError:    nil,
  1916  			expected:      false,
  1917  		},
  1918  		{
  1919  			description:   "Not Allowed - Error",
  1920  			givenResponse: false,
  1921  			givenError:    errors.New("anyError"),
  1922  			expected:      false,
  1923  		},
  1924  	}
  1925  
  1926  	for _, test := range testCases {
  1927  		mockPerms := MockGDPRPerms{}
  1928  		mockPerms.On("HostCookiesAllowed", mock.Anything).Return(test.givenResponse, test.givenError)
  1929  
  1930  		privacy := usersyncPrivacy{
  1931  			gdprPermissions: &mockPerms,
  1932  		}
  1933  
  1934  		result := privacy.GDPRAllowsHostCookie()
  1935  		assert.Equal(t, test.expected, result, test.description)
  1936  	}
  1937  }
  1938  
  1939  func TestUsersyncPrivacyGDPRAllowsBidderSync(t *testing.T) {
  1940  	testCases := []struct {
  1941  		description   string
  1942  		givenResponse bool
  1943  		givenError    error
  1944  		expected      bool
  1945  	}{
  1946  		{
  1947  			description:   "Allowed - No Error",
  1948  			givenResponse: true,
  1949  			givenError:    nil,
  1950  			expected:      true,
  1951  		},
  1952  		{
  1953  			description:   "Allowed - Error",
  1954  			givenResponse: true,
  1955  			givenError:    errors.New("anyError"),
  1956  			expected:      false,
  1957  		},
  1958  		{
  1959  			description:   "Not Allowed - No Error",
  1960  			givenResponse: false,
  1961  			givenError:    nil,
  1962  			expected:      false,
  1963  		},
  1964  		{
  1965  			description:   "Not Allowed - Error",
  1966  			givenResponse: false,
  1967  			givenError:    errors.New("anyError"),
  1968  			expected:      false,
  1969  		},
  1970  	}
  1971  
  1972  	for _, test := range testCases {
  1973  		mockPerms := MockGDPRPerms{}
  1974  		mockPerms.On("BidderSyncAllowed", mock.Anything, openrtb_ext.BidderName("foo")).Return(test.givenResponse, test.givenError)
  1975  
  1976  		privacy := usersyncPrivacy{
  1977  			gdprPermissions: &mockPerms,
  1978  		}
  1979  
  1980  		result := privacy.GDPRAllowsBidderSync("foo")
  1981  		assert.Equal(t, test.expected, result, test.description)
  1982  	}
  1983  }
  1984  
  1985  func TestUsersyncPrivacyCCPAAllowsBidderSync(t *testing.T) {
  1986  	testCases := []struct {
  1987  		description  string
  1988  		givenConsent string
  1989  		expected     bool
  1990  	}{
  1991  		{
  1992  			description:  "Allowed - No Opt-Out",
  1993  			givenConsent: "1NNN",
  1994  			expected:     true,
  1995  		},
  1996  		{
  1997  			description:  "Not Allowed - Opt-Out",
  1998  			givenConsent: "1NYN",
  1999  			expected:     false,
  2000  		},
  2001  		{
  2002  			description:  "Not Specified",
  2003  			givenConsent: "",
  2004  			expected:     true,
  2005  		},
  2006  	}
  2007  
  2008  	for _, test := range testCases {
  2009  		validBidders := map[string]struct{}{"foo": {}}
  2010  		parsedPolicy, err := ccpa.Policy{Consent: test.givenConsent}.Parse(validBidders)
  2011  
  2012  		if assert.NoError(t, err) {
  2013  			privacy := usersyncPrivacy{ccpaParsedPolicy: parsedPolicy}
  2014  			result := privacy.CCPAAllowsBidderSync("foo")
  2015  			assert.Equal(t, test.expected, result, test.description)
  2016  		}
  2017  	}
  2018  }
  2019  
  2020  func TestCookieSyncActivityControlIntegration(t *testing.T) {
  2021  	testCases := []struct {
  2022  		name           string
  2023  		bidderName     string
  2024  		accountPrivacy *config.AccountPrivacy
  2025  		expectedResult bool
  2026  	}{
  2027  		{
  2028  			name:           "activity_is_allowed",
  2029  			bidderName:     "bidderA",
  2030  			accountPrivacy: getDefaultActivityConfig("bidderA", true),
  2031  			expectedResult: true,
  2032  		},
  2033  		{
  2034  			name:           "activity_is_denied",
  2035  			bidderName:     "bidderA",
  2036  			accountPrivacy: getDefaultActivityConfig("bidderA", false),
  2037  			expectedResult: false,
  2038  		},
  2039  		{
  2040  			name:           "activity_is_abstain",
  2041  			bidderName:     "bidderA",
  2042  			accountPrivacy: nil,
  2043  			expectedResult: true,
  2044  		},
  2045  	}
  2046  
  2047  	for _, test := range testCases {
  2048  		t.Run(test.name, func(t *testing.T) {
  2049  			activities := privacy.NewActivityControl(test.accountPrivacy)
  2050  			up := usersyncPrivacy{
  2051  				activityControl: activities,
  2052  			}
  2053  			actualResult := up.ActivityAllowsUserSync(test.bidderName)
  2054  			assert.Equal(t, test.expectedResult, actualResult)
  2055  		})
  2056  	}
  2057  }
  2058  
  2059  func TestUsersyncPrivacyGDPRInScope(t *testing.T) {
  2060  	testCases := []struct {
  2061  		description     string
  2062  		givenGdprSignal gdpr.Signal
  2063  		expected        bool
  2064  	}{
  2065  		{
  2066  			description:     "GDPR Signal Yes",
  2067  			givenGdprSignal: gdpr.SignalYes,
  2068  			expected:        true,
  2069  		},
  2070  		{
  2071  			description:     "GDPR Signal No",
  2072  			givenGdprSignal: gdpr.SignalNo,
  2073  			expected:        false,
  2074  		},
  2075  		{
  2076  			description:     "GDPR Signal Ambigious",
  2077  			givenGdprSignal: gdpr.SignalAmbiguous,
  2078  			expected:        false,
  2079  		},
  2080  	}
  2081  
  2082  	for _, test := range testCases {
  2083  		privacy := usersyncPrivacy{
  2084  			gdprSignal: test.givenGdprSignal,
  2085  		}
  2086  
  2087  		result := privacy.GDPRInScope()
  2088  		assert.Equal(t, test.expected, result, test.description)
  2089  	}
  2090  }
  2091  
  2092  func TestCombineErrors(t *testing.T) {
  2093  	testCases := []struct {
  2094  		description    string
  2095  		givenErrorList []error
  2096  		expectedError  error
  2097  	}{
  2098  		{
  2099  			description:    "No errors given",
  2100  			givenErrorList: []error{},
  2101  			expectedError:  errors.New(""),
  2102  		},
  2103  		{
  2104  			description:    "One error given",
  2105  			givenErrorList: []error{errors.New("Error #1")},
  2106  			expectedError:  errors.New("Error #1"),
  2107  		},
  2108  		{
  2109  			description:    "Multiple errors given",
  2110  			givenErrorList: []error{errors.New("Error #1"), errors.New("Error #2")},
  2111  			expectedError:  errors.New("Error #1 Error #2"),
  2112  		},
  2113  		{
  2114  			description:    "Special Case: blocked (rejected via block list)",
  2115  			givenErrorList: []error{&errortypes.AccountDisabled{}},
  2116  			expectedError:  errCookieSyncAccountBlocked,
  2117  		},
  2118  		{
  2119  			description:    "Special Case: invalid (rejected via allow list)",
  2120  			givenErrorList: []error{&errortypes.AcctRequired{}},
  2121  			expectedError:  errCookieSyncAccountInvalid,
  2122  		},
  2123  		{
  2124  			description:    "Special Case: malformed account config",
  2125  			givenErrorList: []error{&errortypes.MalformedAcct{}},
  2126  			expectedError:  errCookieSyncAccountConfigMalformed,
  2127  		},
  2128  		{
  2129  			description:    "Special Case: multiple special cases, first one wins",
  2130  			givenErrorList: []error{&errortypes.AccountDisabled{}, &errortypes.AcctRequired{}, &errortypes.MalformedAcct{}},
  2131  			expectedError:  errCookieSyncAccountBlocked,
  2132  		},
  2133  	}
  2134  
  2135  	for _, test := range testCases {
  2136  		combinedErrors := combineErrors(test.givenErrorList)
  2137  		assert.Equal(t, test.expectedError, combinedErrors, test.description)
  2138  	}
  2139  }
  2140  
  2141  type FakeChooser struct {
  2142  	Result usersync.Result
  2143  }
  2144  
  2145  func (c FakeChooser) Choose(request usersync.Request, cookie *usersync.Cookie) usersync.Result {
  2146  	return c.Result
  2147  }
  2148  
  2149  type MockSyncer struct {
  2150  	mock.Mock
  2151  }
  2152  
  2153  func (m *MockSyncer) Key() string {
  2154  	args := m.Called()
  2155  	return args.String(0)
  2156  }
  2157  
  2158  func (m *MockSyncer) DefaultResponseFormat() usersync.SyncType {
  2159  	args := m.Called()
  2160  	return args.Get(0).(usersync.SyncType)
  2161  }
  2162  
  2163  func (m *MockSyncer) SupportsType(syncTypes []usersync.SyncType) bool {
  2164  	args := m.Called(syncTypes)
  2165  	return args.Bool(0)
  2166  }
  2167  
  2168  func (m *MockSyncer) GetSync(syncTypes []usersync.SyncType, privacyMacros macros.UserSyncPrivacy) (usersync.Sync, error) {
  2169  	args := m.Called(syncTypes, privacyMacros)
  2170  	return args.Get(0).(usersync.Sync), args.Error(1)
  2171  }
  2172  
  2173  type MockAnalyticsRunner struct {
  2174  	mock.Mock
  2175  }
  2176  
  2177  func (m *MockAnalyticsRunner) LogAuctionObject(obj *analytics.AuctionObject, ac privacy.ActivityControl) {
  2178  	m.Called(obj, ac)
  2179  }
  2180  
  2181  func (m *MockAnalyticsRunner) LogVideoObject(obj *analytics.VideoObject, ac privacy.ActivityControl) {
  2182  	m.Called(obj, ac)
  2183  }
  2184  
  2185  func (m *MockAnalyticsRunner) LogCookieSyncObject(obj *analytics.CookieSyncObject) {
  2186  	m.Called(obj)
  2187  }
  2188  
  2189  func (m *MockAnalyticsRunner) LogSetUIDObject(obj *analytics.SetUIDObject) {
  2190  	m.Called(obj)
  2191  }
  2192  
  2193  func (m *MockAnalyticsRunner) LogAmpObject(obj *analytics.AmpObject, ac privacy.ActivityControl) {
  2194  	m.Called(obj, ac)
  2195  }
  2196  
  2197  func (m *MockAnalyticsRunner) LogNotificationEventObject(obj *analytics.NotificationEvent, ac privacy.ActivityControl) {
  2198  	m.Called(obj, ac)
  2199  }
  2200  
  2201  type MockGDPRPerms struct {
  2202  	mock.Mock
  2203  }
  2204  
  2205  func (m *MockGDPRPerms) HostCookiesAllowed(ctx context.Context) (bool, error) {
  2206  	args := m.Called(ctx)
  2207  	return args.Bool(0), args.Error(1)
  2208  }
  2209  
  2210  func (m *MockGDPRPerms) BidderSyncAllowed(ctx context.Context, bidder openrtb_ext.BidderName) (bool, error) {
  2211  	args := m.Called(ctx, bidder)
  2212  	return args.Bool(0), args.Error(1)
  2213  }
  2214  
  2215  func (m *MockGDPRPerms) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) (permissions gdpr.AuctionPermissions, err error) {
  2216  	args := m.Called(ctx, bidderCoreName, bidder)
  2217  	return args.Get(0).(gdpr.AuctionPermissions), args.Error(1)
  2218  }
  2219  
  2220  type FakeAccountsFetcher struct {
  2221  	AccountData map[string]json.RawMessage
  2222  }
  2223  
  2224  func (f FakeAccountsFetcher) FetchAccount(ctx context.Context, _ json.RawMessage, accountID string) (json.RawMessage, []error) {
  2225  	defaultAccountJSON := json.RawMessage(`{"disabled":false}`)
  2226  
  2227  	if accountID == metrics.PublisherUnknown {
  2228  		return defaultAccountJSON, nil
  2229  	}
  2230  	if account, ok := f.AccountData[accountID]; ok {
  2231  		return account, nil
  2232  	}
  2233  	return nil, []error{errors.New("Account not found")}
  2234  }
  2235  
  2236  type fakePermissions struct {
  2237  }
  2238  
  2239  func (p *fakePermissions) HostCookiesAllowed(ctx context.Context) (bool, error) {
  2240  	return true, nil
  2241  }
  2242  
  2243  func (p *fakePermissions) BidderSyncAllowed(ctx context.Context, bidder openrtb_ext.BidderName) (bool, error) {
  2244  	return true, nil
  2245  }
  2246  
  2247  func (p *fakePermissions) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) (permissions gdpr.AuctionPermissions, err error) {
  2248  	return gdpr.AuctionPermissions{
  2249  		AllowBidRequest: true,
  2250  	}, nil
  2251  }
  2252  
  2253  func getDefaultActivityConfig(componentName string, allow bool) *config.AccountPrivacy {
  2254  	return &config.AccountPrivacy{
  2255  		AllowActivities: &config.AllowActivities{
  2256  			SyncUser: config.Activity{
  2257  				Default: ptrutil.ToPtr(true),
  2258  				Rules: []config.ActivityRule{
  2259  					{
  2260  						Allow: allow,
  2261  						Condition: config.ActivityCondition{
  2262  							ComponentName: []string{componentName},
  2263  							ComponentType: []string{"bidder"},
  2264  						},
  2265  					},
  2266  				},
  2267  			},
  2268  		},
  2269  	}
  2270  }
  2271  
  2272  func TestSetCookieDeprecationHeader(t *testing.T) {
  2273  	getTestRequest := func(addCookie bool) *http.Request {
  2274  		r := httptest.NewRequest("POST", "/cookie_sync", nil)
  2275  		if addCookie {
  2276  			r.AddCookie(&http.Cookie{Name: receiveCookieDeprecation, Value: "1"})
  2277  		}
  2278  		return r
  2279  	}
  2280  
  2281  	tests := []struct {
  2282  		name                            string
  2283  		responseWriter                  http.ResponseWriter
  2284  		request                         *http.Request
  2285  		account                         *config.Account
  2286  		expectedCookieDeprecationHeader bool
  2287  	}{
  2288  		{
  2289  			name:                            "not-present-account-nil",
  2290  			request:                         getTestRequest(false),
  2291  			responseWriter:                  httptest.NewRecorder(),
  2292  			account:                         nil,
  2293  			expectedCookieDeprecationHeader: false,
  2294  		},
  2295  		{
  2296  			name:           "not-present-cookiedeprecation-disabled",
  2297  			request:        getTestRequest(false),
  2298  			responseWriter: httptest.NewRecorder(),
  2299  			account: &config.Account{
  2300  				Privacy: config.AccountPrivacy{
  2301  					PrivacySandbox: config.PrivacySandbox{
  2302  						CookieDeprecation: config.CookieDeprecation{
  2303  							Enabled: false,
  2304  						},
  2305  					},
  2306  				},
  2307  			},
  2308  			expectedCookieDeprecationHeader: false,
  2309  		},
  2310  		{
  2311  			name:           "present-cookiedeprecation-disabled",
  2312  			request:        getTestRequest(true),
  2313  			responseWriter: httptest.NewRecorder(),
  2314  			account: &config.Account{
  2315  				Privacy: config.AccountPrivacy{
  2316  					PrivacySandbox: config.PrivacySandbox{
  2317  						CookieDeprecation: config.CookieDeprecation{
  2318  							Enabled: false,
  2319  						},
  2320  					},
  2321  				},
  2322  			},
  2323  			expectedCookieDeprecationHeader: false,
  2324  		},
  2325  		{
  2326  			name:           "present-cookiedeprecation-enabled",
  2327  			request:        getTestRequest(true),
  2328  			responseWriter: httptest.NewRecorder(),
  2329  			account: &config.Account{
  2330  				Privacy: config.AccountPrivacy{
  2331  					PrivacySandbox: config.PrivacySandbox{
  2332  						CookieDeprecation: config.CookieDeprecation{
  2333  							Enabled: true,
  2334  							TTLSec:  86400,
  2335  						},
  2336  					},
  2337  				},
  2338  			},
  2339  
  2340  			expectedCookieDeprecationHeader: false,
  2341  		},
  2342  		{
  2343  			name:                            "present-account-nil",
  2344  			request:                         getTestRequest(true),
  2345  			responseWriter:                  httptest.NewRecorder(),
  2346  			account:                         nil,
  2347  			expectedCookieDeprecationHeader: false,
  2348  		},
  2349  		{
  2350  			name:           "not-present-cookiedeprecation-enabled",
  2351  			request:        getTestRequest(false),
  2352  			responseWriter: httptest.NewRecorder(),
  2353  			account: &config.Account{
  2354  				Privacy: config.AccountPrivacy{
  2355  					PrivacySandbox: config.PrivacySandbox{
  2356  						CookieDeprecation: config.CookieDeprecation{
  2357  							Enabled: true,
  2358  							TTLSec:  86400,
  2359  						},
  2360  					},
  2361  				},
  2362  			},
  2363  			expectedCookieDeprecationHeader: true,
  2364  		},
  2365  		{
  2366  			name:           "failed-to-read-cookiedeprecation-enabled",
  2367  			request:        &http.Request{}, // nil cookie. error: http: named cookie not present
  2368  			responseWriter: httptest.NewRecorder(),
  2369  			account: &config.Account{
  2370  				Privacy: config.AccountPrivacy{
  2371  					PrivacySandbox: config.PrivacySandbox{
  2372  						CookieDeprecation: config.CookieDeprecation{
  2373  							Enabled: true,
  2374  							TTLSec:  86400,
  2375  						},
  2376  					},
  2377  				},
  2378  			},
  2379  			expectedCookieDeprecationHeader: true,
  2380  		},
  2381  	}
  2382  	for _, tt := range tests {
  2383  		t.Run(tt.name, func(t *testing.T) {
  2384  			c := &cookieSyncEndpoint{
  2385  				time: &fakeTime{time: time.Date(2024, 2, 22, 9, 42, 4, 13, time.UTC)},
  2386  			}
  2387  			c.setCookieDeprecationHeader(tt.responseWriter, tt.request, tt.account)
  2388  			gotCookie := tt.responseWriter.Header().Get("Set-Cookie")
  2389  			if tt.expectedCookieDeprecationHeader {
  2390  				wantCookieTTL := c.time.Now().Add(time.Second * time.Duration(86400)).UTC().Format(http.TimeFormat)
  2391  				wantCookie := fmt.Sprintf("receive-cookie-deprecation=1; Path=/; Expires=%v; HttpOnly; Secure; SameSite=None; Partitioned;", wantCookieTTL)
  2392  				assert.Equal(t, wantCookie, gotCookie, ":set_cookie_deprecation_header")
  2393  			} else {
  2394  				assert.Empty(t, gotCookie, ":set_cookie_deprecation_header")
  2395  			}
  2396  		})
  2397  	}
  2398  }