github.com/prebid/prebid-server@v0.275.0/firstpartydata/first_party_data_test.go (about)

     1  package firstpartydata
     2  
     3  import (
     4  	"encoding/json"
     5  	"os"
     6  	"reflect"
     7  	"testing"
     8  
     9  	"github.com/prebid/openrtb/v19/openrtb2"
    10  	"github.com/prebid/prebid-server/errortypes"
    11  	"github.com/prebid/prebid-server/openrtb_ext"
    12  	"github.com/stretchr/testify/assert"
    13  	"github.com/stretchr/testify/require"
    14  )
    15  
    16  func TestExtractGlobalFPD(t *testing.T) {
    17  	testCases := []struct {
    18  		description string
    19  		input       openrtb_ext.RequestWrapper
    20  		expectedReq openrtb_ext.RequestWrapper
    21  		expectedFpd map[string][]byte
    22  	}{
    23  		{
    24  			description: "Site, app and user data present",
    25  			input: openrtb_ext.RequestWrapper{
    26  				BidRequest: &openrtb2.BidRequest{
    27  					ID: "bid_id",
    28  					Site: &openrtb2.Site{
    29  						ID:   "reqSiteId",
    30  						Page: "http://www.foobar.com/1234.html",
    31  						Publisher: &openrtb2.Publisher{
    32  							ID: "1",
    33  						},
    34  						Ext: json.RawMessage(`{"data": {"somesitefpd": "sitefpdDataTest"}}`),
    35  					},
    36  					User: &openrtb2.User{
    37  						ID:     "reqUserID",
    38  						Yob:    1982,
    39  						Gender: "M",
    40  						Ext:    json.RawMessage(`{"data": {"someuserfpd": "userfpdDataTest"}}`),
    41  					},
    42  					App: &openrtb2.App{
    43  						ID:  "appId",
    44  						Ext: json.RawMessage(`{"data": {"someappfpd": "appfpdDataTest"}}`),
    45  					},
    46  				},
    47  			},
    48  			expectedReq: openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{
    49  				ID: "bid_id",
    50  				Site: &openrtb2.Site{
    51  					ID:   "reqSiteId",
    52  					Page: "http://www.foobar.com/1234.html",
    53  					Publisher: &openrtb2.Publisher{
    54  						ID: "1",
    55  					},
    56  				},
    57  				User: &openrtb2.User{
    58  					ID:     "reqUserID",
    59  					Yob:    1982,
    60  					Gender: "M",
    61  				},
    62  				App: &openrtb2.App{
    63  					ID: "appId",
    64  				},
    65  			}},
    66  			expectedFpd: map[string][]byte{
    67  				"site": []byte(`{"somesitefpd": "sitefpdDataTest"}`),
    68  				"user": []byte(`{"someuserfpd": "userfpdDataTest"}`),
    69  				"app":  []byte(`{"someappfpd": "appfpdDataTest"}`),
    70  			},
    71  		},
    72  		{
    73  			description: "App FPD only present",
    74  			input: openrtb_ext.RequestWrapper{
    75  				BidRequest: &openrtb2.BidRequest{
    76  					ID: "bid_id",
    77  					Site: &openrtb2.Site{
    78  						ID:   "reqSiteId",
    79  						Page: "http://www.foobar.com/1234.html",
    80  						Publisher: &openrtb2.Publisher{
    81  							ID: "1",
    82  						},
    83  					},
    84  					App: &openrtb2.App{
    85  						ID:  "appId",
    86  						Ext: json.RawMessage(`{"data": {"someappfpd": "appfpdDataTest"}}`),
    87  					},
    88  				},
    89  			},
    90  			expectedReq: openrtb_ext.RequestWrapper{
    91  				BidRequest: &openrtb2.BidRequest{
    92  					ID: "bid_id",
    93  					Site: &openrtb2.Site{
    94  						ID:   "reqSiteId",
    95  						Page: "http://www.foobar.com/1234.html",
    96  						Publisher: &openrtb2.Publisher{
    97  							ID: "1",
    98  						},
    99  					},
   100  					App: &openrtb2.App{
   101  						ID: "appId",
   102  					},
   103  				},
   104  			},
   105  			expectedFpd: map[string][]byte{
   106  				"app":  []byte(`{"someappfpd": "appfpdDataTest"}`),
   107  				"user": nil,
   108  				"site": nil,
   109  			},
   110  		},
   111  		{
   112  			description: "User FPD only present",
   113  			input: openrtb_ext.RequestWrapper{
   114  				BidRequest: &openrtb2.BidRequest{
   115  					ID: "bid_id",
   116  					Site: &openrtb2.Site{
   117  						ID:   "reqSiteId",
   118  						Page: "http://www.foobar.com/1234.html",
   119  						Publisher: &openrtb2.Publisher{
   120  							ID: "1",
   121  						},
   122  					},
   123  					User: &openrtb2.User{
   124  						ID:     "reqUserID",
   125  						Yob:    1982,
   126  						Gender: "M",
   127  						Ext:    json.RawMessage(`{"data": {"someuserfpd": "userfpdDataTest"}}`),
   128  					},
   129  				},
   130  			},
   131  			expectedReq: openrtb_ext.RequestWrapper{
   132  				BidRequest: &openrtb2.BidRequest{
   133  					ID: "bid_id",
   134  					Site: &openrtb2.Site{
   135  						ID:   "reqSiteId",
   136  						Page: "http://www.foobar.com/1234.html",
   137  						Publisher: &openrtb2.Publisher{
   138  							ID: "1",
   139  						},
   140  					},
   141  					User: &openrtb2.User{
   142  						ID:     "reqUserID",
   143  						Yob:    1982,
   144  						Gender: "M",
   145  					},
   146  				},
   147  			},
   148  			expectedFpd: map[string][]byte{
   149  				"app":  nil,
   150  				"user": []byte(`{"someuserfpd": "userfpdDataTest"}`),
   151  				"site": nil,
   152  			},
   153  		},
   154  		{
   155  			description: "No FPD present in req",
   156  			input: openrtb_ext.RequestWrapper{
   157  				BidRequest: &openrtb2.BidRequest{
   158  					ID: "bid_id",
   159  					Site: &openrtb2.Site{
   160  						ID:   "reqSiteId",
   161  						Page: "http://www.foobar.com/1234.html",
   162  						Publisher: &openrtb2.Publisher{
   163  							ID: "1",
   164  						},
   165  					},
   166  					User: &openrtb2.User{
   167  						ID:     "reqUserID",
   168  						Yob:    1982,
   169  						Gender: "M",
   170  					},
   171  					App: &openrtb2.App{
   172  						ID: "appId",
   173  					},
   174  				},
   175  			},
   176  			expectedReq: openrtb_ext.RequestWrapper{
   177  				BidRequest: &openrtb2.BidRequest{
   178  					ID: "bid_id",
   179  					Site: &openrtb2.Site{
   180  						ID:   "reqSiteId",
   181  						Page: "http://www.foobar.com/1234.html",
   182  						Publisher: &openrtb2.Publisher{
   183  							ID: "1",
   184  						},
   185  					},
   186  					User: &openrtb2.User{
   187  						ID:     "reqUserID",
   188  						Yob:    1982,
   189  						Gender: "M",
   190  					},
   191  					App: &openrtb2.App{
   192  						ID: "appId",
   193  					},
   194  				},
   195  			},
   196  			expectedFpd: map[string][]byte{
   197  				"app":  nil,
   198  				"user": nil,
   199  				"site": nil,
   200  			},
   201  		},
   202  		{
   203  			description: "Site FPD only present",
   204  			input: openrtb_ext.RequestWrapper{
   205  				BidRequest: &openrtb2.BidRequest{
   206  					ID: "bid_id",
   207  					Site: &openrtb2.Site{
   208  						ID:   "reqSiteId",
   209  						Page: "http://www.foobar.com/1234.html",
   210  						Publisher: &openrtb2.Publisher{
   211  							ID: "1",
   212  						},
   213  						Ext: json.RawMessage(`{"data": {"someappfpd": true}}`),
   214  					},
   215  					App: &openrtb2.App{
   216  						ID: "appId",
   217  					},
   218  				},
   219  			},
   220  			expectedReq: openrtb_ext.RequestWrapper{
   221  				BidRequest: &openrtb2.BidRequest{
   222  					ID: "bid_id",
   223  					Site: &openrtb2.Site{
   224  						ID:   "reqSiteId",
   225  						Page: "http://www.foobar.com/1234.html",
   226  						Publisher: &openrtb2.Publisher{
   227  							ID: "1",
   228  						},
   229  					},
   230  					App: &openrtb2.App{
   231  						ID: "appId",
   232  					},
   233  				},
   234  			},
   235  			expectedFpd: map[string][]byte{
   236  				"app":  nil,
   237  				"user": nil,
   238  				"site": []byte(`{"someappfpd": true}`),
   239  			},
   240  		},
   241  	}
   242  	for _, test := range testCases {
   243  
   244  		inputReq := &test.input
   245  		fpd, err := ExtractGlobalFPD(inputReq)
   246  		assert.NoError(t, err, "Error should be nil")
   247  		err = inputReq.RebuildRequest()
   248  		assert.NoError(t, err, "Error should be nil")
   249  
   250  		assert.Equal(t, test.expectedReq.BidRequest, inputReq.BidRequest, "Incorrect input request after global fpd extraction")
   251  
   252  		assert.Equal(t, test.expectedFpd[userKey], fpd[userKey], "Incorrect User FPD")
   253  		assert.Equal(t, test.expectedFpd[appKey], fpd[appKey], "Incorrect App FPD")
   254  		assert.Equal(t, test.expectedFpd[siteKey], fpd[siteKey], "Incorrect Site FPDt")
   255  	}
   256  }
   257  
   258  func TestExtractOpenRtbGlobalFPD(t *testing.T) {
   259  	testCases := []struct {
   260  		description     string
   261  		input           openrtb2.BidRequest
   262  		output          openrtb2.BidRequest
   263  		expectedFpdData map[string][]openrtb2.Data
   264  	}{
   265  		{
   266  			description: "Site, app and user data present",
   267  			input: openrtb2.BidRequest{
   268  				ID: "bid_id",
   269  				Imp: []openrtb2.Imp{
   270  					{ID: "impid"},
   271  				},
   272  				Site: &openrtb2.Site{
   273  					ID: "reqSiteId",
   274  					Content: &openrtb2.Content{
   275  						Data: []openrtb2.Data{
   276  							{ID: "siteDataId1", Name: "siteDataName1"},
   277  							{ID: "siteDataId2", Name: "siteDataName2"},
   278  						},
   279  					},
   280  				},
   281  				User: &openrtb2.User{
   282  					ID:     "reqUserID",
   283  					Yob:    1982,
   284  					Gender: "M",
   285  					Data: []openrtb2.Data{
   286  						{ID: "userDataId1", Name: "userDataName1"},
   287  					},
   288  				},
   289  				App: &openrtb2.App{
   290  					ID: "appId",
   291  					Content: &openrtb2.Content{
   292  						Data: []openrtb2.Data{
   293  							{ID: "appDataId1", Name: "appDataName1"},
   294  						},
   295  					},
   296  				},
   297  			},
   298  			output: openrtb2.BidRequest{
   299  				ID: "bid_id",
   300  				Imp: []openrtb2.Imp{
   301  					{ID: "impid"},
   302  				},
   303  				Site: &openrtb2.Site{
   304  					ID:      "reqSiteId",
   305  					Content: &openrtb2.Content{},
   306  				},
   307  				User: &openrtb2.User{
   308  					ID:     "reqUserID",
   309  					Yob:    1982,
   310  					Gender: "M",
   311  				},
   312  				App: &openrtb2.App{
   313  					ID:      "appId",
   314  					Content: &openrtb2.Content{},
   315  				},
   316  			},
   317  			expectedFpdData: map[string][]openrtb2.Data{
   318  				siteContentDataKey: {{ID: "siteDataId1", Name: "siteDataName1"}, {ID: "siteDataId2", Name: "siteDataName2"}},
   319  				userDataKey:        {{ID: "userDataId1", Name: "userDataName1"}},
   320  				appContentDataKey:  {{ID: "appDataId1", Name: "appDataName1"}},
   321  			},
   322  		},
   323  		{
   324  			description: "No Site, app or user data present",
   325  			input: openrtb2.BidRequest{
   326  				ID: "bid_id",
   327  				Imp: []openrtb2.Imp{
   328  					{ID: "impid"},
   329  				},
   330  			},
   331  			output: openrtb2.BidRequest{
   332  				ID: "bid_id",
   333  				Imp: []openrtb2.Imp{
   334  					{ID: "impid"},
   335  				},
   336  			},
   337  			expectedFpdData: map[string][]openrtb2.Data{
   338  				siteContentDataKey: nil,
   339  				userDataKey:        nil,
   340  				appContentDataKey:  nil,
   341  			},
   342  		},
   343  		{
   344  			description: "Site only data present",
   345  			input: openrtb2.BidRequest{
   346  				ID: "bid_id",
   347  				Imp: []openrtb2.Imp{
   348  					{ID: "impid"},
   349  				},
   350  				Site: &openrtb2.Site{
   351  					ID:   "reqSiteId",
   352  					Page: "test/page",
   353  					Content: &openrtb2.Content{
   354  						Data: []openrtb2.Data{
   355  							{ID: "siteDataId1", Name: "siteDataName1"},
   356  						},
   357  					},
   358  				},
   359  			},
   360  			output: openrtb2.BidRequest{
   361  				ID: "bid_id",
   362  				Imp: []openrtb2.Imp{
   363  					{ID: "impid"},
   364  				},
   365  				Site: &openrtb2.Site{
   366  					ID:      "reqSiteId",
   367  					Page:    "test/page",
   368  					Content: &openrtb2.Content{},
   369  				},
   370  			},
   371  			expectedFpdData: map[string][]openrtb2.Data{
   372  				siteContentDataKey: {{ID: "siteDataId1", Name: "siteDataName1"}},
   373  				userDataKey:        nil,
   374  				appContentDataKey:  nil,
   375  			},
   376  		},
   377  		{
   378  			description: "App only data present",
   379  			input: openrtb2.BidRequest{
   380  				ID: "bid_id",
   381  				Imp: []openrtb2.Imp{
   382  					{ID: "impid"},
   383  				},
   384  				App: &openrtb2.App{
   385  					ID: "reqAppId",
   386  					Content: &openrtb2.Content{
   387  						Data: []openrtb2.Data{
   388  							{ID: "appDataId1", Name: "appDataName1"},
   389  						},
   390  					},
   391  				},
   392  			},
   393  			output: openrtb2.BidRequest{
   394  				ID: "bid_id",
   395  				Imp: []openrtb2.Imp{
   396  					{ID: "impid"},
   397  				},
   398  				App: &openrtb2.App{
   399  					ID:      "reqAppId",
   400  					Content: &openrtb2.Content{},
   401  				},
   402  			},
   403  			expectedFpdData: map[string][]openrtb2.Data{
   404  				siteContentDataKey: nil,
   405  				userDataKey:        nil,
   406  				appContentDataKey:  {{ID: "appDataId1", Name: "appDataName1"}},
   407  			},
   408  		},
   409  		{
   410  			description: "User only data present",
   411  			input: openrtb2.BidRequest{
   412  				ID: "bid_id",
   413  				Imp: []openrtb2.Imp{
   414  					{ID: "impid"},
   415  				},
   416  				Site: &openrtb2.Site{
   417  					ID: "reqSiteId",
   418  				},
   419  				App: &openrtb2.App{
   420  					ID: "reqAppId",
   421  				},
   422  				User: &openrtb2.User{
   423  					ID:     "reqUserId",
   424  					Yob:    1982,
   425  					Gender: "M",
   426  					Data: []openrtb2.Data{
   427  						{ID: "userDataId1", Name: "userDataName1"},
   428  					},
   429  				},
   430  			},
   431  			output: openrtb2.BidRequest{
   432  				ID: "bid_id",
   433  				Imp: []openrtb2.Imp{
   434  					{ID: "impid"},
   435  				},
   436  				Site: &openrtb2.Site{
   437  					ID: "reqSiteId",
   438  				},
   439  				App: &openrtb2.App{
   440  					ID: "reqAppId",
   441  				},
   442  				User: &openrtb2.User{
   443  					ID:     "reqUserId",
   444  					Yob:    1982,
   445  					Gender: "M",
   446  				},
   447  			},
   448  			expectedFpdData: map[string][]openrtb2.Data{
   449  				siteContentDataKey: nil,
   450  				userDataKey:        {{ID: "userDataId1", Name: "userDataName1"}},
   451  				appContentDataKey:  nil,
   452  			},
   453  		},
   454  	}
   455  	for _, test := range testCases {
   456  
   457  		inputReq := &test.input
   458  
   459  		res := ExtractOpenRtbGlobalFPD(inputReq)
   460  
   461  		assert.Equal(t, &test.output, inputReq, "Result request is incorrect")
   462  		assert.Equal(t, test.expectedFpdData[siteContentDataKey], res[siteContentDataKey], "siteContentData data is incorrect")
   463  		assert.Equal(t, test.expectedFpdData[userDataKey], res[userDataKey], "userData is incorrect")
   464  		assert.Equal(t, test.expectedFpdData[appContentDataKey], res[appContentDataKey], "appContentData is incorrect")
   465  
   466  	}
   467  }
   468  
   469  func TestExtractBidderConfigFPD(t *testing.T) {
   470  	testPath := "tests/extractbidderconfigfpd"
   471  
   472  	tests, err := os.ReadDir(testPath)
   473  	require.NoError(t, err, "Cannot Discover Tests")
   474  
   475  	for _, test := range tests {
   476  		t.Run(test.Name(), func(t *testing.T) {
   477  			filePath := testPath + "/" + test.Name()
   478  
   479  			fpdFile, err := loadFpdFile(filePath)
   480  			require.NoError(t, err, "Cannot Load Test")
   481  
   482  			givenRequestExtPrebid := &openrtb_ext.ExtRequestPrebid{}
   483  			err = json.Unmarshal(fpdFile.InputRequestData, givenRequestExtPrebid)
   484  			require.NoError(t, err, "Cannot Load Test Conditions")
   485  
   486  			testRequest := &openrtb_ext.RequestExt{}
   487  			testRequest.SetPrebid(givenRequestExtPrebid)
   488  
   489  			// run test
   490  			results, err := ExtractBidderConfigFPD(testRequest)
   491  
   492  			// assert errors
   493  			if len(fpdFile.ValidationErrors) > 0 {
   494  				require.EqualError(t, err, fpdFile.ValidationErrors[0].Message, "Expected Error Not Received")
   495  			} else {
   496  				require.NoError(t, err, "Error Not Expected")
   497  				assert.Nil(t, testRequest.GetPrebid().BidderConfigs, "Bidder specific FPD config should be removed from request")
   498  			}
   499  
   500  			// assert fpd (with normalization for nicer looking tests)
   501  			for bidderName, expectedFPD := range fpdFile.BidderConfigFPD {
   502  				if expectedFPD.App != nil {
   503  					assert.JSONEq(t, string(expectedFPD.App), string(results[bidderName].App), "app is incorrect")
   504  				} else {
   505  					assert.Nil(t, results[bidderName].App, "app expected to be nil")
   506  				}
   507  
   508  				if expectedFPD.Site != nil {
   509  					assert.JSONEq(t, string(expectedFPD.Site), string(results[bidderName].Site), "site is incorrect")
   510  				} else {
   511  					assert.Nil(t, results[bidderName].Site, "site expected to be nil")
   512  				}
   513  
   514  				if expectedFPD.User != nil {
   515  					assert.JSONEq(t, string(expectedFPD.User), string(results[bidderName].User), "user is incorrect")
   516  				} else {
   517  					assert.Nil(t, results[bidderName].User, "user expected to be nil")
   518  				}
   519  			}
   520  		})
   521  	}
   522  }
   523  
   524  func TestResolveFPD(t *testing.T) {
   525  	testPath := "tests/resolvefpd"
   526  
   527  	tests, err := os.ReadDir(testPath)
   528  	require.NoError(t, err, "Cannot Discover Tests")
   529  
   530  	for _, test := range tests {
   531  		t.Run(test.Name(), func(t *testing.T) {
   532  			filePath := testPath + "/" + test.Name()
   533  
   534  			fpdFile, err := loadFpdFile(filePath)
   535  			require.NoError(t, err, "Cannot Load Test")
   536  
   537  			request := &openrtb2.BidRequest{}
   538  			err = json.Unmarshal(fpdFile.InputRequestData, &request)
   539  			require.NoError(t, err, "Cannot Load Request")
   540  
   541  			originalRequest := &openrtb2.BidRequest{}
   542  			err = json.Unmarshal(fpdFile.InputRequestData, &originalRequest)
   543  			require.NoError(t, err, "Cannot Load Request")
   544  
   545  			outputReq := &openrtb2.BidRequest{}
   546  			err = json.Unmarshal(fpdFile.OutputRequestData, &outputReq)
   547  			require.NoError(t, err, "Cannot Load Output Request")
   548  
   549  			reqExtFPD := make(map[string][]byte)
   550  			reqExtFPD["site"] = fpdFile.GlobalFPD["site"]
   551  			reqExtFPD["app"] = fpdFile.GlobalFPD["app"]
   552  			reqExtFPD["user"] = fpdFile.GlobalFPD["user"]
   553  
   554  			reqFPD := make(map[string][]openrtb2.Data, 3)
   555  
   556  			reqFPDSiteContentData := fpdFile.GlobalFPD[siteContentDataKey]
   557  			if len(reqFPDSiteContentData) > 0 {
   558  				var siteConData []openrtb2.Data
   559  				err = json.Unmarshal(reqFPDSiteContentData, &siteConData)
   560  				if err != nil {
   561  					t.Errorf("Unable to unmarshal site.content.data:")
   562  				}
   563  				reqFPD[siteContentDataKey] = siteConData
   564  			}
   565  
   566  			reqFPDAppContentData := fpdFile.GlobalFPD[appContentDataKey]
   567  			if len(reqFPDAppContentData) > 0 {
   568  				var appConData []openrtb2.Data
   569  				err = json.Unmarshal(reqFPDAppContentData, &appConData)
   570  				if err != nil {
   571  					t.Errorf("Unable to unmarshal app.content.data: ")
   572  				}
   573  				reqFPD[appContentDataKey] = appConData
   574  			}
   575  
   576  			reqFPDUserData := fpdFile.GlobalFPD[userDataKey]
   577  			if len(reqFPDUserData) > 0 {
   578  				var userData []openrtb2.Data
   579  				err = json.Unmarshal(reqFPDUserData, &userData)
   580  				if err != nil {
   581  					t.Errorf("Unable to unmarshal app.content.data: ")
   582  				}
   583  				reqFPD[userDataKey] = userData
   584  			}
   585  			if fpdFile.BidderConfigFPD == nil {
   586  				fpdFile.BidderConfigFPD = make(map[openrtb_ext.BidderName]*openrtb_ext.ORTB2)
   587  				fpdFile.BidderConfigFPD["appnexus"] = &openrtb_ext.ORTB2{}
   588  			}
   589  
   590  			// run test
   591  			resultFPD, errL := ResolveFPD(request, fpdFile.BidderConfigFPD, reqExtFPD, reqFPD, []string{"appnexus"})
   592  
   593  			if len(errL) == 0 {
   594  				assert.Equal(t, request, originalRequest, "Original request should not be modified")
   595  
   596  				bidderFPD := resultFPD["appnexus"]
   597  
   598  				if outputReq.Site != nil && len(outputReq.Site.Ext) > 0 {
   599  					resSiteExt := bidderFPD.Site.Ext
   600  					expectedSiteExt := outputReq.Site.Ext
   601  					bidderFPD.Site.Ext = nil
   602  					outputReq.Site.Ext = nil
   603  					assert.JSONEq(t, string(expectedSiteExt), string(resSiteExt), "site.ext is incorrect")
   604  
   605  					assert.Equal(t, outputReq.Site, bidderFPD.Site, "Site is incorrect")
   606  				}
   607  				if outputReq.App != nil && len(outputReq.App.Ext) > 0 {
   608  					resAppExt := bidderFPD.App.Ext
   609  					expectedAppExt := outputReq.App.Ext
   610  					bidderFPD.App.Ext = nil
   611  					outputReq.App.Ext = nil
   612  
   613  					assert.JSONEq(t, string(expectedAppExt), string(resAppExt), "app.ext is incorrect")
   614  
   615  					assert.Equal(t, outputReq.App, bidderFPD.App, "App is incorrect")
   616  				}
   617  				if outputReq.User != nil && len(outputReq.User.Ext) > 0 {
   618  					resUserExt := bidderFPD.User.Ext
   619  					expectedUserExt := outputReq.User.Ext
   620  					bidderFPD.User.Ext = nil
   621  					outputReq.User.Ext = nil
   622  					assert.JSONEq(t, string(expectedUserExt), string(resUserExt), "user.ext is incorrect")
   623  
   624  					assert.Equal(t, outputReq.User, bidderFPD.User, "User is incorrect")
   625  				}
   626  			} else {
   627  				assert.ElementsMatch(t, errL, fpdFile.ValidationErrors, "Incorrect first party data warning message")
   628  			}
   629  		})
   630  	}
   631  }
   632  
   633  func TestExtractFPDForBidders(t *testing.T) {
   634  	if specFiles, err := os.ReadDir("./tests/extractfpdforbidders"); err == nil {
   635  		for _, specFile := range specFiles {
   636  			fileName := "./tests/extractfpdforbidders/" + specFile.Name()
   637  
   638  			fpdFile, err := loadFpdFile(fileName)
   639  
   640  			if err != nil {
   641  				t.Errorf("Unable to load file: %s", fileName)
   642  			}
   643  
   644  			var expectedRequest openrtb2.BidRequest
   645  			err = json.Unmarshal(fpdFile.OutputRequestData, &expectedRequest)
   646  			if err != nil {
   647  				t.Errorf("Unable to unmarshal input request: %s", fileName)
   648  			}
   649  
   650  			resultRequest := &openrtb_ext.RequestWrapper{}
   651  			resultRequest.BidRequest = &openrtb2.BidRequest{}
   652  			err = json.Unmarshal(fpdFile.InputRequestData, resultRequest.BidRequest)
   653  			assert.NoError(t, err, "Error should be nil")
   654  
   655  			resultFPD, errL := ExtractFPDForBidders(resultRequest)
   656  
   657  			if len(fpdFile.ValidationErrors) > 0 {
   658  				assert.Equal(t, len(fpdFile.ValidationErrors), len(errL), "Incorrect number of errors was returned")
   659  				assert.ElementsMatch(t, errL, fpdFile.ValidationErrors, "Incorrect errors were returned")
   660  				//in case or error no further assertions needed
   661  				continue
   662  			}
   663  			assert.Empty(t, errL, "Error should be empty")
   664  			assert.Equal(t, len(resultFPD), len(fpdFile.BiddersFPDResolved))
   665  
   666  			for bidderName, expectedValue := range fpdFile.BiddersFPDResolved {
   667  				actualValue := resultFPD[bidderName]
   668  				if expectedValue.Site != nil {
   669  					if len(expectedValue.Site.Ext) > 0 {
   670  						assert.JSONEq(t, string(expectedValue.Site.Ext), string(actualValue.Site.Ext), "Incorrect first party data")
   671  						expectedValue.Site.Ext = nil
   672  						actualValue.Site.Ext = nil
   673  					}
   674  					assert.Equal(t, expectedValue.Site, actualValue.Site, "Incorrect first party data")
   675  				}
   676  				if expectedValue.App != nil {
   677  					if len(expectedValue.App.Ext) > 0 {
   678  						assert.JSONEq(t, string(expectedValue.App.Ext), string(actualValue.App.Ext), "Incorrect first party data")
   679  						expectedValue.App.Ext = nil
   680  						actualValue.App.Ext = nil
   681  					}
   682  					assert.Equal(t, expectedValue.App, actualValue.App, "Incorrect first party data")
   683  				}
   684  				if expectedValue.User != nil {
   685  					if len(expectedValue.User.Ext) > 0 {
   686  						assert.JSONEq(t, string(expectedValue.User.Ext), string(actualValue.User.Ext), "Incorrect first party data")
   687  						expectedValue.User.Ext = nil
   688  						actualValue.User.Ext = nil
   689  					}
   690  					assert.Equal(t, expectedValue.User, actualValue.User, "Incorrect first party data")
   691  				}
   692  			}
   693  
   694  			if expectedRequest.Site != nil {
   695  				if len(expectedRequest.Site.Ext) > 0 {
   696  					assert.JSONEq(t, string(expectedRequest.Site.Ext), string(resultRequest.BidRequest.Site.Ext), "Incorrect site in request")
   697  					expectedRequest.Site.Ext = nil
   698  					resultRequest.BidRequest.Site.Ext = nil
   699  				}
   700  				assert.Equal(t, expectedRequest.Site, resultRequest.BidRequest.Site, "Incorrect site in request")
   701  			}
   702  			if expectedRequest.App != nil {
   703  				if len(expectedRequest.App.Ext) > 0 {
   704  					assert.JSONEq(t, string(expectedRequest.App.Ext), string(resultRequest.BidRequest.App.Ext), "Incorrect app in request")
   705  					expectedRequest.App.Ext = nil
   706  					resultRequest.BidRequest.App.Ext = nil
   707  				}
   708  				assert.Equal(t, expectedRequest.App, resultRequest.BidRequest.App, "Incorrect app in request")
   709  			}
   710  			if expectedRequest.User != nil {
   711  				if len(expectedRequest.User.Ext) > 0 {
   712  					assert.JSONEq(t, string(expectedRequest.User.Ext), string(resultRequest.BidRequest.User.Ext), "Incorrect user in request")
   713  					expectedRequest.User.Ext = nil
   714  					resultRequest.BidRequest.User.Ext = nil
   715  				}
   716  				assert.Equal(t, expectedRequest.User, resultRequest.BidRequest.User, "Incorrect user in request")
   717  			}
   718  
   719  		}
   720  	}
   721  }
   722  
   723  func loadFpdFile(filename string) (fpdFile, error) {
   724  	var fileData fpdFile
   725  	fileContents, err := os.ReadFile(filename)
   726  	if err != nil {
   727  		return fileData, err
   728  	}
   729  	err = json.Unmarshal(fileContents, &fileData)
   730  	if err != nil {
   731  		return fileData, err
   732  	}
   733  
   734  	return fileData, nil
   735  }
   736  
   737  type fpdFile struct {
   738  	InputRequestData   json.RawMessage                                    `json:"inputRequestData,omitempty"`
   739  	OutputRequestData  json.RawMessage                                    `json:"outputRequestData,omitempty"`
   740  	BidderConfigFPD    map[openrtb_ext.BidderName]*openrtb_ext.ORTB2      `json:"bidderConfigFPD,omitempty"`
   741  	BiddersFPDResolved map[openrtb_ext.BidderName]*ResolvedFirstPartyData `json:"biddersFPDResolved,omitempty"`
   742  	GlobalFPD          map[string]json.RawMessage                         `json:"globalFPD,omitempty"`
   743  	ValidationErrors   []*errortypes.BadInput                             `json:"validationErrors,omitempty"`
   744  }
   745  
   746  func TestResolveUser(t *testing.T) {
   747  	testCases := []struct {
   748  		description      string
   749  		fpdConfig        *openrtb_ext.ORTB2
   750  		bidRequestUser   *openrtb2.User
   751  		globalFPD        map[string][]byte
   752  		openRtbGlobalFPD map[string][]openrtb2.Data
   753  		expectedUser     *openrtb2.User
   754  		expectedError    string
   755  	}{
   756  		{
   757  			description:  "FPD config and bid request user are not specified",
   758  			expectedUser: nil,
   759  		},
   760  		{
   761  			description:  "FPD config user only is specified",
   762  			fpdConfig:    &openrtb_ext.ORTB2{User: json.RawMessage(`{"id": "test"}`)},
   763  			expectedUser: &openrtb2.User{ID: "test"},
   764  		},
   765  		{
   766  			description:    "FPD config and bid request user are specified",
   767  			fpdConfig:      &openrtb_ext.ORTB2{User: json.RawMessage(`{"id": "test1"}`)},
   768  			bidRequestUser: &openrtb2.User{ID: "test2"},
   769  			expectedUser:   &openrtb2.User{ID: "test1"},
   770  		},
   771  		{
   772  			description:    "FPD config, bid request and global fpd user are specified, no input user ext",
   773  			fpdConfig:      &openrtb_ext.ORTB2{User: json.RawMessage(`{"id": "test1"}`)},
   774  			bidRequestUser: &openrtb2.User{ID: "test2"},
   775  			globalFPD:      map[string][]byte{userKey: []byte(`{"globalFPDUserData": "globalFPDUserDataValue"}`)},
   776  			expectedUser:   &openrtb2.User{ID: "test1", Ext: json.RawMessage(`{"data":{"globalFPDUserData":"globalFPDUserDataValue"}}`)},
   777  		},
   778  		{
   779  			description:    "FPD config, bid request user with ext and global fpd user are specified, no input user ext",
   780  			fpdConfig:      &openrtb_ext.ORTB2{User: json.RawMessage(`{"id": "test1"}`)},
   781  			bidRequestUser: &openrtb2.User{ID: "test2", Ext: json.RawMessage(`{"test":{"inputFPDUserData":"inputFPDUserDataValue"}}`)},
   782  			globalFPD:      map[string][]byte{userKey: []byte(`{"globalFPDUserData": "globalFPDUserDataValue"}`)},
   783  			expectedUser:   &openrtb2.User{ID: "test1", Ext: json.RawMessage(`{"data":{"globalFPDUserData":"globalFPDUserDataValue"},"test":{"inputFPDUserData":"inputFPDUserDataValue"}}`)},
   784  		},
   785  		{
   786  			description:    "FPD config, bid request and global fpd user are specified, with input user ext.data",
   787  			fpdConfig:      &openrtb_ext.ORTB2{User: json.RawMessage(`{"id": "test1"}`)},
   788  			bidRequestUser: &openrtb2.User{ID: "test2", Ext: json.RawMessage(`{"data":{"inputFPDUserData":"inputFPDUserDataValue"}}`)},
   789  			globalFPD:      map[string][]byte{userKey: []byte(`{"globalFPDUserData": "globalFPDUserDataValue"}`)},
   790  			expectedUser:   &openrtb2.User{ID: "test1", Ext: json.RawMessage(`{"data":{"globalFPDUserData":"globalFPDUserDataValue","inputFPDUserData":"inputFPDUserDataValue"}}`)},
   791  		},
   792  		{
   793  			description:    "FPD config, bid request and global fpd user are specified, with input user ext.data malformed",
   794  			fpdConfig:      &openrtb_ext.ORTB2{User: json.RawMessage(`{"id": "test1"}`)},
   795  			bidRequestUser: &openrtb2.User{ID: "test2", Ext: json.RawMessage(`{"data":{"inputFPDUserData":"inputFPDUserDataValue"}}`)},
   796  			globalFPD:      map[string][]byte{userKey: []byte(`malformed`)},
   797  			expectedError:  "Invalid JSON Patch",
   798  		},
   799  		{
   800  			description:    "bid request and openrtb global fpd user are specified, no input user ext",
   801  			bidRequestUser: &openrtb2.User{ID: "test2"},
   802  			openRtbGlobalFPD: map[string][]openrtb2.Data{userDataKey: {
   803  				{ID: "DataId1", Name: "Name1"},
   804  				{ID: "DataId2", Name: "Name2"},
   805  			}},
   806  			expectedUser: &openrtb2.User{ID: "test2", Data: []openrtb2.Data{
   807  				{ID: "DataId1", Name: "Name1"},
   808  				{ID: "DataId2", Name: "Name2"},
   809  			}},
   810  		},
   811  		{
   812  			description:    "fpd config user, bid request and openrtb global fpd user are specified, no input user ext",
   813  			fpdConfig:      &openrtb_ext.ORTB2{User: json.RawMessage(`{"id": "test1"}`)},
   814  			bidRequestUser: &openrtb2.User{ID: "test2"},
   815  			openRtbGlobalFPD: map[string][]openrtb2.Data{userDataKey: {
   816  				{ID: "DataId1", Name: "Name1"},
   817  				{ID: "DataId2", Name: "Name2"},
   818  			}},
   819  			expectedUser: &openrtb2.User{ID: "test1", Data: []openrtb2.Data{
   820  				{ID: "DataId1", Name: "Name1"},
   821  				{ID: "DataId2", Name: "Name2"},
   822  			}},
   823  		},
   824  		{
   825  			description:    "fpd config user with ext, bid request and openrtb global fpd user are specified, no input user ext",
   826  			fpdConfig:      &openrtb_ext.ORTB2{User: json.RawMessage(`{"id": "test1", "ext":{"test":1}}`)},
   827  			bidRequestUser: &openrtb2.User{ID: "test2"},
   828  			openRtbGlobalFPD: map[string][]openrtb2.Data{userDataKey: {
   829  				{ID: "DataId1", Name: "Name1"},
   830  				{ID: "DataId2", Name: "Name2"},
   831  			}},
   832  			expectedUser: &openrtb2.User{ID: "test1", Data: []openrtb2.Data{
   833  				{ID: "DataId1", Name: "Name1"},
   834  				{ID: "DataId2", Name: "Name2"},
   835  			},
   836  				Ext: json.RawMessage(`{"test":1}`)},
   837  		},
   838  		{
   839  			description:    "fpd config user with ext, bid requestuser with ext and openrtb global fpd user are specified, no input user ext",
   840  			fpdConfig:      &openrtb_ext.ORTB2{User: json.RawMessage(`{"id": "test1", "ext":{"test":1}}`)},
   841  			bidRequestUser: &openrtb2.User{ID: "test2", Ext: json.RawMessage(`{"test":2, "key": "value"}`)},
   842  			openRtbGlobalFPD: map[string][]openrtb2.Data{userDataKey: {
   843  				{ID: "DataId1", Name: "Name1"},
   844  				{ID: "DataId2", Name: "Name2"},
   845  			}},
   846  			expectedUser: &openrtb2.User{ID: "test1", Data: []openrtb2.Data{
   847  				{ID: "DataId1", Name: "Name1"},
   848  				{ID: "DataId2", Name: "Name2"},
   849  			},
   850  				Ext: json.RawMessage(`{"key":"value","test":1}`)},
   851  		},
   852  		{
   853  			description:    "fpd config user with malformed ext, bid requestuser with ext and openrtb global fpd user are specified, no input user ext",
   854  			fpdConfig:      &openrtb_ext.ORTB2{User: json.RawMessage(`{"id": "test1", "ext":{malformed}}`)},
   855  			bidRequestUser: &openrtb2.User{ID: "test2", Ext: json.RawMessage(`{"test":2, "key": "value"}`)},
   856  			openRtbGlobalFPD: map[string][]openrtb2.Data{userDataKey: {
   857  				{ID: "DataId1", Name: "Name1"},
   858  				{ID: "DataId2", Name: "Name2"},
   859  			}},
   860  			expectedUser: &openrtb2.User{ID: "test1", Data: []openrtb2.Data{
   861  				{ID: "DataId1", Name: "Name1"},
   862  				{ID: "DataId2", Name: "Name2"},
   863  			},
   864  				Ext: json.RawMessage(`{"key":"value","test":1}`),
   865  			},
   866  			expectedError: "invalid character 'm' looking for beginning of object key string",
   867  		},
   868  	}
   869  	for _, test := range testCases {
   870  		t.Run(test.description, func(t *testing.T) {
   871  			resultUser, err := resolveUser(test.fpdConfig, test.bidRequestUser, test.globalFPD, test.openRtbGlobalFPD, "bidderA")
   872  
   873  			if test.expectedError == "" {
   874  				assert.NoError(t, err, "unexpected error returned")
   875  				assert.Equal(t, test.expectedUser, resultUser, "Result user is incorrect")
   876  			} else {
   877  				assert.EqualError(t, err, test.expectedError, "expected error incorrect")
   878  			}
   879  		})
   880  	}
   881  }
   882  
   883  func TestResolveSite(t *testing.T) {
   884  	testCases := []struct {
   885  		description      string
   886  		fpdConfig        *openrtb_ext.ORTB2
   887  		bidRequestSite   *openrtb2.Site
   888  		globalFPD        map[string][]byte
   889  		openRtbGlobalFPD map[string][]openrtb2.Data
   890  		expectedSite     *openrtb2.Site
   891  		expectedError    string
   892  	}{
   893  		{
   894  			description:  "FPD config and bid request site are not specified",
   895  			expectedSite: nil,
   896  		},
   897  		{
   898  			description:   "FPD config site only is specified",
   899  			fpdConfig:     &openrtb_ext.ORTB2{Site: json.RawMessage(`{"id": "test"}`)},
   900  			expectedError: "incorrect First Party Data for bidder bidderA: Site object is not defined in request, but defined in FPD config",
   901  		},
   902  		{
   903  			description:    "FPD config and bid request site are specified",
   904  			fpdConfig:      &openrtb_ext.ORTB2{Site: json.RawMessage(`{"id": "test1"}`)},
   905  			bidRequestSite: &openrtb2.Site{ID: "test2"},
   906  			expectedSite:   &openrtb2.Site{ID: "test1"},
   907  		},
   908  		{
   909  			description:    "FPD config, bid request and global fpd site are specified, no input site ext",
   910  			fpdConfig:      &openrtb_ext.ORTB2{Site: json.RawMessage(`{"id": "test1"}`)},
   911  			bidRequestSite: &openrtb2.Site{ID: "test2"},
   912  			globalFPD:      map[string][]byte{siteKey: []byte(`{"globalFPDSiteData": "globalFPDSiteDataValue"}`)},
   913  			expectedSite:   &openrtb2.Site{ID: "test1", Ext: json.RawMessage(`{"data":{"globalFPDSiteData":"globalFPDSiteDataValue"}}`)},
   914  		},
   915  		{
   916  			description:    "FPD config, bid request site with ext and global fpd site are specified, no input site ext",
   917  			fpdConfig:      &openrtb_ext.ORTB2{Site: json.RawMessage(`{"id": "test1"}`)},
   918  			bidRequestSite: &openrtb2.Site{ID: "test2", Ext: json.RawMessage(`{"test":{"inputFPDSiteData":"inputFPDSiteDataValue"}}`)},
   919  			globalFPD:      map[string][]byte{siteKey: []byte(`{"globalFPDSiteData": "globalFPDSiteDataValue"}`)},
   920  			expectedSite:   &openrtb2.Site{ID: "test1", Ext: json.RawMessage(`{"data":{"globalFPDSiteData":"globalFPDSiteDataValue"},"test":{"inputFPDSiteData":"inputFPDSiteDataValue"}}`)},
   921  		},
   922  		{
   923  			description:    "FPD config, bid request and global fpd site are specified, with input site ext.data",
   924  			fpdConfig:      &openrtb_ext.ORTB2{Site: json.RawMessage(`{"id": "test1"}`)},
   925  			bidRequestSite: &openrtb2.Site{ID: "test2", Ext: json.RawMessage(`{"data":{"inputFPDSiteData":"inputFPDSiteDataValue"}}`)},
   926  			globalFPD:      map[string][]byte{siteKey: []byte(`{"globalFPDSiteData": "globalFPDSiteDataValue"}`)},
   927  			expectedSite:   &openrtb2.Site{ID: "test1", Ext: json.RawMessage(`{"data":{"globalFPDSiteData":"globalFPDSiteDataValue","inputFPDSiteData":"inputFPDSiteDataValue"}}`)},
   928  		},
   929  		{
   930  			description:    "FPD config, bid request and global fpd site are specified, with input site ext.data malformed",
   931  			fpdConfig:      &openrtb_ext.ORTB2{Site: json.RawMessage(`{"id": "test1"}`)},
   932  			bidRequestSite: &openrtb2.Site{ID: "test2", Ext: json.RawMessage(`{"data":{"inputFPDSiteData":"inputFPDSiteDataValue"}}`)},
   933  			globalFPD:      map[string][]byte{siteKey: []byte(`malformed`)},
   934  			expectedError:  "Invalid JSON Patch",
   935  		},
   936  		{
   937  			description:    "bid request and openrtb global fpd site are specified, no input site ext",
   938  			bidRequestSite: &openrtb2.Site{ID: "test2"},
   939  			openRtbGlobalFPD: map[string][]openrtb2.Data{siteContentDataKey: {
   940  				{ID: "DataId1", Name: "Name1"},
   941  				{ID: "DataId2", Name: "Name2"},
   942  			}},
   943  			expectedSite: &openrtb2.Site{ID: "test2", Content: &openrtb2.Content{Data: []openrtb2.Data{
   944  				{ID: "DataId1", Name: "Name1"},
   945  				{ID: "DataId2", Name: "Name2"},
   946  			}}},
   947  		},
   948  		{
   949  			description: "bid request with content and openrtb global fpd site are specified, no input site ext",
   950  			bidRequestSite: &openrtb2.Site{ID: "test2", Content: &openrtb2.Content{
   951  				ID: "InputSiteContentId",
   952  				Data: []openrtb2.Data{
   953  					{ID: "1", Name: "N1"},
   954  					{ID: "2", Name: "N2"},
   955  				},
   956  				Ext: json.RawMessage(`{"contentPresent":true}`),
   957  			}},
   958  			openRtbGlobalFPD: map[string][]openrtb2.Data{siteContentDataKey: {
   959  				{ID: "DataId1", Name: "Name1"},
   960  				{ID: "DataId2", Name: "Name2"},
   961  			}},
   962  			expectedSite: &openrtb2.Site{ID: "test2", Content: &openrtb2.Content{
   963  				ID: "InputSiteContentId",
   964  				Data: []openrtb2.Data{
   965  					{ID: "DataId1", Name: "Name1"},
   966  					{ID: "DataId2", Name: "Name2"},
   967  				},
   968  				Ext: json.RawMessage(`{"contentPresent":true}`),
   969  			}},
   970  		},
   971  		{
   972  			description:    "fpd config site, bid request and openrtb global fpd site are specified, no input site ext",
   973  			fpdConfig:      &openrtb_ext.ORTB2{Site: json.RawMessage(`{"id": "test1"}`)},
   974  			bidRequestSite: &openrtb2.Site{ID: "test2"},
   975  			openRtbGlobalFPD: map[string][]openrtb2.Data{siteContentDataKey: {
   976  				{ID: "DataId1", Name: "Name1"},
   977  				{ID: "DataId2", Name: "Name2"},
   978  			}},
   979  			expectedSite: &openrtb2.Site{ID: "test1", Content: &openrtb2.Content{Data: []openrtb2.Data{
   980  				{ID: "DataId1", Name: "Name1"},
   981  				{ID: "DataId2", Name: "Name2"},
   982  			}}},
   983  		},
   984  		{
   985  			description:    "fpd config site with ext, bid request and openrtb global fpd site are specified, no input site ext",
   986  			fpdConfig:      &openrtb_ext.ORTB2{Site: json.RawMessage(`{"id": "test1", "ext":{"test":1}}`)},
   987  			bidRequestSite: &openrtb2.Site{ID: "test2"},
   988  			openRtbGlobalFPD: map[string][]openrtb2.Data{siteContentDataKey: {
   989  				{ID: "DataId1", Name: "Name1"},
   990  				{ID: "DataId2", Name: "Name2"},
   991  			}},
   992  			expectedSite: &openrtb2.Site{ID: "test1", Content: &openrtb2.Content{Data: []openrtb2.Data{
   993  				{ID: "DataId1", Name: "Name1"},
   994  				{ID: "DataId2", Name: "Name2"},
   995  			}},
   996  				Ext: json.RawMessage(`{"test":1}`)},
   997  		},
   998  		{
   999  			description:    "fpd config site with ext, bid request site with ext and openrtb global fpd site are specified, no input site ext",
  1000  			fpdConfig:      &openrtb_ext.ORTB2{Site: json.RawMessage(`{"id": "test1", "ext":{"test":1}}`)},
  1001  			bidRequestSite: &openrtb2.Site{ID: "test2", Ext: json.RawMessage(`{"test":2, "key": "value"}`)},
  1002  			openRtbGlobalFPD: map[string][]openrtb2.Data{siteContentDataKey: {
  1003  				{ID: "DataId1", Name: "Name1"},
  1004  				{ID: "DataId2", Name: "Name2"},
  1005  			}},
  1006  			expectedSite: &openrtb2.Site{ID: "test1", Content: &openrtb2.Content{Data: []openrtb2.Data{
  1007  				{ID: "DataId1", Name: "Name1"},
  1008  				{ID: "DataId2", Name: "Name2"},
  1009  			}},
  1010  				Ext: json.RawMessage(`{"key":"value","test":1}`)},
  1011  		},
  1012  		{
  1013  			description:    "fpd config site with malformed ext, bid request site with ext and openrtb global fpd site are specified, no input site ext",
  1014  			fpdConfig:      &openrtb_ext.ORTB2{Site: json.RawMessage(`{"id": "test1", "ext":{malformed}}`)},
  1015  			bidRequestSite: &openrtb2.Site{ID: "test2", Ext: json.RawMessage(`{"test":2, "key": "value"}`)},
  1016  			openRtbGlobalFPD: map[string][]openrtb2.Data{siteContentDataKey: {
  1017  				{ID: "DataId1", Name: "Name1"},
  1018  				{ID: "DataId2", Name: "Name2"},
  1019  			}},
  1020  			expectedSite: &openrtb2.Site{ID: "test1", Content: &openrtb2.Content{Data: []openrtb2.Data{
  1021  				{ID: "DataId1", Name: "Name1"},
  1022  				{ID: "DataId2", Name: "Name2"},
  1023  			}},
  1024  				Ext: json.RawMessage(`{"key":"value","test":1}`),
  1025  			},
  1026  			expectedError: "invalid character 'm' looking for beginning of object key string",
  1027  		},
  1028  	}
  1029  	for _, test := range testCases {
  1030  		t.Run(test.description, func(t *testing.T) {
  1031  			resultSite, err := resolveSite(test.fpdConfig, test.bidRequestSite, test.globalFPD, test.openRtbGlobalFPD, "bidderA")
  1032  
  1033  			if test.expectedError == "" {
  1034  				assert.NoError(t, err, "unexpected error returned")
  1035  				assert.Equal(t, test.expectedSite, resultSite, "Result site is incorrect")
  1036  			} else {
  1037  				assert.EqualError(t, err, test.expectedError, "expected error incorrect")
  1038  			}
  1039  		})
  1040  	}
  1041  }
  1042  
  1043  func TestResolveApp(t *testing.T) {
  1044  	testCases := []struct {
  1045  		description      string
  1046  		fpdConfig        *openrtb_ext.ORTB2
  1047  		bidRequestApp    *openrtb2.App
  1048  		globalFPD        map[string][]byte
  1049  		openRtbGlobalFPD map[string][]openrtb2.Data
  1050  		expectedApp      *openrtb2.App
  1051  		expectedError    string
  1052  	}{
  1053  		{
  1054  			description: "FPD config and bid request app are not specified",
  1055  			expectedApp: nil,
  1056  		},
  1057  		{
  1058  			description:   "FPD config app only is specified",
  1059  			fpdConfig:     &openrtb_ext.ORTB2{App: json.RawMessage(`{"id": "test"}`)},
  1060  			expectedError: "incorrect First Party Data for bidder bidderA: App object is not defined in request, but defined in FPD config",
  1061  		},
  1062  		{
  1063  			description:   "FPD config and bid request app are specified",
  1064  			fpdConfig:     &openrtb_ext.ORTB2{App: json.RawMessage(`{"id": "test1"}`)},
  1065  			bidRequestApp: &openrtb2.App{ID: "test2"},
  1066  			expectedApp:   &openrtb2.App{ID: "test1"},
  1067  		},
  1068  		{
  1069  			description:   "FPD config, bid request and global fpd app are specified, no input app ext",
  1070  			fpdConfig:     &openrtb_ext.ORTB2{App: json.RawMessage(`{"id": "test1"}`)},
  1071  			bidRequestApp: &openrtb2.App{ID: "test2"},
  1072  			globalFPD:     map[string][]byte{appKey: []byte(`{"globalFPDAppData": "globalFPDAppDataValue"}`)},
  1073  			expectedApp:   &openrtb2.App{ID: "test1", Ext: json.RawMessage(`{"data":{"globalFPDAppData":"globalFPDAppDataValue"}}`)},
  1074  		},
  1075  		{
  1076  			description:   "FPD config, bid request app with ext and global fpd app are specified, no input app ext",
  1077  			fpdConfig:     &openrtb_ext.ORTB2{App: json.RawMessage(`{"id": "test1"}`)},
  1078  			bidRequestApp: &openrtb2.App{ID: "test2", Ext: json.RawMessage(`{"test":{"inputFPDAppData":"inputFPDAppDataValue"}}`)},
  1079  			globalFPD:     map[string][]byte{appKey: []byte(`{"globalFPDAppData": "globalFPDAppDataValue"}`)},
  1080  			expectedApp:   &openrtb2.App{ID: "test1", Ext: json.RawMessage(`{"data":{"globalFPDAppData":"globalFPDAppDataValue"},"test":{"inputFPDAppData":"inputFPDAppDataValue"}}`)},
  1081  		},
  1082  		{
  1083  			description:   "FPD config, bid request and global fpd app are specified, with input app ext.data",
  1084  			fpdConfig:     &openrtb_ext.ORTB2{App: json.RawMessage(`{"id": "test1"}`)},
  1085  			bidRequestApp: &openrtb2.App{ID: "test2", Ext: json.RawMessage(`{"data":{"inputFPDAppData":"inputFPDAppDataValue"}}`)},
  1086  			globalFPD:     map[string][]byte{appKey: []byte(`{"globalFPDAppData": "globalFPDAppDataValue"}`)},
  1087  			expectedApp:   &openrtb2.App{ID: "test1", Ext: json.RawMessage(`{"data":{"globalFPDAppData":"globalFPDAppDataValue","inputFPDAppData":"inputFPDAppDataValue"}}`)},
  1088  		},
  1089  		{
  1090  			description:   "FPD config, bid request and global fpd app are specified, with input app ext.data malformed",
  1091  			fpdConfig:     &openrtb_ext.ORTB2{App: json.RawMessage(`{"id": "test1"}`)},
  1092  			bidRequestApp: &openrtb2.App{ID: "test2", Ext: json.RawMessage(`{"data":{"inputFPDAppData":"inputFPDAppDataValue"}}`)},
  1093  			globalFPD:     map[string][]byte{appKey: []byte(`malformed`)},
  1094  			expectedError: "Invalid JSON Patch",
  1095  		},
  1096  		{
  1097  			description:   "bid request and openrtb global fpd app are specified, no input app ext",
  1098  			bidRequestApp: &openrtb2.App{ID: "test2"},
  1099  			openRtbGlobalFPD: map[string][]openrtb2.Data{appContentDataKey: {
  1100  				{ID: "DataId1", Name: "Name1"},
  1101  				{ID: "DataId2", Name: "Name2"},
  1102  			}},
  1103  			expectedApp: &openrtb2.App{ID: "test2", Content: &openrtb2.Content{Data: []openrtb2.Data{
  1104  				{ID: "DataId1", Name: "Name1"},
  1105  				{ID: "DataId2", Name: "Name2"},
  1106  			}}},
  1107  		},
  1108  		{
  1109  			description: "bid request with content and openrtb global fpd app are specified, no input app ext",
  1110  			bidRequestApp: &openrtb2.App{ID: "test2", Content: &openrtb2.Content{
  1111  				ID: "InputAppContentId",
  1112  				Data: []openrtb2.Data{
  1113  					{ID: "1", Name: "N1"},
  1114  					{ID: "2", Name: "N2"},
  1115  				},
  1116  				Ext: json.RawMessage(`{"contentPresent":true}`),
  1117  			}},
  1118  			openRtbGlobalFPD: map[string][]openrtb2.Data{appContentDataKey: {
  1119  				{ID: "DataId1", Name: "Name1"},
  1120  				{ID: "DataId2", Name: "Name2"},
  1121  			}},
  1122  			expectedApp: &openrtb2.App{ID: "test2", Content: &openrtb2.Content{
  1123  				ID: "InputAppContentId",
  1124  				Data: []openrtb2.Data{
  1125  					{ID: "DataId1", Name: "Name1"},
  1126  					{ID: "DataId2", Name: "Name2"},
  1127  				},
  1128  				Ext: json.RawMessage(`{"contentPresent":true}`),
  1129  			}},
  1130  		},
  1131  		{
  1132  			description:   "fpd config app, bid request and openrtb global fpd app are specified, no input app ext",
  1133  			fpdConfig:     &openrtb_ext.ORTB2{App: json.RawMessage(`{"id": "test1"}`)},
  1134  			bidRequestApp: &openrtb2.App{ID: "test2"},
  1135  			openRtbGlobalFPD: map[string][]openrtb2.Data{appContentDataKey: {
  1136  				{ID: "DataId1", Name: "Name1"},
  1137  				{ID: "DataId2", Name: "Name2"},
  1138  			}},
  1139  			expectedApp: &openrtb2.App{ID: "test1", Content: &openrtb2.Content{Data: []openrtb2.Data{
  1140  				{ID: "DataId1", Name: "Name1"},
  1141  				{ID: "DataId2", Name: "Name2"},
  1142  			}}},
  1143  		},
  1144  		{
  1145  			description:   "fpd config app with ext, bid request and openrtb global fpd app are specified, no input app ext",
  1146  			fpdConfig:     &openrtb_ext.ORTB2{App: json.RawMessage(`{"id": "test1", "ext":{"test":1}}`)},
  1147  			bidRequestApp: &openrtb2.App{ID: "test2"},
  1148  			openRtbGlobalFPD: map[string][]openrtb2.Data{appContentDataKey: {
  1149  				{ID: "DataId1", Name: "Name1"},
  1150  				{ID: "DataId2", Name: "Name2"},
  1151  			}},
  1152  			expectedApp: &openrtb2.App{ID: "test1", Content: &openrtb2.Content{Data: []openrtb2.Data{
  1153  				{ID: "DataId1", Name: "Name1"},
  1154  				{ID: "DataId2", Name: "Name2"},
  1155  			}},
  1156  				Ext: json.RawMessage(`{"test":1}`)},
  1157  		},
  1158  		{
  1159  			description:   "fpd config app with ext, bid request app with ext and openrtb global fpd app are specified, no input app ext",
  1160  			fpdConfig:     &openrtb_ext.ORTB2{App: json.RawMessage(`{"id": "test1", "ext":{"test":1}}`)},
  1161  			bidRequestApp: &openrtb2.App{ID: "test2", Ext: json.RawMessage(`{"test":2, "key": "value"}`)},
  1162  			openRtbGlobalFPD: map[string][]openrtb2.Data{appContentDataKey: {
  1163  				{ID: "DataId1", Name: "Name1"},
  1164  				{ID: "DataId2", Name: "Name2"},
  1165  			}},
  1166  			expectedApp: &openrtb2.App{ID: "test1", Content: &openrtb2.Content{Data: []openrtb2.Data{
  1167  				{ID: "DataId1", Name: "Name1"},
  1168  				{ID: "DataId2", Name: "Name2"},
  1169  			}},
  1170  				Ext: json.RawMessage(`{"key":"value","test":1}`)},
  1171  		},
  1172  		{
  1173  			description:   "fpd config app with malformed ext, bid request app with ext and openrtb global fpd app are specified, no input app ext",
  1174  			fpdConfig:     &openrtb_ext.ORTB2{App: json.RawMessage(`{"id": "test1", "ext":{malformed}}`)},
  1175  			bidRequestApp: &openrtb2.App{ID: "test2", Ext: json.RawMessage(`{"test":2, "key": "value"}`)},
  1176  			openRtbGlobalFPD: map[string][]openrtb2.Data{appContentDataKey: {
  1177  				{ID: "DataId1", Name: "Name1"},
  1178  				{ID: "DataId2", Name: "Name2"},
  1179  			}},
  1180  			expectedApp: &openrtb2.App{ID: "test1", Content: &openrtb2.Content{Data: []openrtb2.Data{
  1181  				{ID: "DataId1", Name: "Name1"},
  1182  				{ID: "DataId2", Name: "Name2"},
  1183  			}},
  1184  				Ext: json.RawMessage(`{"key":"value","test":1}`),
  1185  			},
  1186  			expectedError: "invalid character 'm' looking for beginning of object key string",
  1187  		},
  1188  	}
  1189  	for _, test := range testCases {
  1190  		t.Run(test.description, func(t *testing.T) {
  1191  			resultApp, err := resolveApp(test.fpdConfig, test.bidRequestApp, test.globalFPD, test.openRtbGlobalFPD, "bidderA")
  1192  
  1193  			if test.expectedError == "" {
  1194  				assert.NoError(t, err, "unexpected error returned")
  1195  				assert.Equal(t, test.expectedApp, resultApp, "Result app is incorrect")
  1196  			} else {
  1197  				assert.EqualError(t, err, test.expectedError, "expected error incorrect")
  1198  			}
  1199  		})
  1200  	}
  1201  }
  1202  
  1203  func TestBuildExtData(t *testing.T) {
  1204  	testCases := []struct {
  1205  		description string
  1206  		input       []byte
  1207  		expectedRes string
  1208  	}{
  1209  		{
  1210  			description: "Input object with int value",
  1211  			input:       []byte(`{"someData": 123}`),
  1212  			expectedRes: `{"data": {"someData": 123}}`,
  1213  		},
  1214  		{
  1215  			description: "Input object with bool value",
  1216  			input:       []byte(`{"someData": true}`),
  1217  			expectedRes: `{"data": {"someData": true}}`,
  1218  		},
  1219  		{
  1220  			description: "Input object with string value",
  1221  			input:       []byte(`{"someData": "true"}`),
  1222  			expectedRes: `{"data": {"someData": "true"}}`,
  1223  		},
  1224  		{
  1225  			description: "No input object",
  1226  			input:       []byte(`{}`),
  1227  			expectedRes: `{"data": {}}`,
  1228  		},
  1229  		{
  1230  			description: "Input object with object value",
  1231  			input:       []byte(`{"someData": {"moreFpdData": "fpddata"}}`),
  1232  			expectedRes: `{"data": {"someData": {"moreFpdData": "fpddata"}}}`,
  1233  		},
  1234  	}
  1235  
  1236  	for _, test := range testCases {
  1237  		actualRes := buildExtData(test.input)
  1238  		assert.JSONEq(t, test.expectedRes, string(actualRes), "Incorrect result data")
  1239  	}
  1240  }
  1241  
  1242  func TestMergeUser(t *testing.T) {
  1243  	testCases := []struct {
  1244  		name         string
  1245  		givenUser    openrtb2.User
  1246  		givenFPD     json.RawMessage
  1247  		expectedUser openrtb2.User
  1248  		expectedErr  string
  1249  	}{
  1250  		{
  1251  			name:         "empty",
  1252  			givenUser:    openrtb2.User{},
  1253  			givenFPD:     []byte(`{}`),
  1254  			expectedUser: openrtb2.User{},
  1255  		},
  1256  		{
  1257  			name:         "toplevel",
  1258  			givenUser:    openrtb2.User{ID: "1"},
  1259  			givenFPD:     []byte(`{"id":"2"}`),
  1260  			expectedUser: openrtb2.User{ID: "2"},
  1261  		},
  1262  		{
  1263  			name:         "toplevel-ext",
  1264  			givenUser:    openrtb2.User{Ext: []byte(`{"a":1,"b":2}`)},
  1265  			givenFPD:     []byte(`{"ext":{"b":100,"c":3}}`),
  1266  			expectedUser: openrtb2.User{Ext: []byte(`{"a":1,"b":100,"c":3}`)},
  1267  		},
  1268  		{
  1269  			name:        "toplevel-ext-err",
  1270  			givenUser:   openrtb2.User{ID: "1", Ext: []byte(`malformed`)},
  1271  			givenFPD:    []byte(`{"id":"2"}`),
  1272  			expectedErr: "invalid request ext",
  1273  		},
  1274  		{
  1275  			name:         "nested-geo",
  1276  			givenUser:    openrtb2.User{Geo: &openrtb2.Geo{Lat: 1}},
  1277  			givenFPD:     []byte(`{"geo":{"lat": 2}}`),
  1278  			expectedUser: openrtb2.User{Geo: &openrtb2.Geo{Lat: 2}},
  1279  		},
  1280  		{
  1281  			name:         "nested-geo-ext",
  1282  			givenUser:    openrtb2.User{Geo: &openrtb2.Geo{Ext: []byte(`{"a":1,"b":2}`)}},
  1283  			givenFPD:     []byte(`{"geo":{"ext":{"b":100,"c":3}}}`),
  1284  			expectedUser: openrtb2.User{Geo: &openrtb2.Geo{Ext: []byte(`{"a":1,"b":100,"c":3}`)}},
  1285  		},
  1286  		{
  1287  			name:         "toplevel-ext-and-nested-geo-ext",
  1288  			givenUser:    openrtb2.User{Ext: []byte(`{"a":1,"b":2}`), Geo: &openrtb2.Geo{Ext: []byte(`{"a":10,"b":20}`)}},
  1289  			givenFPD:     []byte(`{"ext":{"b":100,"c":3}, "geo":{"ext":{"b":100,"c":3}}}`),
  1290  			expectedUser: openrtb2.User{Ext: []byte(`{"a":1,"b":100,"c":3}`), Geo: &openrtb2.Geo{Ext: []byte(`{"a":10,"b":100,"c":3}`)}},
  1291  		},
  1292  		{
  1293  			name:        "nested-geo-ext-err",
  1294  			givenUser:   openrtb2.User{Geo: &openrtb2.Geo{Ext: []byte(`malformed`)}},
  1295  			givenFPD:    []byte(`{"geo":{"ext":{"b":100,"c":3}}}`),
  1296  			expectedErr: "invalid request ext",
  1297  		},
  1298  		{
  1299  			name:        "fpd-err",
  1300  			givenUser:   openrtb2.User{ID: "1", Ext: []byte(`{"a":1}`)},
  1301  			givenFPD:    []byte(`malformed`),
  1302  			expectedErr: "invalid character 'm' looking for beginning of value",
  1303  		},
  1304  	}
  1305  
  1306  	for _, test := range testCases {
  1307  		t.Run(test.name, func(t *testing.T) {
  1308  			err := mergeUser(&test.givenUser, test.givenFPD)
  1309  
  1310  			if test.expectedErr == "" {
  1311  				assert.NoError(t, err, "unexpected error returned")
  1312  				assert.Equal(t, test.expectedUser, test.givenUser, "result user is incorrect")
  1313  			} else {
  1314  				assert.EqualError(t, err, test.expectedErr, "expected error incorrect")
  1315  			}
  1316  		})
  1317  	}
  1318  }
  1319  
  1320  func TestMergeApp(t *testing.T) {
  1321  	testCases := []struct {
  1322  		name        string
  1323  		givenApp    openrtb2.App
  1324  		givenFPD    json.RawMessage
  1325  		expectedApp openrtb2.App
  1326  		expectedErr string
  1327  	}{
  1328  		{
  1329  			name:        "empty",
  1330  			givenApp:    openrtb2.App{},
  1331  			givenFPD:    []byte(`{}`),
  1332  			expectedApp: openrtb2.App{},
  1333  		},
  1334  		{
  1335  			name:        "toplevel",
  1336  			givenApp:    openrtb2.App{ID: "1"},
  1337  			givenFPD:    []byte(`{"id":"2"}`),
  1338  			expectedApp: openrtb2.App{ID: "2"},
  1339  		},
  1340  		{
  1341  			name:        "toplevel-ext",
  1342  			givenApp:    openrtb2.App{Ext: []byte(`{"a":1,"b":2}`)},
  1343  			givenFPD:    []byte(`{"ext":{"b":100,"c":3}}`),
  1344  			expectedApp: openrtb2.App{Ext: []byte(`{"a":1,"b":100,"c":3}`)},
  1345  		},
  1346  		{
  1347  			name:        "toplevel-ext-err",
  1348  			givenApp:    openrtb2.App{ID: "1", Ext: []byte(`malformed`)},
  1349  			givenFPD:    []byte(`{"id":"2"}`),
  1350  			expectedErr: "invalid request ext",
  1351  		},
  1352  		{
  1353  			name:        "nested-publisher",
  1354  			givenApp:    openrtb2.App{Publisher: &openrtb2.Publisher{Name: "pub1"}},
  1355  			givenFPD:    []byte(`{"publisher":{"name": "pub2"}}`),
  1356  			expectedApp: openrtb2.App{Publisher: &openrtb2.Publisher{Name: "pub2"}},
  1357  		},
  1358  		{
  1359  			name:        "nested-content",
  1360  			givenApp:    openrtb2.App{Content: &openrtb2.Content{Title: "content1"}},
  1361  			givenFPD:    []byte(`{"content":{"title": "content2"}}`),
  1362  			expectedApp: openrtb2.App{Content: &openrtb2.Content{Title: "content2"}},
  1363  		},
  1364  		{
  1365  			name:        "nested-content-producer",
  1366  			givenApp:    openrtb2.App{Content: &openrtb2.Content{Title: "content1", Producer: &openrtb2.Producer{Name: "producer1"}}},
  1367  			givenFPD:    []byte(`{"content":{"title": "content2", "producer":{"name":"producer2"}}}`),
  1368  			expectedApp: openrtb2.App{Content: &openrtb2.Content{Title: "content2", Producer: &openrtb2.Producer{Name: "producer2"}}},
  1369  		},
  1370  		{
  1371  			name:        "nested-content-network",
  1372  			givenApp:    openrtb2.App{Content: &openrtb2.Content{Title: "content1", Network: &openrtb2.Network{Name: "network1"}}},
  1373  			givenFPD:    []byte(`{"content":{"title": "content2", "network":{"name":"network2"}}}`),
  1374  			expectedApp: openrtb2.App{Content: &openrtb2.Content{Title: "content2", Network: &openrtb2.Network{Name: "network2"}}},
  1375  		},
  1376  		{
  1377  			name:        "nested-content-channel",
  1378  			givenApp:    openrtb2.App{Content: &openrtb2.Content{Title: "content1", Channel: &openrtb2.Channel{Name: "channel1"}}},
  1379  			givenFPD:    []byte(`{"content":{"title": "content2", "channel":{"name":"channel2"}}}`),
  1380  			expectedApp: openrtb2.App{Content: &openrtb2.Content{Title: "content2", Channel: &openrtb2.Channel{Name: "channel2"}}},
  1381  		},
  1382  		{
  1383  			name:        "nested-publisher-ext",
  1384  			givenApp:    openrtb2.App{Publisher: &openrtb2.Publisher{Ext: []byte(`{"a":1,"b":2}`)}},
  1385  			givenFPD:    []byte(`{"publisher":{"ext":{"b":100,"c":3}}}`),
  1386  			expectedApp: openrtb2.App{Publisher: &openrtb2.Publisher{Ext: []byte(`{"a":1,"b":100,"c":3}`)}},
  1387  		},
  1388  		{
  1389  			name:        "nested-content-ext",
  1390  			givenApp:    openrtb2.App{Content: &openrtb2.Content{Ext: []byte(`{"a":1,"b":2}`)}},
  1391  			givenFPD:    []byte(`{"content":{"ext":{"b":100,"c":3}}}`),
  1392  			expectedApp: openrtb2.App{Content: &openrtb2.Content{Ext: []byte(`{"a":1,"b":100,"c":3}`)}},
  1393  		},
  1394  		{
  1395  			name:        "nested-content-producer-ext",
  1396  			givenApp:    openrtb2.App{Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`{"a":1,"b":2}`)}}},
  1397  			givenFPD:    []byte(`{"content":{"producer":{"ext":{"b":100,"c":3}}}}`),
  1398  			expectedApp: openrtb2.App{Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}},
  1399  		},
  1400  		{
  1401  			name:        "nested-content-network-ext",
  1402  			givenApp:    openrtb2.App{Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`{"a":1,"b":2}`)}}},
  1403  			givenFPD:    []byte(`{"content":{"network":{"ext":{"b":100,"c":3}}}}`),
  1404  			expectedApp: openrtb2.App{Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}},
  1405  		},
  1406  		{
  1407  			name:        "nested-content-channel-ext",
  1408  			givenApp:    openrtb2.App{Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`{"a":1,"b":2}`)}}},
  1409  			givenFPD:    []byte(`{"content":{"channel":{"ext":{"b":100,"c":3}}}}`),
  1410  			expectedApp: openrtb2.App{Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}},
  1411  		},
  1412  		{
  1413  			name:        "toplevel-ext-and-nested-publisher-ext",
  1414  			givenApp:    openrtb2.App{Ext: []byte(`{"a":1,"b":2}`), Publisher: &openrtb2.Publisher{Ext: []byte(`{"a":10,"b":20}`)}},
  1415  			givenFPD:    []byte(`{"ext":{"b":100,"c":3}, "publisher":{"ext":{"b":100,"c":3}}}`),
  1416  			expectedApp: openrtb2.App{Ext: []byte(`{"a":1,"b":100,"c":3}`), Publisher: &openrtb2.Publisher{Ext: []byte(`{"a":10,"b":100,"c":3}`)}},
  1417  		},
  1418  		{
  1419  			name:        "toplevel-ext-and-nested-content-ext",
  1420  			givenApp:    openrtb2.App{Ext: []byte(`{"a":1,"b":2}`), Content: &openrtb2.Content{Ext: []byte(`{"a":10,"b":20}`)}},
  1421  			givenFPD:    []byte(`{"ext":{"b":100,"c":3}, "content":{"ext":{"b":100,"c":3}}}`),
  1422  			expectedApp: openrtb2.App{Ext: []byte(`{"a":1,"b":100,"c":3}`), Content: &openrtb2.Content{Ext: []byte(`{"a":10,"b":100,"c":3}`)}},
  1423  		},
  1424  		{
  1425  			name:        "toplevel-ext-and-nested-content-producer-ext",
  1426  			givenApp:    openrtb2.App{Ext: []byte(`{"a":1,"b":2}`), Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`{"a":10,"b":20}`)}}},
  1427  			givenFPD:    []byte(`{"ext":{"b":100,"c":3}, "content":{"producer": {"ext":{"b":100,"c":3}}}}`),
  1428  			expectedApp: openrtb2.App{Ext: []byte(`{"a":1,"b":100,"c":3}`), Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}},
  1429  		},
  1430  		{
  1431  			name:        "toplevel-ext-and-nested-content-network-ext",
  1432  			givenApp:    openrtb2.App{Ext: []byte(`{"a":1,"b":2}`), Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`{"a":10,"b":20}`)}}},
  1433  			givenFPD:    []byte(`{"ext":{"b":100,"c":3}, "content":{"network": {"ext":{"b":100,"c":3}}}}`),
  1434  			expectedApp: openrtb2.App{Ext: []byte(`{"a":1,"b":100,"c":3}`), Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}},
  1435  		},
  1436  		{
  1437  			name:        "toplevel-ext-and-nested-content-channel-ext",
  1438  			givenApp:    openrtb2.App{Ext: []byte(`{"a":1,"b":2}`), Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`{"a":10,"b":20}`)}}},
  1439  			givenFPD:    []byte(`{"ext":{"b":100,"c":3}, "content":{"channel": {"ext":{"b":100,"c":3}}}}`),
  1440  			expectedApp: openrtb2.App{Ext: []byte(`{"a":1,"b":100,"c":3}`), Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}},
  1441  		},
  1442  		{
  1443  			name:        "nested-publisher-ext-err",
  1444  			givenApp:    openrtb2.App{Publisher: &openrtb2.Publisher{Ext: []byte(`malformed`)}},
  1445  			givenFPD:    []byte(`{"publisher":{"ext":{"b":100,"c":3}}}`),
  1446  			expectedErr: "invalid request ext",
  1447  		},
  1448  		{
  1449  			name:        "nested-content-ext-err",
  1450  			givenApp:    openrtb2.App{Content: &openrtb2.Content{Ext: []byte(`malformed`)}},
  1451  			givenFPD:    []byte(`{"content":{"ext":{"b":100,"c":3}}}`),
  1452  			expectedErr: "invalid request ext",
  1453  		},
  1454  		{
  1455  			name:        "nested-content-producer-ext-err",
  1456  			givenApp:    openrtb2.App{Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`malformed`)}}},
  1457  			givenFPD:    []byte(`{"content":{"producer": {"ext":{"b":100,"c":3}}}}`),
  1458  			expectedErr: "invalid request ext",
  1459  		},
  1460  		{
  1461  			name:        "nested-content-network-ext-err",
  1462  			givenApp:    openrtb2.App{Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`malformed`)}}},
  1463  			givenFPD:    []byte(`{"content":{"network": {"ext":{"b":100,"c":3}}}}`),
  1464  			expectedErr: "invalid request ext",
  1465  		},
  1466  		{
  1467  			name:        "nested-content-channel-ext-err",
  1468  			givenApp:    openrtb2.App{Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`malformed`)}}},
  1469  			givenFPD:    []byte(`{"content":{"channelx": {"ext":{"b":100,"c":3}}}}`),
  1470  			expectedErr: "invalid request ext",
  1471  		},
  1472  		{
  1473  			name:        "fpd-err",
  1474  			givenApp:    openrtb2.App{ID: "1", Ext: []byte(`{"a":1}`)},
  1475  			givenFPD:    []byte(`malformed`),
  1476  			expectedErr: "invalid character 'm' looking for beginning of value",
  1477  		},
  1478  	}
  1479  
  1480  	for _, test := range testCases {
  1481  		t.Run(test.name, func(t *testing.T) {
  1482  			err := mergeApp(&test.givenApp, test.givenFPD)
  1483  
  1484  			if test.expectedErr == "" {
  1485  				assert.NoError(t, err, "unexpected error returned")
  1486  				assert.Equal(t, test.expectedApp, test.givenApp, " result app is incorrect")
  1487  			} else {
  1488  				assert.EqualError(t, err, test.expectedErr, "expected error incorrect")
  1489  			}
  1490  		})
  1491  	}
  1492  }
  1493  
  1494  func TestMergeSite(t *testing.T) {
  1495  	testCases := []struct {
  1496  		name         string
  1497  		givenSite    openrtb2.Site
  1498  		givenFPD     json.RawMessage
  1499  		expectedSite openrtb2.Site
  1500  		expectedErr  string
  1501  	}{
  1502  		{
  1503  			name:        "empty",
  1504  			givenSite:   openrtb2.Site{},
  1505  			givenFPD:    []byte(`{}`),
  1506  			expectedErr: "incorrect First Party Data for bidder BidderA: Site object cannot set empty page if req.site.id is empty",
  1507  		},
  1508  		{
  1509  			name:         "toplevel",
  1510  			givenSite:    openrtb2.Site{ID: "1"},
  1511  			givenFPD:     []byte(`{"id":"2"}`),
  1512  			expectedSite: openrtb2.Site{ID: "2"},
  1513  		},
  1514  		{
  1515  			name:         "toplevel-ext",
  1516  			givenSite:    openrtb2.Site{Page: "test.com/page", Ext: []byte(`{"a":1,"b":2}`)},
  1517  			givenFPD:     []byte(`{"ext":{"b":100,"c":3}}`),
  1518  			expectedSite: openrtb2.Site{Page: "test.com/page", Ext: []byte(`{"a":1,"b":100,"c":3}`)},
  1519  		},
  1520  		{
  1521  			name:        "toplevel-ext-err",
  1522  			givenSite:   openrtb2.Site{ID: "1", Ext: []byte(`malformed`)},
  1523  			givenFPD:    []byte(`{"id":"2"}`),
  1524  			expectedErr: "invalid request ext",
  1525  		},
  1526  		{
  1527  			name:         "nested-publisher",
  1528  			givenSite:    openrtb2.Site{Page: "test.com/page", Publisher: &openrtb2.Publisher{Name: "pub1"}},
  1529  			givenFPD:     []byte(`{"publisher":{"name": "pub2"}}`),
  1530  			expectedSite: openrtb2.Site{Page: "test.com/page", Publisher: &openrtb2.Publisher{Name: "pub2"}},
  1531  		},
  1532  		{
  1533  			name:         "nested-content",
  1534  			givenSite:    openrtb2.Site{Page: "test.com/page", Content: &openrtb2.Content{Title: "content1"}},
  1535  			givenFPD:     []byte(`{"content":{"title": "content2"}}`),
  1536  			expectedSite: openrtb2.Site{Page: "test.com/page", Content: &openrtb2.Content{Title: "content2"}},
  1537  		},
  1538  		{
  1539  			name:         "nested-content-producer",
  1540  			givenSite:    openrtb2.Site{ID: "1", Content: &openrtb2.Content{Title: "content1", Producer: &openrtb2.Producer{Name: "producer1"}}},
  1541  			givenFPD:     []byte(`{"content":{"title": "content2", "producer":{"name":"producer2"}}}`),
  1542  			expectedSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Title: "content2", Producer: &openrtb2.Producer{Name: "producer2"}}},
  1543  		},
  1544  		{
  1545  			name:         "nested-content-network",
  1546  			givenSite:    openrtb2.Site{ID: "1", Content: &openrtb2.Content{Title: "content1", Network: &openrtb2.Network{Name: "network1"}}},
  1547  			givenFPD:     []byte(`{"content":{"title": "content2", "network":{"name":"network2"}}}`),
  1548  			expectedSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Title: "content2", Network: &openrtb2.Network{Name: "network2"}}},
  1549  		},
  1550  		{
  1551  			name:         "nested-content-channel",
  1552  			givenSite:    openrtb2.Site{ID: "1", Content: &openrtb2.Content{Title: "content1", Channel: &openrtb2.Channel{Name: "channel1"}}},
  1553  			givenFPD:     []byte(`{"content":{"title": "content2", "channel":{"name":"channel2"}}}`),
  1554  			expectedSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Title: "content2", Channel: &openrtb2.Channel{Name: "channel2"}}},
  1555  		},
  1556  		{
  1557  			name:         "nested-publisher-ext",
  1558  			givenSite:    openrtb2.Site{ID: "1", Publisher: &openrtb2.Publisher{Ext: []byte(`{"a":1,"b":2}`)}},
  1559  			givenFPD:     []byte(`{"publisher":{"ext":{"b":100,"c":3}}}`),
  1560  			expectedSite: openrtb2.Site{ID: "1", Publisher: &openrtb2.Publisher{Ext: []byte(`{"a":1,"b":100,"c":3}`)}},
  1561  		},
  1562  		{
  1563  			name:         "nested-content-ext",
  1564  			givenSite:    openrtb2.Site{ID: "1", Content: &openrtb2.Content{Ext: []byte(`{"a":1,"b":2}`)}},
  1565  			givenFPD:     []byte(`{"content":{"ext":{"b":100,"c":3}}}`),
  1566  			expectedSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Ext: []byte(`{"a":1,"b":100,"c":3}`)}},
  1567  		},
  1568  		{
  1569  			name:         "nested-content-producer-ext",
  1570  			givenSite:    openrtb2.Site{ID: "1", Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`{"a":1,"b":2}`)}}},
  1571  			givenFPD:     []byte(`{"content":{"producer":{"ext":{"b":100,"c":3}}}}`),
  1572  			expectedSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}},
  1573  		},
  1574  		{
  1575  			name:         "nested-content-network-ext",
  1576  			givenSite:    openrtb2.Site{ID: "1", Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`{"a":1,"b":2}`)}}},
  1577  			givenFPD:     []byte(`{"content":{"network":{"ext":{"b":100,"c":3}}}}`),
  1578  			expectedSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}},
  1579  		},
  1580  		{
  1581  			name:         "nested-content-channel-ext",
  1582  			givenSite:    openrtb2.Site{ID: "1", Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`{"a":1,"b":2}`)}}},
  1583  			givenFPD:     []byte(`{"content":{"channel":{"ext":{"b":100,"c":3}}}}`),
  1584  			expectedSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}},
  1585  		},
  1586  		{
  1587  			name:         "toplevel-ext-and-nested-publisher-ext",
  1588  			givenSite:    openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":2}`), Publisher: &openrtb2.Publisher{Ext: []byte(`{"a":10,"b":20}`)}},
  1589  			givenFPD:     []byte(`{"ext":{"b":100,"c":3}, "publisher":{"ext":{"b":100,"c":3}}}`),
  1590  			expectedSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":100,"c":3}`), Publisher: &openrtb2.Publisher{Ext: []byte(`{"a":10,"b":100,"c":3}`)}},
  1591  		},
  1592  		{
  1593  			name:         "toplevel-ext-and-nested-content-ext",
  1594  			givenSite:    openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":2}`), Content: &openrtb2.Content{Ext: []byte(`{"a":10,"b":20}`)}},
  1595  			givenFPD:     []byte(`{"ext":{"b":100,"c":3}, "content":{"ext":{"b":100,"c":3}}}`),
  1596  			expectedSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":100,"c":3}`), Content: &openrtb2.Content{Ext: []byte(`{"a":10,"b":100,"c":3}`)}},
  1597  		},
  1598  		{
  1599  			name:         "toplevel-ext-and-nested-content-producer-ext",
  1600  			givenSite:    openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":2}`), Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`{"a":10,"b":20}`)}}},
  1601  			givenFPD:     []byte(`{"ext":{"b":100,"c":3}, "content":{"producer": {"ext":{"b":100,"c":3}}}}`),
  1602  			expectedSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":100,"c":3}`), Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}},
  1603  		},
  1604  		{
  1605  			name:         "toplevel-ext-and-nested-content-network-ext",
  1606  			givenSite:    openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":2}`), Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`{"a":10,"b":20}`)}}},
  1607  			givenFPD:     []byte(`{"ext":{"b":100,"c":3}, "content":{"network": {"ext":{"b":100,"c":3}}}}`),
  1608  			expectedSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":100,"c":3}`), Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}},
  1609  		},
  1610  		{
  1611  			name:         "toplevel-ext-and-nested-content-channel-ext",
  1612  			givenSite:    openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":2}`), Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`{"a":10,"b":20}`)}}},
  1613  			givenFPD:     []byte(`{"ext":{"b":100,"c":3}, "content":{"channel": {"ext":{"b":100,"c":3}}}}`),
  1614  			expectedSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":100,"c":3}`), Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}},
  1615  		},
  1616  		{
  1617  			name:        "nested-publisher-ext-err",
  1618  			givenSite:   openrtb2.Site{ID: "1", Publisher: &openrtb2.Publisher{Ext: []byte(`malformed`)}},
  1619  			givenFPD:    []byte(`{"publisher":{"ext":{"b":100,"c":3}}}`),
  1620  			expectedErr: "invalid request ext",
  1621  		},
  1622  		{
  1623  			name:        "nested-content-ext-err",
  1624  			givenSite:   openrtb2.Site{ID: "1", Content: &openrtb2.Content{Ext: []byte(`malformed`)}},
  1625  			givenFPD:    []byte(`{"content":{"ext":{"b":100,"c":3}}}`),
  1626  			expectedErr: "invalid request ext",
  1627  		},
  1628  		{
  1629  			name:        "nested-content-producer-ext-err",
  1630  			givenSite:   openrtb2.Site{ID: "1", Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`malformed`)}}},
  1631  			givenFPD:    []byte(`{"content":{"producer": {"ext":{"b":100,"c":3}}}}`),
  1632  			expectedErr: "invalid request ext",
  1633  		},
  1634  		{
  1635  			name:        "nested-content-network-ext-err",
  1636  			givenSite:   openrtb2.Site{ID: "1", Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`malformed`)}}},
  1637  			givenFPD:    []byte(`{"content":{"network": {"ext":{"b":100,"c":3}}}}`),
  1638  			expectedErr: "invalid request ext",
  1639  		},
  1640  		{
  1641  			name:        "nested-content-channel-ext-err",
  1642  			givenSite:   openrtb2.Site{ID: "1", Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`malformed`)}}},
  1643  			givenFPD:    []byte(`{"content":{"channelx": {"ext":{"b":100,"c":3}}}}`),
  1644  			expectedErr: "invalid request ext",
  1645  		},
  1646  		{
  1647  			name:        "fpd-err",
  1648  			givenSite:   openrtb2.Site{ID: "1", Ext: []byte(`{"a":1}`)},
  1649  			givenFPD:    []byte(`malformed`),
  1650  			expectedErr: "invalid character 'm' looking for beginning of value",
  1651  		},
  1652  	}
  1653  
  1654  	for _, test := range testCases {
  1655  		t.Run(test.name, func(t *testing.T) {
  1656  			err := mergeSite(&test.givenSite, test.givenFPD, "BidderA")
  1657  
  1658  			if test.expectedErr == "" {
  1659  				assert.NoError(t, err, "unexpected error returned")
  1660  				assert.Equal(t, test.expectedSite, test.givenSite, " result Site is incorrect")
  1661  			} else {
  1662  				assert.EqualError(t, err, test.expectedErr, "expected error incorrect")
  1663  			}
  1664  		})
  1665  	}
  1666  }
  1667  
  1668  // TestMergeObjectStructure detects when new nested objects are added to First Party Data supported
  1669  // fields, as these will invalidate the mergeSite, mergeApp, and mergeUser methods. If this test fails,
  1670  // fix the merge methods to add support and update this test to set a new baseline.
  1671  func TestMergeObjectStructure(t *testing.T) {
  1672  	testCases := []struct {
  1673  		name         string
  1674  		kind         any
  1675  		knownStructs []string
  1676  	}{
  1677  		{
  1678  			name: "Site",
  1679  			kind: openrtb2.Site{},
  1680  			knownStructs: []string{
  1681  				"Publisher",
  1682  				"Content",
  1683  				"Content.Producer",
  1684  				"Content.Network",
  1685  				"Content.Channel",
  1686  			},
  1687  		},
  1688  		{
  1689  			name: "App",
  1690  			kind: openrtb2.App{},
  1691  			knownStructs: []string{
  1692  				"Publisher",
  1693  				"Content",
  1694  				"Content.Producer",
  1695  				"Content.Network",
  1696  				"Content.Channel",
  1697  			},
  1698  		},
  1699  		{
  1700  			name: "User",
  1701  			kind: openrtb2.User{},
  1702  			knownStructs: []string{
  1703  				"Geo",
  1704  			},
  1705  		},
  1706  	}
  1707  
  1708  	for _, test := range testCases {
  1709  		t.Run(test.name, func(t *testing.T) {
  1710  			nestedStructs := []string{}
  1711  
  1712  			var discover func(parent string, t reflect.Type)
  1713  			discover = func(parent string, t reflect.Type) {
  1714  				fields := reflect.VisibleFields(t)
  1715  				for _, field := range fields {
  1716  					if field.Type.Kind() == reflect.Pointer && field.Type.Elem().Kind() == reflect.Struct {
  1717  						nestedStructs = append(nestedStructs, parent+field.Name)
  1718  						discover(parent+field.Name+".", field.Type.Elem())
  1719  					}
  1720  				}
  1721  			}
  1722  			discover("", reflect.TypeOf(test.kind))
  1723  
  1724  			assert.ElementsMatch(t, test.knownStructs, nestedStructs)
  1725  		})
  1726  	}
  1727  }
  1728  
  1729  // user memory protect test
  1730  func TestMergeUserMemoryProtection(t *testing.T) {
  1731  	inputGeo := &openrtb2.Geo{
  1732  		Ext: json.RawMessage(`{"a":1,"b":2}`),
  1733  	}
  1734  	input := openrtb2.User{
  1735  		ID:  "1",
  1736  		Geo: inputGeo,
  1737  	}
  1738  
  1739  	err := mergeUser(&input, userFPD)
  1740  	assert.NoError(t, err)
  1741  
  1742  	// Input user object is expected to be a copy. Changes are ok.
  1743  	assert.Equal(t, "2", input.ID, "user-id-copied")
  1744  
  1745  	// Nested objects must be copied before changes.
  1746  	assert.JSONEq(t, `{"a":1,"b":2}`, string(inputGeo.Ext), "geo-input")
  1747  	assert.JSONEq(t, `{"a":1,"b":100,"c":3}`, string(input.Geo.Ext), "geo-copied")
  1748  }
  1749  
  1750  // app memory protect test
  1751  func TestMergeAppMemoryProtection(t *testing.T) {
  1752  	inputPublisher := &openrtb2.Publisher{
  1753  		ID:  "InPubId",
  1754  		Ext: json.RawMessage(`{"a": "inputPubExt", "b": 1}`),
  1755  	}
  1756  	inputContent := &openrtb2.Content{
  1757  		ID:  "InContentId",
  1758  		Ext: json.RawMessage(`{"a": "inputContentExt", "b": 1}`),
  1759  		Producer: &openrtb2.Producer{
  1760  			ID:  "InProducerId",
  1761  			Ext: json.RawMessage(`{"a": "inputProducerExt", "b": 1}`),
  1762  		},
  1763  		Network: &openrtb2.Network{
  1764  			ID:  "InNetworkId",
  1765  			Ext: json.RawMessage(`{"a": "inputNetworkExt", "b": 1}`),
  1766  		},
  1767  		Channel: &openrtb2.Channel{
  1768  			ID:  "InChannelId",
  1769  			Ext: json.RawMessage(`{"a": "inputChannelExt", "b": 1}`),
  1770  		},
  1771  	}
  1772  	input := openrtb2.App{
  1773  		ID:        "InAppID",
  1774  		Publisher: inputPublisher,
  1775  		Content:   inputContent,
  1776  		Ext:       json.RawMessage(`{"a": "inputAppExt", "b": 1}`),
  1777  	}
  1778  
  1779  	err := mergeApp(&input, fpdWithPublisherAndContent)
  1780  	assert.NoError(t, err)
  1781  
  1782  	// Input app object is expected to be a copy. Changes are ok.
  1783  	assert.Equal(t, "FPDID", input.ID, "app-id-copied")
  1784  	assert.JSONEq(t, `{"a": "FPDExt", "b": 2}`, string(input.Ext), "app-ext-copied")
  1785  
  1786  	// Nested objects must be copied before changes.
  1787  	assert.Equal(t, "InPubId", inputPublisher.ID, "app-pub-id-input")
  1788  	assert.Equal(t, "FPDPubId", input.Publisher.ID, "app-pub-id-copied")
  1789  	assert.JSONEq(t, `{"a": "inputPubExt", "b": 1}`, string(inputPublisher.Ext), "app-pub-ext-input")
  1790  	assert.JSONEq(t, `{"a": "FPDPubExt", "b": 2}`, string(input.Publisher.Ext), "app-pub-ext-copied")
  1791  
  1792  	assert.Equal(t, "InContentId", inputContent.ID, "app-content-id-input")
  1793  	assert.Equal(t, "FPDContentId", input.Content.ID, "app-content-id-copied")
  1794  	assert.JSONEq(t, `{"a": "inputContentExt", "b": 1}`, string(inputContent.Ext), "app-content-ext-input")
  1795  	assert.JSONEq(t, `{"a": "FPDContentExt", "b": 2}`, string(input.Content.Ext), "app-content-ext-copied")
  1796  
  1797  	assert.Equal(t, "InProducerId", inputContent.Producer.ID, "app-content-producer-id-input")
  1798  	assert.Equal(t, "FPDProducerId", input.Content.Producer.ID, "app-content-producer-id-copied")
  1799  	assert.JSONEq(t, `{"a": "inputProducerExt", "b": 1}`, string(inputContent.Producer.Ext), "app-content-producer-ext-input")
  1800  	assert.JSONEq(t, `{"a": "FPDProducerExt", "b": 2}`, string(input.Content.Producer.Ext), "app-content-producer-ext-copied")
  1801  
  1802  	assert.Equal(t, "InNetworkId", inputContent.Network.ID, "app-content-network-id-input")
  1803  	assert.Equal(t, "FPDNetworkId", input.Content.Network.ID, "app-content-network-id-copied")
  1804  	assert.JSONEq(t, `{"a": "inputNetworkExt", "b": 1}`, string(inputContent.Network.Ext), "app-content-network-ext-input")
  1805  	assert.JSONEq(t, `{"a": "FPDNetworkExt", "b": 2}`, string(input.Content.Network.Ext), "app-content-network-ext-copied")
  1806  
  1807  	assert.Equal(t, "InChannelId", inputContent.Channel.ID, "app-content-channel-id-input")
  1808  	assert.Equal(t, "FPDChannelId", input.Content.Channel.ID, "app-content-channel-id-copied")
  1809  	assert.JSONEq(t, `{"a": "inputChannelExt", "b": 1}`, string(inputContent.Channel.Ext), "app-content-channel-ext-input")
  1810  	assert.JSONEq(t, `{"a": "FPDChannelExt", "b": 2}`, string(input.Content.Channel.Ext), "app-content-channel-ext-copied")
  1811  }
  1812  
  1813  // site memory protect test
  1814  func TestMergeSiteMemoryProtection(t *testing.T) {
  1815  	inputPublisher := &openrtb2.Publisher{
  1816  		ID:  "InPubId",
  1817  		Ext: json.RawMessage(`{"a": "inputPubExt", "b": 1}`),
  1818  	}
  1819  	inputContent := &openrtb2.Content{
  1820  		ID:  "InContentId",
  1821  		Ext: json.RawMessage(`{"a": "inputContentExt", "b": 1}`),
  1822  		Producer: &openrtb2.Producer{
  1823  			ID:  "InProducerId",
  1824  			Ext: json.RawMessage(`{"a": "inputProducerExt", "b": 1}`),
  1825  		},
  1826  		Network: &openrtb2.Network{
  1827  			ID:  "InNetworkId",
  1828  			Ext: json.RawMessage(`{"a": "inputNetworkExt", "b": 1}`),
  1829  		},
  1830  		Channel: &openrtb2.Channel{
  1831  			ID:  "InChannelId",
  1832  			Ext: json.RawMessage(`{"a": "inputChannelExt", "b": 1}`),
  1833  		},
  1834  	}
  1835  	input := openrtb2.Site{
  1836  		ID:        "InSiteID",
  1837  		Publisher: inputPublisher,
  1838  		Content:   inputContent,
  1839  		Ext:       json.RawMessage(`{"a": "inputSiteExt", "b": 1}`),
  1840  	}
  1841  
  1842  	err := mergeSite(&input, fpdWithPublisherAndContent, "BidderA")
  1843  	assert.NoError(t, err)
  1844  
  1845  	// Input app object is expected to be a copy. Changes are ok.
  1846  	assert.Equal(t, "FPDID", input.ID, "site-id-copied")
  1847  	assert.JSONEq(t, `{"a": "FPDExt", "b": 2}`, string(input.Ext), "site-ext-copied")
  1848  
  1849  	// Nested objects must be copied before changes.
  1850  	assert.Equal(t, "InPubId", inputPublisher.ID, "site-pub-id-input")
  1851  	assert.Equal(t, "FPDPubId", input.Publisher.ID, "site-pub-id-copied")
  1852  	assert.JSONEq(t, `{"a": "inputPubExt", "b": 1}`, string(inputPublisher.Ext), "site-pub-ext-input")
  1853  	assert.JSONEq(t, `{"a": "FPDPubExt", "b": 2}`, string(input.Publisher.Ext), "site-pub-ext-copied")
  1854  
  1855  	assert.Equal(t, "InContentId", inputContent.ID, "site-content-id-input")
  1856  	assert.Equal(t, "FPDContentId", input.Content.ID, "site-content-id-copied")
  1857  	assert.JSONEq(t, `{"a": "inputContentExt", "b": 1}`, string(inputContent.Ext), "site-content-ext-input")
  1858  	assert.JSONEq(t, `{"a": "FPDContentExt", "b": 2}`, string(input.Content.Ext), "site-content-ext-copied")
  1859  
  1860  	assert.Equal(t, "InProducerId", inputContent.Producer.ID, "site-content-producer-id-input")
  1861  	assert.Equal(t, "FPDProducerId", input.Content.Producer.ID, "site-content-producer-id-copied")
  1862  	assert.JSONEq(t, `{"a": "inputProducerExt", "b": 1}`, string(inputContent.Producer.Ext), "site-content-producer-ext-input")
  1863  	assert.JSONEq(t, `{"a": "FPDProducerExt", "b": 2}`, string(input.Content.Producer.Ext), "site-content-producer-ext-copied")
  1864  
  1865  	assert.Equal(t, "InNetworkId", inputContent.Network.ID, "site-content-network-id-input")
  1866  	assert.Equal(t, "FPDNetworkId", input.Content.Network.ID, "site-content-network-id-copied")
  1867  	assert.JSONEq(t, `{"a": "inputNetworkExt", "b": 1}`, string(inputContent.Network.Ext), "site-content-network-ext-input")
  1868  	assert.JSONEq(t, `{"a": "FPDNetworkExt", "b": 2}`, string(input.Content.Network.Ext), "site-content-network-ext-copied")
  1869  
  1870  	assert.Equal(t, "InChannelId", inputContent.Channel.ID, "site-content-channel-id-input")
  1871  	assert.Equal(t, "FPDChannelId", input.Content.Channel.ID, "site-content-channel-id-copied")
  1872  	assert.JSONEq(t, `{"a": "inputChannelExt", "b": 1}`, string(inputContent.Channel.Ext), "site-content-channel-ext-input")
  1873  	assert.JSONEq(t, `{"a": "FPDChannelExt", "b": 2}`, string(input.Content.Channel.Ext), "site-content-channel-ext-copied")
  1874  }
  1875  
  1876  var (
  1877  	userFPD = []byte(`
  1878  {
  1879    "id": "2",
  1880    "geo": {
  1881      "ext": {
  1882        "b": 100,
  1883        "c": 3
  1884      }
  1885    }
  1886  }
  1887  `)
  1888  
  1889  	fpdWithPublisherAndContent = []byte(`
  1890  {
  1891    "id": "FPDID",
  1892    "ext": {"a": "FPDExt", "b": 2},
  1893    "publisher": {
  1894      "id": "FPDPubId",
  1895      "ext": {"a": "FPDPubExt", "b": 2}
  1896    },
  1897    "content": {
  1898      "id": "FPDContentId",
  1899      "ext": {"a": "FPDContentExt", "b": 2},
  1900      "producer": {
  1901        "id": "FPDProducerId",
  1902        "ext": {"a": "FPDProducerExt", "b": 2}
  1903      },
  1904      "network": {
  1905        "id": "FPDNetworkId",
  1906        "ext": {"a": "FPDNetworkExt", "b": 2}
  1907      },
  1908      "channel": {
  1909        "id": "FPDChannelId",
  1910        "ext": {"a": "FPDChannelExt", "b": 2}
  1911      }
  1912    }
  1913  }
  1914  `)
  1915  
  1916  	user = []byte(`
  1917  {
  1918    "id": "2",
  1919    "yob": 2000,
  1920    "geo": {
  1921      "city": "LA",
  1922      "ext": {
  1923        "b": 100,
  1924        "c": 3
  1925      }
  1926    }
  1927  }
  1928  `)
  1929  )