github.com/prebid/prebid-server/v2@v2.18.0/exchange/auction_test.go (about)

     1  package exchange
     2  
     3  import (
     4  	"context"
     5  	"encoding/xml"
     6  	"fmt"
     7  	"os"
     8  	"path/filepath"
     9  	"regexp"
    10  	"sort"
    11  	"strconv"
    12  	"testing"
    13  
    14  	"github.com/prebid/openrtb/v20/openrtb2"
    15  	"github.com/prebid/prebid-server/v2/config"
    16  	"github.com/prebid/prebid-server/v2/exchange/entities"
    17  	"github.com/prebid/prebid-server/v2/openrtb_ext"
    18  	"github.com/prebid/prebid-server/v2/prebid_cache_client"
    19  	"github.com/prebid/prebid-server/v2/util/jsonutil"
    20  	"github.com/prebid/prebid-server/v2/util/ptrutil"
    21  
    22  	"github.com/stretchr/testify/assert"
    23  )
    24  
    25  func TestMakeVASTGiven(t *testing.T) {
    26  	const expect = `<VAST version="3.0"></VAST>`
    27  	bid := &openrtb2.Bid{
    28  		AdM: expect,
    29  	}
    30  	vast := makeVAST(bid)
    31  	assert.Equal(t, expect, vast)
    32  }
    33  
    34  func TestMakeVASTNurl(t *testing.T) {
    35  	const url = "http://domain.com/win-notify/1"
    36  	const expect = `<VAST version="3.0"><Ad><Wrapper>` +
    37  		`<AdSystem>prebid.org wrapper</AdSystem>` +
    38  		`<VASTAdTagURI><![CDATA[` + url + `]]></VASTAdTagURI>` +
    39  		`<Impression></Impression><Creatives></Creatives>` +
    40  		`</Wrapper></Ad></VAST>`
    41  	bid := &openrtb2.Bid{
    42  		NURL: url,
    43  	}
    44  	vast := makeVAST(bid)
    45  	assert.Equal(t, expect, vast)
    46  }
    47  
    48  func TestBuildCacheString(t *testing.T) {
    49  	testCases := []struct {
    50  		description      string
    51  		debugLog         DebugLog
    52  		expectedDebugLog DebugLog
    53  	}{
    54  		{
    55  			description: "DebugLog strings should have tags and be formatted",
    56  			debugLog: DebugLog{
    57  				Data: DebugData{
    58  					Request:  "test request string",
    59  					Headers:  "test headers string",
    60  					Response: "test response string",
    61  				},
    62  				Regexp: regexp.MustCompile(`[<>]`),
    63  			},
    64  			expectedDebugLog: DebugLog{
    65  				Data: DebugData{
    66  					Request:  "<Request>test request string</Request>",
    67  					Headers:  "<Headers>test headers string</Headers>",
    68  					Response: "<Response>test response string</Response>",
    69  				},
    70  				Regexp: regexp.MustCompile(`[<>]`),
    71  			},
    72  		},
    73  		{
    74  			description: "DebugLog strings should have no < or > characters",
    75  			debugLog: DebugLog{
    76  				Data: DebugData{
    77  					Request:  "<test>test request string</test>",
    78  					Headers:  "test <headers string",
    79  					Response: "test <response> string",
    80  				},
    81  				Regexp: regexp.MustCompile(`[<>]`),
    82  			},
    83  			expectedDebugLog: DebugLog{
    84  				Data: DebugData{
    85  					Request:  "<Request>testtest request string/test</Request>",
    86  					Headers:  "<Headers>test headers string</Headers>",
    87  					Response: "<Response>test response string</Response>",
    88  				},
    89  				Regexp: regexp.MustCompile(`[<>]`),
    90  			},
    91  		},
    92  	}
    93  
    94  	for _, test := range testCases {
    95  		test.expectedDebugLog.CacheString = fmt.Sprintf("%s<Log>%s%s%s</Log>", xml.Header, test.expectedDebugLog.Data.Request, test.expectedDebugLog.Data.Headers, test.expectedDebugLog.Data.Response)
    96  
    97  		test.debugLog.BuildCacheString()
    98  
    99  		assert.Equal(t, test.expectedDebugLog, test.debugLog, test.description)
   100  	}
   101  }
   102  
   103  // TestCacheJSON executes tests for all the *.json files in cachetest.
   104  // customcachekey.json test here verifies custom cache key not used for non-vast video
   105  func TestCacheJSON(t *testing.T) {
   106  	for _, dir := range []string{"cachetest", "customcachekeytest", "impcustomcachekeytest", "eventscachetest"} {
   107  		if specFiles, err := os.ReadDir(dir); err == nil {
   108  			for _, specFile := range specFiles {
   109  				fileName := filepath.Join(dir, specFile.Name())
   110  				fileDisplayName := "exchange/" + fileName
   111  				t.Run(fileDisplayName, func(t *testing.T) {
   112  					specData, err := loadCacheSpec(fileName)
   113  					if assert.NoError(t, err, "Failed to load contents of file %s: %v", fileDisplayName, err) {
   114  						runCacheSpec(t, fileDisplayName, specData)
   115  					}
   116  				})
   117  			}
   118  		} else {
   119  			t.Fatalf("Failed to read contents of directory exchange/%s: %v", dir, err)
   120  		}
   121  	}
   122  }
   123  
   124  func TestIsDebugOverrideEnabled(t *testing.T) {
   125  	type inTest struct {
   126  		debugHeader string
   127  		configToken string
   128  	}
   129  	type aTest struct {
   130  		desc   string
   131  		in     inTest
   132  		result bool
   133  	}
   134  	testCases := []aTest{
   135  		{
   136  			desc:   "test debug header is empty, config token is empty",
   137  			in:     inTest{debugHeader: "", configToken: ""},
   138  			result: false,
   139  		},
   140  		{
   141  			desc:   "test debug header is present, config token is empty",
   142  			in:     inTest{debugHeader: "TestToken", configToken: ""},
   143  			result: false,
   144  		},
   145  		{
   146  			desc:   "test debug header is empty, config token is present",
   147  			in:     inTest{debugHeader: "", configToken: "TestToken"},
   148  			result: false,
   149  		},
   150  		{
   151  			desc:   "test debug header is present, config token is present, not equal",
   152  			in:     inTest{debugHeader: "TestToken123", configToken: "TestToken"},
   153  			result: false,
   154  		},
   155  		{
   156  			desc:   "test debug header is present, config token is present, equal",
   157  			in:     inTest{debugHeader: "TestToken", configToken: "TestToken"},
   158  			result: true,
   159  		},
   160  		{
   161  			desc:   "test debug header is present, config token is present, not case equal",
   162  			in:     inTest{debugHeader: "TestTokeN", configToken: "TestToken"},
   163  			result: false,
   164  		},
   165  	}
   166  
   167  	for _, test := range testCases {
   168  		result := IsDebugOverrideEnabled(test.in.debugHeader, test.in.configToken)
   169  		assert.Equal(t, test.result, result, test.desc)
   170  	}
   171  
   172  }
   173  
   174  // LoadCacheSpec reads and parses a file as a test case. If something goes wrong, it returns an error.
   175  func loadCacheSpec(filename string) (*cacheSpec, error) {
   176  	specData, err := os.ReadFile(filename)
   177  	if err != nil {
   178  		return nil, fmt.Errorf("Failed to read file %s: %v", filename, err)
   179  	}
   180  
   181  	var spec cacheSpec
   182  	if err := jsonutil.UnmarshalValid(specData, &spec); err != nil {
   183  		return nil, fmt.Errorf("Failed to unmarshal JSON from file: %v", err)
   184  	}
   185  
   186  	return &spec, nil
   187  }
   188  
   189  // runCacheSpec cycles through the bids found in the json test cases and
   190  // finds the highest bid of every Imp, then tests doCache() with resulting auction object
   191  func runCacheSpec(t *testing.T, fileDisplayName string, specData *cacheSpec) {
   192  	var bid *entities.PbsOrtbBid
   193  	winningBidsByImp := make(map[string]*entities.PbsOrtbBid)
   194  	allBidsByBidder := make(map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid)
   195  	roundedPrices := make(map[*entities.PbsOrtbBid]string)
   196  	bidCategory := make(map[string]string)
   197  
   198  	// Traverse through the bid list found in the parsed in Json file
   199  	for _, pbsBid := range specData.PbsBids {
   200  		bid = &entities.PbsOrtbBid{
   201  			Bid:     pbsBid.Bid,
   202  			BidType: pbsBid.BidType,
   203  		}
   204  		cpm := bid.Bid.Price
   205  
   206  		// Map this bid if it's the highest we've seen from this Imp so far
   207  		wbid, ok := winningBidsByImp[bid.Bid.ImpID]
   208  		if !ok || cpm > wbid.Bid.Price {
   209  			winningBidsByImp[bid.Bid.ImpID] = bid
   210  		}
   211  
   212  		// Map this bid if it's the highest we've seen from this bidder so far
   213  		if bidMap, ok := allBidsByBidder[bid.Bid.ImpID]; ok {
   214  			bidMap[pbsBid.Bidder] = append(bidMap[pbsBid.Bidder], bid)
   215  		} else {
   216  			allBidsByBidder[bid.Bid.ImpID] = map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
   217  				pbsBid.Bidder: {bid},
   218  			}
   219  		}
   220  
   221  		for _, topBidsPerBidder := range allBidsByBidder {
   222  			for _, topBids := range topBidsPerBidder {
   223  				sort.Slice(topBids, func(i, j int) bool {
   224  					return isNewWinningBid(topBids[i].Bid, topBids[j].Bid, true)
   225  				})
   226  			}
   227  		}
   228  
   229  		if len(pbsBid.Bid.Cat) == 1 {
   230  			bidCategory[pbsBid.Bid.ID] = pbsBid.Bid.Cat[0]
   231  		}
   232  		roundedPrices[bid] = strconv.FormatFloat(bid.Bid.Price, 'f', 2, 64)
   233  	}
   234  
   235  	ctx := context.Background()
   236  	cache := &mockCache{}
   237  
   238  	targData := &targetData{
   239  		priceGranularity: openrtb_ext.PriceGranularity{
   240  			Precision: ptrutil.ToPtr(2),
   241  			Ranges: []openrtb_ext.GranularityRange{
   242  				{
   243  					Min:       0,
   244  					Max:       5,
   245  					Increment: 0.05,
   246  				},
   247  				{
   248  					Min:       5,
   249  					Max:       10,
   250  					Increment: 0.1,
   251  				},
   252  				{
   253  					Min:       10,
   254  					Max:       20,
   255  					Increment: 0.5,
   256  				},
   257  			},
   258  		},
   259  		includeWinners:    specData.TargetDataIncludeWinners,
   260  		includeBidderKeys: specData.TargetDataIncludeBidderKeys,
   261  		includeCacheBids:  specData.TargetDataIncludeCacheBids,
   262  		includeCacheVast:  specData.TargetDataIncludeCacheVast,
   263  	}
   264  
   265  	testAuction := &auction{
   266  		winningBids:     winningBidsByImp,
   267  		allBidsByBidder: allBidsByBidder,
   268  		roundedPrices:   roundedPrices,
   269  	}
   270  	evTracking := &eventTracking{
   271  		accountID:          "TEST_ACC_ID",
   272  		enabledForAccount:  specData.EventsDataEnabledForAccount,
   273  		enabledForRequest:  specData.EventsDataEnabledForRequest,
   274  		externalURL:        "http://localhost",
   275  		auctionTimestampMs: 1234567890,
   276  	}
   277  	_ = testAuction.doCache(ctx, cache, targData, evTracking, &specData.BidRequest, 60, &specData.DefaultTTLs, bidCategory, &specData.DebugLog)
   278  
   279  	if len(specData.ExpectedCacheables) > len(cache.items) {
   280  		t.Errorf("%s:  [CACHE_ERROR] Less elements were cached than expected \n", fileDisplayName)
   281  	} else if len(specData.ExpectedCacheables) < len(cache.items) {
   282  		t.Errorf("%s:  [CACHE_ERROR] More elements were cached than expected \n", fileDisplayName)
   283  	} else { // len(specData.ExpectedCacheables) == len(cache.items)
   284  		// We cached the exact number of elements we expected, now we compare them side by side in n^2
   285  		var matched int = 0
   286  		for i, expectedCacheable := range specData.ExpectedCacheables {
   287  			found := false
   288  			var expectedData interface{}
   289  			if err := jsonutil.UnmarshalValid(expectedCacheable.Data, &expectedData); err != nil {
   290  				t.Fatalf("Failed to decode expectedCacheables[%d].value: %v", i, err)
   291  			}
   292  			if s, ok := expectedData.(string); ok && expectedCacheable.Type == prebid_cache_client.TypeJSON {
   293  				// decode again if we have pre-encoded json string values
   294  				if err := jsonutil.UnmarshalValid([]byte(s), &expectedData); err != nil {
   295  					t.Fatalf("Failed to re-decode expectedCacheables[%d].value :%v", i, err)
   296  				}
   297  			}
   298  			for j, cachedItem := range cache.items {
   299  				var actualData interface{}
   300  				if err := jsonutil.UnmarshalValid(cachedItem.Data, &actualData); err != nil {
   301  					t.Fatalf("Failed to decode actual cache[%d].value: %s", j, err)
   302  				}
   303  				if assert.ObjectsAreEqual(expectedData, actualData) &&
   304  					expectedCacheable.TTLSeconds == cachedItem.TTLSeconds &&
   305  					expectedCacheable.Type == cachedItem.Type &&
   306  					len(expectedCacheable.Key) <= len(cachedItem.Key) &&
   307  					expectedCacheable.Key == cachedItem.Key[:len(expectedCacheable.Key)] {
   308  					found = true
   309  					cache.items = append(cache.items[:j], cache.items[j+1:]...) // remove matched item
   310  					break
   311  				}
   312  			}
   313  			if found {
   314  				matched++
   315  			} else {
   316  				t.Errorf("%s: [CACHE_ERROR] Did not see expected cacheable #%d: type=%s, ttl=%d, value=%s", fileDisplayName, i, expectedCacheable.Type, expectedCacheable.TTLSeconds, string(expectedCacheable.Data))
   317  			}
   318  		}
   319  		if matched != len(specData.ExpectedCacheables) {
   320  			for i, item := range cache.items {
   321  				t.Errorf("%s: [CACHE_ERROR] Got unexpected cached item #%d: type=%s, ttl=%d, value=%s", fileDisplayName, i, item.Type, item.TTLSeconds, string(item.Data))
   322  			}
   323  			t.FailNow()
   324  		}
   325  	}
   326  }
   327  
   328  func TestNewAuction(t *testing.T) {
   329  	bid1p077 := entities.PbsOrtbBid{
   330  		Bid: &openrtb2.Bid{
   331  			ImpID: "imp1",
   332  			Price: 0.77,
   333  		},
   334  	}
   335  	bid1p123 := entities.PbsOrtbBid{
   336  		Bid: &openrtb2.Bid{
   337  			ImpID: "imp1",
   338  			Price: 1.23,
   339  		},
   340  	}
   341  	bid1p230 := entities.PbsOrtbBid{
   342  		Bid: &openrtb2.Bid{
   343  			ImpID: "imp1",
   344  			Price: 2.30,
   345  		},
   346  	}
   347  	bid1p088d := entities.PbsOrtbBid{
   348  		Bid: &openrtb2.Bid{
   349  			ImpID:  "imp1",
   350  			Price:  0.88,
   351  			DealID: "SpecialDeal",
   352  		},
   353  	}
   354  	bid1p166d := entities.PbsOrtbBid{
   355  		Bid: &openrtb2.Bid{
   356  			ImpID:  "imp1",
   357  			Price:  1.66,
   358  			DealID: "BigDeal",
   359  		},
   360  	}
   361  	bid2p123 := entities.PbsOrtbBid{
   362  		Bid: &openrtb2.Bid{
   363  			ImpID: "imp2",
   364  			Price: 1.23,
   365  		},
   366  	}
   367  	bid2p144 := entities.PbsOrtbBid{
   368  		Bid: &openrtb2.Bid{
   369  			ImpID: "imp2",
   370  			Price: 1.44,
   371  		},
   372  	}
   373  	bid2p155 := entities.PbsOrtbBid{
   374  		Bid: &openrtb2.Bid{
   375  			ImpID: "imp2",
   376  			Price: 1.55,
   377  		},
   378  	}
   379  	bid2p166 := entities.PbsOrtbBid{
   380  		Bid: &openrtb2.Bid{
   381  			ImpID: "imp2",
   382  			Price: 1.66,
   383  		},
   384  	}
   385  	tests := []struct {
   386  		description     string
   387  		seatBids        map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid
   388  		numImps         int
   389  		preferDeals     bool
   390  		expectedAuction auction
   391  	}{
   392  		{
   393  			description: "Basic auction test",
   394  			seatBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
   395  				"appnexus": {
   396  					Bids: []*entities.PbsOrtbBid{&bid1p123},
   397  				},
   398  				"rubicon": {
   399  					Bids: []*entities.PbsOrtbBid{&bid1p230},
   400  				},
   401  			},
   402  			numImps:     1,
   403  			preferDeals: false,
   404  			expectedAuction: auction{
   405  				winningBids: map[string]*entities.PbsOrtbBid{
   406  					"imp1": &bid1p230,
   407  				},
   408  				allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
   409  					"imp1": {
   410  						"appnexus": []*entities.PbsOrtbBid{&bid1p123},
   411  						"rubicon":  []*entities.PbsOrtbBid{&bid1p230},
   412  					},
   413  				},
   414  			},
   415  		},
   416  		{
   417  			description: "Multi-imp auction",
   418  			seatBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
   419  				"appnexus": {
   420  					Bids: []*entities.PbsOrtbBid{&bid1p230, &bid2p123},
   421  				},
   422  				"rubicon": {
   423  					Bids: []*entities.PbsOrtbBid{&bid1p077, &bid2p144},
   424  				},
   425  				"openx": {
   426  					Bids: []*entities.PbsOrtbBid{&bid1p123},
   427  				},
   428  			},
   429  			numImps:     2,
   430  			preferDeals: false,
   431  			expectedAuction: auction{
   432  				winningBids: map[string]*entities.PbsOrtbBid{
   433  					"imp1": &bid1p230,
   434  					"imp2": &bid2p144,
   435  				},
   436  				allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
   437  					"imp1": {
   438  						"appnexus": []*entities.PbsOrtbBid{&bid1p230},
   439  						"rubicon":  []*entities.PbsOrtbBid{&bid1p077},
   440  						"openx":    []*entities.PbsOrtbBid{&bid1p123},
   441  					},
   442  					"imp2": {
   443  						"appnexus": []*entities.PbsOrtbBid{&bid2p123},
   444  						"rubicon":  []*entities.PbsOrtbBid{&bid2p144},
   445  					},
   446  				},
   447  			},
   448  		},
   449  		{
   450  			description: "Basic auction with deals, no preference",
   451  			seatBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
   452  				"appnexus": {
   453  					Bids: []*entities.PbsOrtbBid{&bid1p123},
   454  				},
   455  				"rubicon": {
   456  					Bids: []*entities.PbsOrtbBid{&bid1p088d},
   457  				},
   458  			},
   459  			numImps:     1,
   460  			preferDeals: false,
   461  			expectedAuction: auction{
   462  				winningBids: map[string]*entities.PbsOrtbBid{
   463  					"imp1": &bid1p123,
   464  				},
   465  				allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
   466  					"imp1": {
   467  						"appnexus": []*entities.PbsOrtbBid{&bid1p123},
   468  						"rubicon":  []*entities.PbsOrtbBid{&bid1p088d},
   469  					},
   470  				},
   471  			},
   472  		},
   473  		{
   474  			description: "Basic auction with deals, prefer deals",
   475  			seatBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
   476  				"appnexus": {
   477  					Bids: []*entities.PbsOrtbBid{&bid1p123},
   478  				},
   479  				"rubicon": {
   480  					Bids: []*entities.PbsOrtbBid{&bid1p088d},
   481  				},
   482  			},
   483  			numImps:     1,
   484  			preferDeals: true,
   485  			expectedAuction: auction{
   486  				winningBids: map[string]*entities.PbsOrtbBid{
   487  					"imp1": &bid1p088d,
   488  				},
   489  				allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
   490  					"imp1": {
   491  						"appnexus": []*entities.PbsOrtbBid{&bid1p123},
   492  						"rubicon":  []*entities.PbsOrtbBid{&bid1p088d},
   493  					},
   494  				},
   495  			},
   496  		},
   497  		{
   498  			description: "Auction with 2 deals",
   499  			seatBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
   500  				"appnexus": {
   501  					Bids: []*entities.PbsOrtbBid{&bid1p166d},
   502  				},
   503  				"rubicon": {
   504  					Bids: []*entities.PbsOrtbBid{&bid1p088d},
   505  				},
   506  			},
   507  			numImps:     1,
   508  			preferDeals: true,
   509  			expectedAuction: auction{
   510  				winningBids: map[string]*entities.PbsOrtbBid{
   511  					"imp1": &bid1p166d,
   512  				},
   513  				allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
   514  					"imp1": {
   515  						"appnexus": []*entities.PbsOrtbBid{&bid1p166d},
   516  						"rubicon":  []*entities.PbsOrtbBid{&bid1p088d},
   517  					},
   518  				},
   519  			},
   520  		},
   521  		{
   522  			description: "Auction with 3 bids and 2 deals",
   523  			seatBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
   524  				"appnexus": {
   525  					Bids: []*entities.PbsOrtbBid{&bid1p166d},
   526  				},
   527  				"rubicon": {
   528  					Bids: []*entities.PbsOrtbBid{&bid1p088d},
   529  				},
   530  				"openx": {
   531  					Bids: []*entities.PbsOrtbBid{&bid1p230},
   532  				},
   533  			},
   534  			numImps:     1,
   535  			preferDeals: true,
   536  			expectedAuction: auction{
   537  				winningBids: map[string]*entities.PbsOrtbBid{
   538  					"imp1": &bid1p166d,
   539  				},
   540  				allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
   541  					"imp1": {
   542  						"appnexus": []*entities.PbsOrtbBid{&bid1p166d},
   543  						"rubicon":  []*entities.PbsOrtbBid{&bid1p088d},
   544  						"openx":    []*entities.PbsOrtbBid{&bid1p230},
   545  					},
   546  				},
   547  			},
   548  		},
   549  		{
   550  			description: "Auction with 3 bids and 2 deals - multiple bids under each seatBids",
   551  			seatBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
   552  				"appnexus": {
   553  					Bids: []*entities.PbsOrtbBid{&bid1p166d, &bid1p077, &bid2p123, &bid2p144},
   554  				},
   555  				"pubmatic": {
   556  					Bids: []*entities.PbsOrtbBid{&bid1p088d, &bid1p123, &bid2p155, &bid2p166},
   557  				},
   558  			},
   559  			numImps:     1,
   560  			preferDeals: true,
   561  			expectedAuction: auction{
   562  				winningBids: map[string]*entities.PbsOrtbBid{
   563  					"imp1": &bid1p166d,
   564  					"imp2": &bid2p166,
   565  				},
   566  				allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
   567  					"imp1": {
   568  						"appnexus": []*entities.PbsOrtbBid{&bid1p166d, &bid1p077},
   569  						"pubmatic": []*entities.PbsOrtbBid{&bid1p088d, &bid1p123},
   570  					},
   571  					"imp2": {
   572  						"appnexus": []*entities.PbsOrtbBid{&bid2p123, &bid2p144},
   573  						"pubmatic": []*entities.PbsOrtbBid{&bid2p155, &bid2p166},
   574  					},
   575  				},
   576  			},
   577  		},
   578  	}
   579  
   580  	for _, test := range tests {
   581  		auc := newAuction(test.seatBids, test.numImps, test.preferDeals)
   582  
   583  		assert.Equal(t, test.expectedAuction, *auc, test.description)
   584  	}
   585  
   586  }
   587  
   588  func TestValidateAndUpdateMultiBid(t *testing.T) {
   589  	// create new bids for new test cases since the last one changes a few bids. Ex marks bid1p001.Bid = nil
   590  	bid1p001 := entities.PbsOrtbBid{
   591  		Bid: &openrtb2.Bid{
   592  			ImpID: "imp1",
   593  			Price: 0.01,
   594  		},
   595  	}
   596  	bid1p077 := entities.PbsOrtbBid{
   597  		Bid: &openrtb2.Bid{
   598  			ImpID: "imp1",
   599  			Price: 0.77,
   600  		},
   601  	}
   602  	bid1p123 := entities.PbsOrtbBid{
   603  		Bid: &openrtb2.Bid{
   604  			ImpID: "imp1",
   605  			Price: 1.23,
   606  		},
   607  	}
   608  	bid1p088d := entities.PbsOrtbBid{
   609  		Bid: &openrtb2.Bid{
   610  			ImpID:  "imp1",
   611  			Price:  0.88,
   612  			DealID: "SpecialDeal",
   613  		},
   614  	}
   615  	bid1p166d := entities.PbsOrtbBid{
   616  		Bid: &openrtb2.Bid{
   617  			ImpID:  "imp1",
   618  			Price:  1.66,
   619  			DealID: "BigDeal",
   620  		},
   621  	}
   622  	bid2p123 := entities.PbsOrtbBid{
   623  		Bid: &openrtb2.Bid{
   624  			ImpID: "imp2",
   625  			Price: 1.23,
   626  		},
   627  	}
   628  	bid2p144 := entities.PbsOrtbBid{
   629  		Bid: &openrtb2.Bid{
   630  			ImpID: "imp2",
   631  			Price: 1.44,
   632  		},
   633  	}
   634  	bid2p155 := entities.PbsOrtbBid{
   635  		Bid: &openrtb2.Bid{
   636  			ImpID: "imp2",
   637  			Price: 1.55,
   638  		},
   639  	}
   640  	bid2p166 := entities.PbsOrtbBid{
   641  		Bid: &openrtb2.Bid{
   642  			ImpID: "imp2",
   643  			Price: 1.66,
   644  		},
   645  	}
   646  
   647  	type fields struct {
   648  		winningBids     map[string]*entities.PbsOrtbBid
   649  		allBidsByBidder map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid
   650  		roundedPrices   map[*entities.PbsOrtbBid]string
   651  		cacheIds        map[*openrtb2.Bid]string
   652  		vastCacheIds    map[*openrtb2.Bid]string
   653  	}
   654  	type args struct {
   655  		adapterBids            map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid
   656  		preferDeals            bool
   657  		accountDefaultBidLimit int
   658  	}
   659  	type want struct {
   660  		allBidsByBidder map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid
   661  		adapterBids     map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid
   662  	}
   663  	tests := []struct {
   664  		description string
   665  		fields      fields
   666  		args        args
   667  		want        want
   668  	}{
   669  		{
   670  			description: "DefaultBidLimit is 0 (default value)",
   671  			fields: fields{
   672  				winningBids: map[string]*entities.PbsOrtbBid{
   673  					"imp1": &bid1p166d,
   674  					"imp2": &bid2p166,
   675  				},
   676  				allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
   677  					"imp1": {
   678  						"appnexus": []*entities.PbsOrtbBid{&bid1p001, &bid1p166d, &bid1p077},
   679  						"pubmatic": []*entities.PbsOrtbBid{&bid1p088d, &bid1p123},
   680  					},
   681  					"imp2": {
   682  						"appnexus": []*entities.PbsOrtbBid{&bid2p123, &bid2p144},
   683  						"pubmatic": []*entities.PbsOrtbBid{&bid2p155, &bid2p166},
   684  					},
   685  				},
   686  			},
   687  			args: args{
   688  				adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
   689  					"appnexus": {
   690  						Bids: []*entities.PbsOrtbBid{&bid1p001, &bid1p166d, &bid1p077, &bid2p123, &bid2p144},
   691  					},
   692  					"pubmatic": {
   693  						Bids: []*entities.PbsOrtbBid{&bid1p088d, &bid1p123, &bid2p155, &bid2p166},
   694  					},
   695  				},
   696  				accountDefaultBidLimit: 0,
   697  				preferDeals:            true,
   698  			},
   699  			want: want{
   700  				allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
   701  					"imp1": {
   702  						"appnexus": []*entities.PbsOrtbBid{&bid1p166d, &bid1p077, &bid1p001},
   703  						"pubmatic": []*entities.PbsOrtbBid{&bid1p088d, &bid1p123},
   704  					},
   705  					"imp2": {
   706  						"appnexus": []*entities.PbsOrtbBid{&bid2p144, &bid2p123},
   707  						"pubmatic": []*entities.PbsOrtbBid{&bid2p166, &bid2p155},
   708  					},
   709  				},
   710  				adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
   711  					"appnexus": {
   712  						Bids: []*entities.PbsOrtbBid{&bid1p001, &bid1p166d, &bid1p077, &bid2p123, &bid2p144},
   713  					},
   714  					"pubmatic": {
   715  						Bids: []*entities.PbsOrtbBid{&bid1p088d, &bid1p123, &bid2p155, &bid2p166},
   716  					},
   717  				},
   718  			},
   719  		},
   720  		{
   721  			description: "Adapters bid count per imp within DefaultBidLimit",
   722  			fields: fields{
   723  				winningBids: map[string]*entities.PbsOrtbBid{
   724  					"imp1": &bid1p166d,
   725  					"imp2": &bid2p166,
   726  				},
   727  				allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
   728  					"imp1": {
   729  						"appnexus": []*entities.PbsOrtbBid{&bid1p001, &bid1p166d, &bid1p077},
   730  						"pubmatic": []*entities.PbsOrtbBid{&bid1p088d, &bid1p123},
   731  					},
   732  					"imp2": {
   733  						"appnexus": []*entities.PbsOrtbBid{&bid2p123, &bid2p144},
   734  						"pubmatic": []*entities.PbsOrtbBid{&bid2p155, &bid2p166},
   735  					},
   736  				},
   737  			},
   738  			args: args{
   739  				adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
   740  					"appnexus": {
   741  						Bids: []*entities.PbsOrtbBid{&bid1p001, &bid1p166d, &bid1p077, &bid2p123, &bid2p144},
   742  					},
   743  					"pubmatic": {
   744  						Bids: []*entities.PbsOrtbBid{&bid1p088d, &bid1p123, &bid2p155, &bid2p166},
   745  					},
   746  				},
   747  				accountDefaultBidLimit: 3,
   748  				preferDeals:            true,
   749  			},
   750  			want: want{
   751  				allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
   752  					"imp1": {
   753  						"appnexus": []*entities.PbsOrtbBid{&bid1p166d, &bid1p077, &bid1p001},
   754  						"pubmatic": []*entities.PbsOrtbBid{&bid1p088d, &bid1p123},
   755  					},
   756  					"imp2": {
   757  						"appnexus": []*entities.PbsOrtbBid{&bid2p144, &bid2p123},
   758  						"pubmatic": []*entities.PbsOrtbBid{&bid2p166, &bid2p155},
   759  					},
   760  				},
   761  				adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
   762  					"appnexus": {
   763  						Bids: []*entities.PbsOrtbBid{&bid1p001, &bid1p166d, &bid1p077, &bid2p123, &bid2p144},
   764  					},
   765  					"pubmatic": {
   766  						Bids: []*entities.PbsOrtbBid{&bid1p088d, &bid1p123, &bid2p155, &bid2p166},
   767  					},
   768  				},
   769  			},
   770  		},
   771  		{
   772  			description: "Adapters bid count per imp more than DefaultBidLimit",
   773  			fields: fields{
   774  				winningBids: map[string]*entities.PbsOrtbBid{
   775  					"imp1": &bid1p166d,
   776  					"imp2": &bid2p166,
   777  				},
   778  				allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
   779  					"imp1": {
   780  						"appnexus": []*entities.PbsOrtbBid{&bid1p001, &bid1p166d, &bid1p077},
   781  						"pubmatic": []*entities.PbsOrtbBid{&bid1p088d, &bid1p123},
   782  					},
   783  					"imp2": {
   784  						"appnexus": []*entities.PbsOrtbBid{&bid2p123, &bid2p144},
   785  						"pubmatic": []*entities.PbsOrtbBid{&bid2p155, &bid2p166},
   786  					},
   787  				},
   788  			},
   789  			args: args{
   790  				adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
   791  					"appnexus": {
   792  						Bids: []*entities.PbsOrtbBid{&bid1p001, &bid1p166d, &bid1p077, &bid2p123, &bid2p144},
   793  					},
   794  					"pubmatic": {
   795  						Bids: []*entities.PbsOrtbBid{&bid1p088d, &bid1p123, &bid2p155, &bid2p166},
   796  					},
   797  				},
   798  				accountDefaultBidLimit: 2,
   799  				preferDeals:            true,
   800  			},
   801  			want: want{
   802  				allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
   803  					"imp1": {
   804  						"appnexus": []*entities.PbsOrtbBid{&bid1p166d, &bid1p077},
   805  						"pubmatic": []*entities.PbsOrtbBid{&bid1p088d, &bid1p123},
   806  					},
   807  					"imp2": {
   808  						"appnexus": []*entities.PbsOrtbBid{&bid2p144, &bid2p123},
   809  						"pubmatic": []*entities.PbsOrtbBid{&bid2p166, &bid2p155},
   810  					},
   811  				},
   812  				adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
   813  					"appnexus": {
   814  						Bids: []*entities.PbsOrtbBid{&bid1p166d, &bid1p077, &bid2p123, &bid2p144},
   815  					},
   816  					"pubmatic": {
   817  						Bids: []*entities.PbsOrtbBid{&bid1p088d, &bid1p123, &bid2p155, &bid2p166},
   818  					},
   819  				},
   820  			},
   821  		},
   822  	}
   823  	for _, tt := range tests {
   824  		t.Run(tt.description, func(t *testing.T) {
   825  			a := &auction{
   826  				winningBids:     tt.fields.winningBids,
   827  				allBidsByBidder: tt.fields.allBidsByBidder,
   828  				roundedPrices:   tt.fields.roundedPrices,
   829  				cacheIds:        tt.fields.cacheIds,
   830  				vastCacheIds:    tt.fields.vastCacheIds,
   831  			}
   832  			a.validateAndUpdateMultiBid(tt.args.adapterBids, tt.args.preferDeals, tt.args.accountDefaultBidLimit)
   833  			assert.Equal(t, tt.want.allBidsByBidder, tt.fields.allBidsByBidder, tt.description)
   834  			assert.Equal(t, tt.want.adapterBids, tt.args.adapterBids, tt.description)
   835  		})
   836  	}
   837  }
   838  
   839  type cacheSpec struct {
   840  	BidRequest                  openrtb2.BidRequest             `json:"bidRequest"`
   841  	PbsBids                     []pbsBid                        `json:"pbsBids"`
   842  	ExpectedCacheables          []prebid_cache_client.Cacheable `json:"expectedCacheables"`
   843  	DefaultTTLs                 config.DefaultTTLs              `json:"defaultTTLs"`
   844  	TargetDataIncludeWinners    bool                            `json:"targetDataIncludeWinners"`
   845  	TargetDataIncludeBidderKeys bool                            `json:"targetDataIncludeBidderKeys"`
   846  	TargetDataIncludeCacheBids  bool                            `json:"targetDataIncludeCacheBids"`
   847  	TargetDataIncludeCacheVast  bool                            `json:"targetDataIncludeCacheVast"`
   848  	EventsDataEnabledForAccount bool                            `json:"eventsDataEnabledForAccount"`
   849  	EventsDataEnabledForRequest bool                            `json:"eventsDataEnabledForRequest"`
   850  	DebugLog                    DebugLog                        `json:"debugLog,omitempty"`
   851  }
   852  
   853  type pbsBid struct {
   854  	Bid     *openrtb2.Bid          `json:"bid"`
   855  	BidType openrtb_ext.BidType    `json:"bidType"`
   856  	Bidder  openrtb_ext.BidderName `json:"bidder"`
   857  }
   858  
   859  type mockCache struct {
   860  	scheme string
   861  	host   string
   862  	path   string
   863  	items  []prebid_cache_client.Cacheable
   864  }
   865  
   866  func (c *mockCache) GetExtCacheData() (scheme string, host string, path string) {
   867  	return c.scheme, c.host, c.path
   868  }
   869  
   870  func (c *mockCache) GetPutUrl() string {
   871  	return ""
   872  }
   873  
   874  func (c *mockCache) PutJson(ctx context.Context, values []prebid_cache_client.Cacheable) ([]string, []error) {
   875  	c.items = values
   876  	return []string{"", "", "", "", ""}, nil
   877  }