From 9f908f6035386fd9fb7d86fc41752c195b899ee2 Mon Sep 17 00:00:00 2001 From: Brian Sardo <1168933+bsardo@users.noreply.github.com> Date: Wed, 12 Jan 2022 19:50:29 -0500 Subject: [PATCH] Add ORTB 2.4 schain support (#2108) --- endpoints/openrtb2/auction.go | 33 ++++- endpoints/openrtb2/auction_test.go | 128 ++++++++++++++++ exchange/utils.go | 65 +-------- exchange/utils_test.go | 219 ++++------------------------ openrtb_ext/request.go | 8 +- openrtb_ext/request_wrapper.go | 152 ++++++++++++++++++- openrtb_ext/request_wrapper_test.go | 23 ++- openrtb_ext/source.go | 6 + schain/schain.go | 25 ++++ schain/schain_test.go | 103 +++++++++++++ schain/schainwriter.go | 78 ++++++++++ schain/schainwriter_test.go | 182 +++++++++++++++++++++++ 12 files changed, 764 insertions(+), 258 deletions(-) create mode 100644 openrtb_ext/source.go create mode 100644 schain/schain.go create mode 100644 schain/schain_test.go create mode 100644 schain/schainwriter.go create mode 100644 schain/schainwriter_test.go diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go index 984d0e5d59a..4d68db50c0c 100644 --- a/endpoints/openrtb2/auction.go +++ b/endpoints/openrtb2/auction.go @@ -35,6 +35,7 @@ import ( "github.com/prebid/prebid-server/prebid_cache_client" "github.com/prebid/prebid-server/privacy/ccpa" "github.com/prebid/prebid-server/privacy/lmt" + "github.com/prebid/prebid-server/schain" "github.com/prebid/prebid-server/stored_requests" "github.com/prebid/prebid-server/stored_requests/backends/empty_fetcher" "github.com/prebid/prebid-server/usersync" @@ -528,6 +529,10 @@ func (deps *endpointDeps) validateRequest(req *openrtb_ext.RequestWrapper, isAmp } } + if err := mapSChains(req); err != nil { + return []error{err} + } + if err := validateOrFillChannel(req, isAmp); err != nil { return []error{err} } @@ -592,6 +597,32 @@ func (deps *endpointDeps) validateRequest(req *openrtb_ext.RequestWrapper, isAmp return errL } +// mapSChains maps an schain defined in an ORTB 2.4 location (req.ext.schain) to the ORTB 2.5 location +// (req.source.ext.schain) if no ORTB 2.5 schain (req.source.ext.schain, req.ext.prebid.schains) exists. +// An ORTB 2.4 schain is always deleted from the 2.4 location regardless of whether an ORTB 2.5 schain exists. +func mapSChains(req *openrtb_ext.RequestWrapper) error { + reqExt, err := req.GetRequestExt() + if err != nil { + return fmt.Errorf("req.ext is invalid: %v", err) + } + sourceExt, err := req.GetSourceExt() + if err != nil { + return fmt.Errorf("source.ext is invalid: %v", err) + } + + reqExtSChain := reqExt.GetSChain() + reqExt.SetSChain(nil) + + if reqPrebid := reqExt.GetPrebid(); reqPrebid != nil && reqPrebid.SChains != nil { + return nil + } else if sourceExt.GetSChain() != nil { + return nil + } else if reqExtSChain != nil { + sourceExt.SetSChain(reqExtSChain) + } + return nil +} + func validateAndFillSourceTID(req *openrtb2.BidRequest) error { if req.Source == nil { req.Source = &openrtb2.Source{} @@ -621,7 +652,7 @@ func (deps *endpointDeps) validateBidAdjustmentFactors(adjustmentFactors map[str } func validateSChains(sChains []*openrtb_ext.ExtRequestPrebidSChain) error { - _, err := exchange.BidderToPrebidSChains(sChains) + _, err := schain.BidderToPrebidSChains(sChains) return err } diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go index ad92ab7d92b..c42580fc1e7 100644 --- a/endpoints/openrtb2/auction_test.go +++ b/endpoints/openrtb2/auction_test.go @@ -2450,6 +2450,134 @@ func TestSChainInvalid(t *testing.T) { assert.ElementsMatch(t, errL, []error{expectedError}) } +func TestMapSChains(t *testing.T) { + const seller1SChain string = `"schain":{"complete":1,"nodes":[{"asi":"directseller1.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}` + const seller2SChain string = `"schain":{"complete":2,"nodes":[{"asi":"directseller2.com","sid":"00002","rid":"BidRequest2","hp":2}],"ver":"2.0"}` + + seller1SChainUnpacked := openrtb_ext.ExtRequestPrebidSChainSChain{ + Complete: 1, + Nodes: []*openrtb_ext.ExtRequestPrebidSChainSChainNode{{ + ASI: "directseller1.com", + SID: "00001", + RID: "BidRequest1", + HP: 1, + }}, + Ver: "1.0", + } + + tests := []struct { + description string + bidRequest openrtb2.BidRequest + wantReqExtSChain *openrtb_ext.ExtRequestPrebidSChainSChain + wantSourceExtSChain *openrtb_ext.ExtRequestPrebidSChainSChain + wantError bool + }{ + { + description: "invalid req.ext", + bidRequest: openrtb2.BidRequest{ + Ext: json.RawMessage(`{"prebid":{"schains":invalid}}`), + Source: &openrtb2.Source{ + Ext: json.RawMessage(`{}`), + }, + }, + wantError: true, + }, + { + description: "invalid source.ext", + bidRequest: openrtb2.BidRequest{ + Ext: json.RawMessage(`{}`), + Source: &openrtb2.Source{ + Ext: json.RawMessage(`{"schain":invalid}}`), + }, + }, + wantError: true, + }, + { + description: "req.ext.prebid.schains, req.source.ext.schain and req.ext.schain are nil", + bidRequest: openrtb2.BidRequest{ + Ext: json.RawMessage(`{}`), + Source: &openrtb2.Source{ + Ext: json.RawMessage(`{}`), + }, + }, + wantReqExtSChain: nil, + wantSourceExtSChain: nil, + }, + { + description: "req.ext.prebid.schains is not nil", + bidRequest: openrtb2.BidRequest{ + Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],` + seller1SChain + `}]}}`), + Source: &openrtb2.Source{ + Ext: json.RawMessage(`{}`), + }, + }, + wantReqExtSChain: nil, + wantSourceExtSChain: nil, + }, + { + description: "req.source.ext is not nil", + bidRequest: openrtb2.BidRequest{ + Ext: json.RawMessage(`{}`), + Source: &openrtb2.Source{ + Ext: json.RawMessage(`{` + seller1SChain + `}`), + }, + }, + wantReqExtSChain: nil, + wantSourceExtSChain: &seller1SChainUnpacked, + }, + { + description: "req.ext.schain is not nil", + bidRequest: openrtb2.BidRequest{ + Ext: json.RawMessage(`{` + seller1SChain + `}`), + Source: &openrtb2.Source{ + Ext: json.RawMessage(`{}`), + }, + }, + wantReqExtSChain: nil, + wantSourceExtSChain: &seller1SChainUnpacked, + }, + { + description: "req.source.ext.schain and req.ext.schain are not nil", + bidRequest: openrtb2.BidRequest{ + Ext: json.RawMessage(`{` + seller2SChain + `}`), + Source: &openrtb2.Source{ + Ext: json.RawMessage(`{` + seller1SChain + `}`), + }, + }, + wantReqExtSChain: nil, + wantSourceExtSChain: &seller1SChainUnpacked, + }, + } + + for _, tt := range tests { + reqWrapper := openrtb_ext.RequestWrapper{ + BidRequest: &tt.bidRequest, + } + + err := mapSChains(&reqWrapper) + + if tt.wantError { + assert.NotNil(t, err, tt.description) + } else { + assert.Nil(t, err, tt.description) + + reqExt, err := reqWrapper.GetRequestExt() + if err != nil { + assert.Fail(t, "Error getting request ext from wrapper", tt.description) + } + reqExtSChain := reqExt.GetSChain() + assert.Equal(t, tt.wantReqExtSChain, reqExtSChain, tt.description) + + sourceExt, err := reqWrapper.GetSourceExt() + if err != nil { + assert.Fail(t, "Error getting source ext from wrapper", tt.description) + } + sourceExtSChain := sourceExt.GetSChain() + assert.Equal(t, tt.wantSourceExtSChain, sourceExtSChain, tt.description) + } + } +} + func TestGetAccountID(t *testing.T) { testPubID := "test-pub" testParentAccount := "test-account" diff --git a/exchange/utils.go b/exchange/utils.go index 48f76b2a734..ec5b666d609 100644 --- a/exchange/utils.go +++ b/exchange/utils.go @@ -20,6 +20,7 @@ import ( "github.com/prebid/prebid-server/privacy" "github.com/prebid/prebid-server/privacy/ccpa" "github.com/prebid/prebid-server/privacy/lmt" + "github.com/prebid/prebid-server/schain" ) var integrationTypeMap = map[metrics.RequestType]config.IntegrationType{ @@ -31,23 +32,6 @@ var integrationTypeMap = map[metrics.RequestType]config.IntegrationType{ const unknownBidder string = "" -func BidderToPrebidSChains(sChains []*openrtb_ext.ExtRequestPrebidSChain) (map[string]*openrtb_ext.ExtRequestPrebidSChainSChain, error) { - bidderToSChains := make(map[string]*openrtb_ext.ExtRequestPrebidSChainSChain) - - for _, schainWrapper := range sChains { - for _, bidder := range schainWrapper.Bidders { - if _, present := bidderToSChains[bidder]; present { - return nil, fmt.Errorf("request.ext.prebid.schains contains multiple schains for bidder %s; "+ - "it must contain no more than one per bidder.", bidder) - } else { - bidderToSChains[bidder] = &schainWrapper.SChain - } - } - } - - return bidderToSChains, nil -} - // cleanOpenRTBRequests splits the input request into requests which are sanitized for each bidder. Intended behavior is: // // 1. BidRequest.Imp[].Ext will only contain the "prebid" field and a "bidder" field which has the params for the intended Bidder. @@ -228,14 +212,9 @@ func getAuctionBidderRequests(req AuctionRequest, return nil, []error{err} } - var sChainsByBidder map[string]*openrtb_ext.ExtRequestPrebidSChainSChain - - // Quick extra wrapper until RequestWrapper makes its way into CleanRequests - if requestExt != nil { - sChainsByBidder, err = BidderToPrebidSChains(requestExt.Prebid.SChains) - if err != nil { - return nil, []error{err} - } + sChainWriter, err := schain.NewSChainWriter(requestExt) + if err != nil { + return nil, []error{err} } var errs []error @@ -245,7 +224,7 @@ func getAuctionBidderRequests(req AuctionRequest, reqCopy := *req.BidRequest reqCopy.Imp = imps - prepareSource(&reqCopy, bidder, sChainsByBidder) + sChainWriter.Write(&reqCopy, bidder) if len(bidderParamsInReqExt) != 0 { @@ -318,40 +297,6 @@ func getExtJson(req *openrtb2.BidRequest, unpackedExt *openrtb_ext.ExtRequest) ( return json.Marshal(extCopy) } -func prepareSource(req *openrtb2.BidRequest, bidder string, sChainsByBidder map[string]*openrtb_ext.ExtRequestPrebidSChainSChain) { - const sChainWildCard = "*" - var selectedSChain *openrtb_ext.ExtRequestPrebidSChainSChain - - wildCardSChain := sChainsByBidder[sChainWildCard] - bidderSChain := sChainsByBidder[bidder] - - // source should not be modified - if bidderSChain == nil && wildCardSChain == nil { - return - } - - if bidderSChain != nil { - selectedSChain = bidderSChain - } else { - selectedSChain = wildCardSChain - } - - // set source - if req.Source == nil { - req.Source = &openrtb2.Source{} - } else { - sourceCopy := *req.Source - req.Source = &sourceCopy - } - schain := openrtb_ext.ExtRequestPrebidSChain{ - SChain: *selectedSChain, - } - sourceExt, err := json.Marshal(schain) - if err == nil { - req.Source.Ext = sourceExt - } -} - // extractBuyerUIDs parses the values from user.ext.prebid.buyeruids, and then deletes those values from the ext. // This prevents a Bidder from using these values to figure out who else is involved in the Auction. func extractBuyerUIDs(user *openrtb2.User) (map[string]string, error) { diff --git a/exchange/utils_test.go b/exchange/utils_test.go index c10e294f7ad..f67b6ffbefe 100644 --- a/exchange/utils_test.go +++ b/exchange/utils_test.go @@ -837,83 +837,60 @@ func TestCleanOpenRTBRequestsCOPPA(t *testing.T) { } func TestCleanOpenRTBRequestsSChain(t *testing.T) { + const seller1SChain string = `"schain":{"complete":1,"nodes":[{"asi":"directseller1.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}` + const seller2SChain string = `"schain":{"complete":2,"nodes":[{"asi":"directseller2.com","sid":"00002","rid":"BidRequest2","hp":2}],"ver":"2.0"}` + testCases := []struct { description string inExt json.RawMessage inSourceExt json.RawMessage - outSourceExt json.RawMessage outRequestExt json.RawMessage + outSourceExt json.RawMessage hasError bool }{ { - description: "Empty root ext and source ext, nil unmarshaled ext", - inExt: nil, - inSourceExt: json.RawMessage(``), - outSourceExt: json.RawMessage(``), - outRequestExt: json.RawMessage(``), - hasError: false, - }, - { - description: "Empty root ext, source ext, and unmarshaled ext", - inExt: json.RawMessage(``), - inSourceExt: json.RawMessage(``), - outSourceExt: json.RawMessage(``), - outRequestExt: json.RawMessage(``), - hasError: false, + description: "source.ext is nil", + inExt: json.RawMessage{}, + inSourceExt: nil, + outRequestExt: json.RawMessage{}, + outSourceExt: nil, }, { - description: "No schains in root ext and empty source ext. Unmarshaled ext is equivalent to root ext", - inSourceExt: json.RawMessage(``), - inExt: json.RawMessage(`{"prebid":{"schains":[]}}`), - outSourceExt: json.RawMessage(``), - outRequestExt: json.RawMessage(`{"prebid":{}}`), - hasError: false, + description: "source.ext is defined with length 0", + inExt: json.RawMessage{}, + inSourceExt: json.RawMessage{}, + outRequestExt: json.RawMessage{}, + outSourceExt: json.RawMessage{}, }, { - description: "Use source schain -- no bidder schain or wildcard schain in ext.prebid.schains. Unmarshaled ext is equivalent to root ext", - inSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"example.com","sid":"example1","rid":"ExampleReq1","hp":1}],"ver":"1.0"}}`), - inExt: json.RawMessage(`{"prebid":{"schains":[{"bidders":["bidder1"],"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}]}}`), - outSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"example.com","sid":"example1","rid":"ExampleReq1","hp":1}],"ver":"1.0"}}`), - outRequestExt: json.RawMessage(`{"prebid":{}}`), - hasError: false, + description: "ORTB 2.5 chain at source.ext.schain", + inExt: json.RawMessage{}, + inSourceExt: json.RawMessage(`{` + seller1SChain + `}`), + outRequestExt: json.RawMessage{}, + outSourceExt: json.RawMessage(`{` + seller1SChain + `}`), }, { - description: "Use schain for bidder in ext.prebid.schains. Unmarshaled ext is equivalent to root ext", - inSourceExt: json.RawMessage(``), - inExt: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}]}}`), - outSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}`), + description: "ORTB 2.5 schain at request.ext.prebid.schains", + inExt: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],` + seller1SChain + `}]}}`), + inSourceExt: json.RawMessage{}, outRequestExt: json.RawMessage(`{"prebid":{}}`), - hasError: false, + outSourceExt: json.RawMessage(`{` + seller1SChain + `}`), }, { - description: "Use wildcard schain in ext.prebid.schains. Unmarshaled ext is equivalent to root ext", - inSourceExt: json.RawMessage(``), - inExt: json.RawMessage(`{"prebid":{"schains":[{"bidders":["*"],"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}]}}`), - outSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}`), - outRequestExt: json.RawMessage(`{"prebid":{}}`), - hasError: false, - }, - { - description: "Use schain for bidder in ext.prebid.schains instead of wildcard. Unmarshaled ext is equivalent to root ext", - inSourceExt: json.RawMessage(``), - inExt: json.RawMessage(`{"prebid":{"aliases":{"appnexus":"alias1"},"schains":[{"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}, {"bidders":["*"],"schain":{"complete":1,"nodes":[{"asi":"wildcard.com","sid":"wildcard1","rid":"WildcardReq1","hp":1}],"ver":"1.0"}} ]}}`), - outSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}`), - outRequestExt: json.RawMessage(`{"prebid":{"aliases":{"appnexus":"alias1"}}}`), - hasError: false, - }, - { - description: "Use source schain -- multiple (two) bidder schains in ext.prebid.schains. Unmarshaled ext is equivalent to root ext", - inSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"example.com","sid":"example1","rid":"ExampleReq1","hp":1}],"ver":"1.0"}}`), - inExt: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller1.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}, {"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller2.com","sid":"00002","rid":"BidRequest2","hp":1}],"ver":"1.0"}}]}}`), - outSourceExt: nil, + description: "schainwriter instantation error -- multiple bidder schains in ext.prebid.schains.", + inExt: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],` + seller1SChain + `},{"bidders":["appnexus"],` + seller2SChain + `}]}}`), + inSourceExt: json.RawMessage(`{` + seller1SChain + `}`), outRequestExt: nil, + outSourceExt: nil, hasError: true, }, } for _, test := range testCases { req := newBidRequest(t) - req.Source.Ext = test.inSourceExt + if test.inSourceExt != nil { + req.Source.Ext = test.inSourceExt + } var extRequest *openrtb_ext.ExtRequest if test.inExt != nil { @@ -2173,101 +2150,6 @@ func TestRandomizeList(t *testing.T) { } } -func TestBidderToPrebidChains(t *testing.T) { - input := openrtb_ext.ExtRequest{ - Prebid: openrtb_ext.ExtRequestPrebid{ - SChains: []*openrtb_ext.ExtRequestPrebidSChain{ - { - Bidders: []string{"Bidder1", "Bidder2"}, - SChain: openrtb_ext.ExtRequestPrebidSChainSChain{ - Complete: 1, - Nodes: []*openrtb_ext.ExtRequestPrebidSChainSChainNode{ - { - ASI: "asi1", - SID: "sid1", - Name: "name1", - RID: "rid1", - Domain: "domain1", - HP: 1, - }, - { - ASI: "asi2", - SID: "sid2", - Name: "name2", - RID: "rid2", - Domain: "domain2", - HP: 2, - }, - }, - Ver: "version1", - }, - }, - { - Bidders: []string{"Bidder3", "Bidder4"}, - SChain: openrtb_ext.ExtRequestPrebidSChainSChain{}, - }, - }, - }, - } - - output, err := BidderToPrebidSChains(input.Prebid.SChains) - - assert.Nil(t, err) - assert.Equal(t, len(output), 4) - assert.Same(t, output["Bidder1"], &input.Prebid.SChains[0].SChain) - assert.Same(t, output["Bidder2"], &input.Prebid.SChains[0].SChain) - assert.Same(t, output["Bidder3"], &input.Prebid.SChains[1].SChain) - assert.Same(t, output["Bidder4"], &input.Prebid.SChains[1].SChain) -} - -func TestBidderToPrebidChainsDiscardMultipleChainsForBidder(t *testing.T) { - input := openrtb_ext.ExtRequest{ - Prebid: openrtb_ext.ExtRequestPrebid{ - SChains: []*openrtb_ext.ExtRequestPrebidSChain{ - { - Bidders: []string{"Bidder1"}, - SChain: openrtb_ext.ExtRequestPrebidSChainSChain{}, - }, - { - Bidders: []string{"Bidder1", "Bidder2"}, - SChain: openrtb_ext.ExtRequestPrebidSChainSChain{}, - }, - }, - }, - } - - output, err := BidderToPrebidSChains(input.Prebid.SChains) - - assert.NotNil(t, err) - assert.Nil(t, output) -} - -func TestBidderToPrebidChainsNilSChains(t *testing.T) { - input := openrtb_ext.ExtRequest{ - Prebid: openrtb_ext.ExtRequestPrebid{ - SChains: nil, - }, - } - - output, err := BidderToPrebidSChains(input.Prebid.SChains) - - assert.Nil(t, err) - assert.Equal(t, len(output), 0) -} - -func TestBidderToPrebidChainsZeroLengthSChains(t *testing.T) { - input := openrtb_ext.ExtRequest{ - Prebid: openrtb_ext.ExtRequestPrebid{ - SChains: []*openrtb_ext.ExtRequestPrebidSChain{}, - }, - } - - output, err := BidderToPrebidSChains(input.Prebid.SChains) - - assert.Nil(t, err) - assert.Equal(t, len(output), 0) -} - func TestRemoveUnpermissionedEids(t *testing.T) { bidder := "bidderA" @@ -2745,47 +2627,6 @@ func TestBuildXPrebidHeader(t *testing.T) { } } -func TestSourceExtSChainCopied(t *testing.T) { - bidRequest := newBidRequest(t) - - bidderSchains := map[string]*openrtb_ext.ExtRequestPrebidSChainSChain{ - "bidder1": { - Ver: "1.0", - Complete: 1, - Nodes: []*openrtb_ext.ExtRequestPrebidSChainSChainNode{ - { - ASI: "bidder1.com", - SID: "0001", - HP: 1, - }, - }, - }, - "bidder2": { - Ver: "1.0", - Complete: 1, - Nodes: []*openrtb_ext.ExtRequestPrebidSChainSChainNode{ - { - ASI: "bidder2.com", - SID: "0002", - HP: 1, - }, - }, - }, - } - - copy1 := *bidRequest - originalTid := copy1.Source.TID - prepareSource(©1, "bidder1", bidderSchains) - copy2 := *bidRequest - copy2.Source.TID = "new TID" - prepareSource(©2, "bidder2", bidderSchains) - - assert.Equal(t, json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"bidder1.com","sid":"0001","hp":1}],"ver":"1.0"}}`), copy1.Source.Ext, "First schain was overwritten or not set") - assert.Equal(t, originalTid, copy1.Source.TID, "Original TID was overwritten") - assert.Equal(t, json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"bidder2.com","sid":"0002","hp":1}],"ver":"1.0"}}`), copy2.Source.Ext, "Second schain was overwritten or not set") - assert.Equal(t, "new TID", copy2.Source.TID, "New TID was not set") -} - func TestCleanOpenRTBRequestsSChainMultipleBidders(t *testing.T) { req := &openrtb2.BidRequest{ Site: &openrtb2.Site{}, diff --git a/openrtb_ext/request.go b/openrtb_ext/request.go index dba30863f6c..ecd913b1610 100644 --- a/openrtb_ext/request.go +++ b/openrtb_ext/request.go @@ -24,7 +24,8 @@ const MaxDecimalFigures int = 15 // ExtRequest defines the contract for bidrequest.ext type ExtRequest struct { - Prebid ExtRequestPrebid `json:"prebid"` + Prebid ExtRequestPrebid `json:"prebid"` + SChain *ExtRequestPrebidSChainSChain `json:"schain,omitempty"` } // ExtRequestPrebid defines the contract for bidrequest.ext.prebid @@ -96,11 +97,6 @@ type ExtRequestPrebidSChainSChainNode struct { Ext json.RawMessage `json:"ext,omitempty"` } -// SourceExt defines the contract for bidrequest.source.ext -type SourceExt struct { - SChain ExtRequestPrebidSChainSChain `json:"schain"` -} - // ExtRequestPrebidChannel defines the contract for bidrequest.ext.prebid.channel type ExtRequestPrebidChannel struct { Name string `json:"name"` diff --git a/openrtb_ext/request_wrapper.go b/openrtb_ext/request_wrapper.go index 9219bbdbc9e..dfc2a41fc44 100644 --- a/openrtb_ext/request_wrapper.go +++ b/openrtb_ext/request_wrapper.go @@ -38,6 +38,7 @@ type RequestWrapper struct { appExt *AppExt regExt *RegExt siteExt *SiteExt + sourceExt *SourceExt } func (rw *RequestWrapper) GetUserExt() (*UserExt, error) { @@ -107,6 +108,17 @@ func (rw *RequestWrapper) GetSiteExt() (*SiteExt, error) { return rw.siteExt, rw.siteExt.unmarshal(rw.Site.Ext) } +func (rw *RequestWrapper) GetSourceExt() (*SourceExt, error) { + if rw.sourceExt != nil { + return rw.sourceExt, nil + } + rw.sourceExt = &SourceExt{} + if rw.BidRequest == nil || rw.Source == nil || rw.Source.Ext == nil { + return rw.sourceExt, rw.sourceExt.unmarshal(json.RawMessage{}) + } + return rw.sourceExt, rw.sourceExt.unmarshal(rw.Source.Ext) +} + func (rw *RequestWrapper) RebuildRequest() error { if rw.BidRequest == nil { return errors.New("Requestwrapper Sync called on a nil BidRequest") @@ -130,6 +142,9 @@ func (rw *RequestWrapper) RebuildRequest() error { if err := rw.rebuildSiteExt(); err != nil { return err } + if err := rw.rebuildSourceExt(); err != nil { + return err + } return nil } @@ -215,6 +230,20 @@ func (rw *RequestWrapper) rebuildSiteExt() error { return nil } +func (rw *RequestWrapper) rebuildSourceExt() error { + if rw.Source == nil && rw.sourceExt != nil && rw.sourceExt.Dirty() { + rw.Source = &openrtb2.Source{} + } + if rw.sourceExt != nil && rw.sourceExt.Dirty() { + sourceJson, err := rw.sourceExt.marshal() + if err != nil { + return err + } + rw.Source.Ext = sourceJson + } + return nil +} + // --------------------------------------------------------------- // UserExt provides an interface for request.user.ext // --------------------------------------------------------------- @@ -382,6 +411,8 @@ type RequestExt struct { extDirty bool prebid *ExtRequestPrebid prebidDirty bool + schain *ExtRequestPrebidSChainSChain // ORTB 2.4 location + schainDirty bool } func (re *RequestExt) unmarshal(extJson json.RawMessage) error { @@ -401,6 +432,11 @@ func (re *RequestExt) unmarshal(extJson json.RawMessage) error { re.prebid = &ExtRequestPrebid{} err = json.Unmarshal(prebidJson, re.prebid) } + schainJson, hasSChain := re.ext["schain"] + if hasSChain { + re.schain = &ExtRequestPrebidSChainSChain{} + err = json.Unmarshal(schainJson, re.schain) + } return err } @@ -419,6 +455,22 @@ func (re *RequestExt) marshal() (json.RawMessage, error) { re.prebidDirty = false } + if re.schainDirty { + if re.schain == nil { + } + + schainJson, err := json.Marshal(re.schain) + if err != nil { + return nil, err + } + if len(schainJson) > 2 && re.schain != nil { + re.ext["schain"] = json.RawMessage(schainJson) + } else { + delete(re.ext, "schain") + } + re.schainDirty = false + } + re.extDirty = false if len(re.ext) == 0 { return nil, nil @@ -427,7 +479,7 @@ func (re *RequestExt) marshal() (json.RawMessage, error) { } func (re *RequestExt) Dirty() bool { - return re.extDirty || re.prebidDirty + return re.extDirty || re.prebidDirty || re.schainDirty } func (re *RequestExt) GetExt() map[string]json.RawMessage { @@ -456,6 +508,22 @@ func (re *RequestExt) SetPrebid(prebid *ExtRequestPrebid) { re.prebidDirty = true } +// These schain methods on the request.ext are only for ORTB 2.4 backwards compatibility and +// should not be used for any other purposes. To access ORTB 2.5 schains, see source.ext.schain +// or request.ext.prebid.schains. +func (re *RequestExt) GetSChain() *ExtRequestPrebidSChainSChain { + if re.schain == nil { + return nil + } + schain := *re.schain + return &schain +} + +func (re *RequestExt) SetSChain(schain *ExtRequestPrebidSChainSChain) { + re.schain = schain + re.schainDirty = true +} + // --------------------------------------------------------------- // DeviceExt provides an interface for request.device.ext // --------------------------------------------------------------- @@ -785,3 +853,85 @@ func (se *SiteExt) SetUSPrivacy(amp int8) { se.amp = amp se.ampDirty = true } + +// --------------------------------------------------------------- +// SourceExt provides an interface for request.source.ext +// --------------------------------------------------------------- + +type SourceExt struct { + ext map[string]json.RawMessage + extDirty bool + schain *ExtRequestPrebidSChainSChain + schainDirty bool +} + +func (se *SourceExt) unmarshal(extJson json.RawMessage) error { + if len(se.ext) != 0 || se.Dirty() { + return nil + } + se.ext = make(map[string]json.RawMessage) + if len(extJson) == 0 { + return nil + } + err := json.Unmarshal(extJson, &se.ext) + if err != nil { + return err + } + schainJson, hasSChain := se.ext["schain"] + if hasSChain { + err = json.Unmarshal(schainJson, &se.schain) + } + + return err +} + +func (se *SourceExt) marshal() (json.RawMessage, error) { + if se.schainDirty { + schainJson, err := json.Marshal(se.schain) + if err != nil { + return nil, err + } + if len(schainJson) > 2 { + se.ext["schain"] = json.RawMessage(schainJson) + } else { + delete(se.ext, "schain") + } + se.schainDirty = false + } + + se.extDirty = false + if len(se.ext) == 0 { + return nil, nil + } + return json.Marshal(se.ext) +} + +func (se *SourceExt) Dirty() bool { + return se.extDirty || se.schainDirty +} + +func (se *SourceExt) GetExt() map[string]json.RawMessage { + ext := make(map[string]json.RawMessage) + for k, v := range se.ext { + ext[k] = v + } + return ext +} + +func (se *SourceExt) SetExt(ext map[string]json.RawMessage) { + se.ext = ext + se.extDirty = true +} + +func (se *SourceExt) GetSChain() *ExtRequestPrebidSChainSChain { + if se.schain == nil { + return nil + } + schain := *se.schain + return &schain +} + +func (se *SourceExt) SetSChain(schain *ExtRequestPrebidSChainSChain) { + se.schain = schain + se.schainDirty = true +} diff --git a/openrtb_ext/request_wrapper_test.go b/openrtb_ext/request_wrapper_test.go index 06cad49aedf..60ef1a33263 100644 --- a/openrtb_ext/request_wrapper_test.go +++ b/openrtb_ext/request_wrapper_test.go @@ -1,6 +1,7 @@ package openrtb_ext import ( + "encoding/json" "testing" "github.com/stretchr/testify/assert" @@ -14,9 +15,29 @@ func TestUserExt(t *testing.T) { userExt.unmarshal(nil) assert.Equal(t, false, userExt.Dirty(), "New UserExt should not be dirty.") assert.Nil(t, userExt.GetConsent(), "Empty UserExt should have nil consent") + assert.Nil(t, userExt.GetEid(), "Empty UserExt should have nil eid") + assert.Nil(t, userExt.GetPrebid(), "Empty UserExt should have nil prebid") newConsent := "NewConsent" userExt.SetConsent(&newConsent) - assert.Equal(t, "NewConsent", *userExt.GetConsent()) + assert.Equal(t, "NewConsent", *userExt.GetConsent(), "UserExt consent is incorrect") + newEid := []ExtUserEid{{}} + userExt.SetEid(&newEid) + assert.Equal(t, []ExtUserEid{{}}, *userExt.GetEid(), "UserExt eid is incorrect") + + buyerIDs := map[string]string{"buyer": "id"} + newPrebid := ExtUserPrebid{BuyerUIDs: buyerIDs} + userExt.SetPrebid(&newPrebid) + assert.Equal(t, ExtUserPrebid{BuyerUIDs: buyerIDs}, *userExt.GetPrebid(), "UserExt prebid is icorrect") + + assert.Equal(t, true, userExt.Dirty(), "UserExt should be dirty after field updates") + + updatedUserExt, err := userExt.marshal() + assert.Nil(t, err, "Marshalling UserExt after updating should not cause an error") + + expectedUserExt := json.RawMessage(`{"consent":"NewConsent","prebid":{"buyeruids":{"buyer":"id"}},"eids":[{"source":""}]}`) + assert.JSONEq(t, string(updatedUserExt), string(expectedUserExt), "Marshalled UserExt is incorrect") + + assert.Equal(t, false, userExt.Dirty(), "UserExt should not be dirty after marshalling") } diff --git a/openrtb_ext/source.go b/openrtb_ext/source.go new file mode 100644 index 00000000000..70b291b11fc --- /dev/null +++ b/openrtb_ext/source.go @@ -0,0 +1,6 @@ +package openrtb_ext + +// ExtSource defines the contract for bidrequest.source.ext +type ExtSource struct { + SChain *ExtRequestPrebidSChainSChain `json:"schain"` +} diff --git a/schain/schain.go b/schain/schain.go new file mode 100644 index 00000000000..785c7e24d7f --- /dev/null +++ b/schain/schain.go @@ -0,0 +1,25 @@ +package schain + +import ( + "fmt" + + "github.com/prebid/prebid-server/openrtb_ext" +) + +// BidderToPrebidSChains organizes the ORTB 2.5 multiple root schain nodes into a map of schain nodes by bidder +func BidderToPrebidSChains(sChains []*openrtb_ext.ExtRequestPrebidSChain) (map[string]*openrtb_ext.ExtRequestPrebidSChainSChain, error) { + bidderToSChains := make(map[string]*openrtb_ext.ExtRequestPrebidSChainSChain) + + for _, schainWrapper := range sChains { + for _, bidder := range schainWrapper.Bidders { + if _, present := bidderToSChains[bidder]; present { + return nil, fmt.Errorf("request.ext.prebid.schains contains multiple schains for bidder %s; "+ + "it must contain no more than one per bidder.", bidder) + } else { + bidderToSChains[bidder] = &schainWrapper.SChain + } + } + } + + return bidderToSChains, nil +} diff --git a/schain/schain_test.go b/schain/schain_test.go new file mode 100644 index 00000000000..22903d69e85 --- /dev/null +++ b/schain/schain_test.go @@ -0,0 +1,103 @@ +package schain + +import ( + "testing" + + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +func TestBidderToPrebidChains(t *testing.T) { + input := openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + SChains: []*openrtb_ext.ExtRequestPrebidSChain{ + { + Bidders: []string{"Bidder1", "Bidder2"}, + SChain: openrtb_ext.ExtRequestPrebidSChainSChain{ + Complete: 1, + Nodes: []*openrtb_ext.ExtRequestPrebidSChainSChainNode{ + { + ASI: "asi1", + SID: "sid1", + Name: "name1", + RID: "rid1", + Domain: "domain1", + HP: 1, + }, + { + ASI: "asi2", + SID: "sid2", + Name: "name2", + RID: "rid2", + Domain: "domain2", + HP: 2, + }, + }, + Ver: "version1", + }, + }, + { + Bidders: []string{"Bidder3", "Bidder4"}, + SChain: openrtb_ext.ExtRequestPrebidSChainSChain{}, + }, + }, + }, + } + + output, err := BidderToPrebidSChains(input.Prebid.SChains) + + assert.Nil(t, err) + assert.Equal(t, len(output), 4) + assert.Same(t, output["Bidder1"], &input.Prebid.SChains[0].SChain) + assert.Same(t, output["Bidder2"], &input.Prebid.SChains[0].SChain) + assert.Same(t, output["Bidder3"], &input.Prebid.SChains[1].SChain) + assert.Same(t, output["Bidder4"], &input.Prebid.SChains[1].SChain) +} + +func TestBidderToPrebidChainsDiscardMultipleChainsForBidder(t *testing.T) { + input := openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + SChains: []*openrtb_ext.ExtRequestPrebidSChain{ + { + Bidders: []string{"Bidder1"}, + SChain: openrtb_ext.ExtRequestPrebidSChainSChain{}, + }, + { + Bidders: []string{"Bidder1", "Bidder2"}, + SChain: openrtb_ext.ExtRequestPrebidSChainSChain{}, + }, + }, + }, + } + + output, err := BidderToPrebidSChains(input.Prebid.SChains) + + assert.NotNil(t, err) + assert.Nil(t, output) +} + +func TestBidderToPrebidChainsNilSChains(t *testing.T) { + input := openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + SChains: nil, + }, + } + + output, err := BidderToPrebidSChains(input.Prebid.SChains) + + assert.Nil(t, err) + assert.Equal(t, len(output), 0) +} + +func TestBidderToPrebidChainsZeroLengthSChains(t *testing.T) { + input := openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + SChains: []*openrtb_ext.ExtRequestPrebidSChain{}, + }, + } + + output, err := BidderToPrebidSChains(input.Prebid.SChains) + + assert.Nil(t, err) + assert.Equal(t, len(output), 0) +} diff --git a/schain/schainwriter.go b/schain/schainwriter.go new file mode 100644 index 00000000000..bf2f564017f --- /dev/null +++ b/schain/schainwriter.go @@ -0,0 +1,78 @@ +package schain + +import ( + "encoding/json" + + "github.com/mxmCherry/openrtb/v15/openrtb2" + "github.com/prebid/prebid-server/openrtb_ext" +) + +// NewSChainWriter creates an ORTB 2.5 schain writer instance +func NewSChainWriter(reqExt *openrtb_ext.ExtRequest) (*SChainWriter, error) { + if !extPrebidSChainExists(reqExt) { + return &SChainWriter{}, nil + } + + sChainsByBidder, err := BidderToPrebidSChains(reqExt.Prebid.SChains) + if err != nil { + return nil, err + } + + writer := SChainWriter{ + sChainsByBidder: sChainsByBidder, + } + return &writer, nil +} + +// SChainWriter is used to write the appropriate schain for a particular bidder defined in the ORTB 2.5 multi-schain +// location (req.ext.prebid.schain) to the ORTB 2.5 location (req.source.ext) +type SChainWriter struct { + sChainsByBidder map[string]*openrtb_ext.ExtRequestPrebidSChainSChain +} + +// Write selects an schain from the multi-schain ORTB 2.5 location (req.ext.prebid.schains) for the specified bidder +// and copies it to the ORTB 2.5 location (req.source.ext). If no schain exists for the bidder in the multi-schain +// location and no wildcard schain exists, the request is not modified. +func (w SChainWriter) Write(req *openrtb2.BidRequest, bidder string) { + const sChainWildCard = "*" + var selectedSChain *openrtb_ext.ExtRequestPrebidSChainSChain + + wildCardSChain := w.sChainsByBidder[sChainWildCard] + bidderSChain := w.sChainsByBidder[bidder] + + // source should not be modified + if bidderSChain == nil && wildCardSChain == nil { + return + } + + if bidderSChain != nil { + selectedSChain = bidderSChain + } else { + selectedSChain = wildCardSChain + } + + if req.Source == nil { + req.Source = &openrtb2.Source{} + } else { + sourceCopy := *req.Source + req.Source = &sourceCopy + } + schain := openrtb_ext.ExtRequestPrebidSChain{ + SChain: *selectedSChain, + } + sourceExt, err := json.Marshal(schain) + if err == nil { + req.Source.Ext = sourceExt + } +} + +// extPrebidSChainExists checks if an schain exists in the ORTB 2.5 req.ext.prebid.schain location +func extPrebidSChainExists(reqExt *openrtb_ext.ExtRequest) bool { + if reqExt == nil { + return false + } + if reqExt.Prebid.SChains == nil { + return false + } + return true +} diff --git a/schain/schainwriter_test.go b/schain/schainwriter_test.go new file mode 100644 index 00000000000..92dc550d5e6 --- /dev/null +++ b/schain/schainwriter_test.go @@ -0,0 +1,182 @@ +package schain + +import ( + "encoding/json" + "testing" + + "github.com/mxmCherry/openrtb/v15/openrtb2" + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +func TestSChainWriter(t *testing.T) { + + const seller1SChain string = `"schain":{"complete":1,"nodes":[{"asi":"directseller1.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}` + const seller2SChain string = `"schain":{"complete":2,"nodes":[{"asi":"directseller2.com","sid":"00002","rid":"BidRequest2","hp":2}],"ver":"2.0"}` + const seller3SChain string = `"schain":{"complete":3,"nodes":[{"asi":"directseller3.com","sid":"00003","rid":"BidRequest3","hp":3}],"ver":"3.0"}` + const sellerWildCardSChain string = `"schain":{"complete":1,"nodes":[{"asi":"wildcard1.com","sid":"wildcard1","rid":"WildcardReq1","hp":1}],"ver":"1.0"}` + + tests := []struct { + description string + giveRequest openrtb2.BidRequest + giveBidder string + wantRequest openrtb2.BidRequest + wantError bool + }{ + { + description: "nil source and nil ext.prebid.schains", + giveRequest: openrtb2.BidRequest{ + Ext: nil, + Source: nil, + }, + giveBidder: "appnexus", + wantRequest: openrtb2.BidRequest{ + Ext: nil, + Source: nil, + }, + }, + { + description: "Use source schain -- no bidder schain or wildcard schain in nil ext.prebid.schains", + giveRequest: openrtb2.BidRequest{ + Ext: json.RawMessage(`{}`), + Source: &openrtb2.Source{ + Ext: json.RawMessage(`{` + seller2SChain + `}`), + }, + }, + giveBidder: "appnexus", + wantRequest: openrtb2.BidRequest{ + Ext: json.RawMessage(`{}`), + Source: &openrtb2.Source{ + Ext: json.RawMessage(`{` + seller2SChain + `}`), + }, + }, + }, + { + description: "Use source schain -- no bidder schain or wildcard schain in not nil ext.prebid.schains", + giveRequest: openrtb2.BidRequest{ + Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],` + seller1SChain + `}]}}`), + Source: &openrtb2.Source{ + Ext: json.RawMessage(`{` + seller2SChain + `}`), + }, + }, + giveBidder: "rubicon", + wantRequest: openrtb2.BidRequest{ + Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],` + seller1SChain + `}]}}`), + Source: &openrtb2.Source{ + Ext: json.RawMessage(`{` + seller2SChain + `}`), + }, + }, + }, + { + description: "Use schain for bidder in ext.prebid.schains; ensure other ext.source field values are retained.", + giveRequest: openrtb2.BidRequest{ + Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],` + seller1SChain + `}]}}`), + Source: &openrtb2.Source{ + FD: 1, + TID: "tid data", + PChain: "pchain data", + Ext: json.RawMessage(`{` + seller2SChain + `}`), + }, + }, + giveBidder: "appnexus", + wantRequest: openrtb2.BidRequest{ + Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],` + seller1SChain + `}]}}`), + Source: &openrtb2.Source{ + FD: 1, + TID: "tid data", + PChain: "pchain data", + Ext: json.RawMessage(`{` + seller1SChain + `}`), + }, + }, + }, + { + description: "Use schain for bidder in ext.prebid.schains, nil req.source ", + giveRequest: openrtb2.BidRequest{ + Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],` + seller1SChain + `}]}}`), + Source: nil, + }, + giveBidder: "appnexus", + wantRequest: openrtb2.BidRequest{ + Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],` + seller1SChain + `}]}}`), + Source: &openrtb2.Source{ + Ext: json.RawMessage(`{` + seller1SChain + `}`), + }, + }, + }, + { + description: "Use wildcard schain in ext.prebid.schains.", + giveRequest: openrtb2.BidRequest{ + Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["*"],` + sellerWildCardSChain + `}]}}`), + Source: &openrtb2.Source{ + Ext: nil, + }, + }, + giveBidder: "appnexus", + wantRequest: openrtb2.BidRequest{ + Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["*"],` + sellerWildCardSChain + `}]}}`), + Source: &openrtb2.Source{ + Ext: json.RawMessage(`{` + sellerWildCardSChain + `}`), + }, + }, + }, + { + description: "Use schain for bidder in ext.prebid.schains instead of wildcard.", + giveRequest: openrtb2.BidRequest{ + Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],` + seller1SChain + `},{"bidders":["*"],` + sellerWildCardSChain + `}]}}`), + Source: &openrtb2.Source{ + Ext: nil, + }, + }, + giveBidder: "appnexus", + wantRequest: openrtb2.BidRequest{ + Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],` + seller1SChain + `},{"bidders":["*"],` + sellerWildCardSChain + `}]}}`), + Source: &openrtb2.Source{ + Ext: json.RawMessage(`{` + seller1SChain + `}`), + }, + }, + }, + { + description: "Use source schain -- multiple (two) bidder schains in ext.prebid.schains.", + giveRequest: openrtb2.BidRequest{ + Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],` + seller1SChain + `},{"bidders":["appnexus"],` + seller2SChain + `}]}}`), + Source: &openrtb2.Source{ + Ext: json.RawMessage(`{` + seller3SChain + `}`), + }, + }, + giveBidder: "appnexus", + wantRequest: openrtb2.BidRequest{ + Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],` + seller1SChain + `},{"bidders":["appnexus"],` + seller2SChain + `}]}}`), + Source: &openrtb2.Source{ + Ext: json.RawMessage(`{` + seller3SChain + `}`), + }, + }, + wantError: true, + }, + } + + for _, tt := range tests { + // unmarshal ext to get schains object needed to initialize writer + var reqExt *openrtb_ext.ExtRequest + if tt.giveRequest.Ext != nil { + reqExt = &openrtb_ext.ExtRequest{} + err := json.Unmarshal(tt.giveRequest.Ext, reqExt) + if err != nil { + t.Error("Unable to unmarshal request.ext") + } + } + + writer, err := NewSChainWriter(reqExt) + + if tt.wantError { + assert.NotNil(t, err) + assert.Nil(t, writer) + } else { + assert.Nil(t, err) + assert.NotNil(t, writer) + + writer.Write(&tt.giveRequest, tt.giveBidder) + + assert.Equal(t, tt.wantRequest, tt.giveRequest, tt.description) + } + } +}