diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go index 0eaf0e302..f6ac6dfd9 100644 --- a/endpoints/openrtb2/auction.go +++ b/endpoints/openrtb2/auction.go @@ -1778,7 +1778,7 @@ func (deps *endpointDeps) processStoredRequests(requestJson []byte, impInfo []Im } // Extract Passthrough from Merged Imp - passthrough, _, _, err := jsonparser.Get(resolvedImp, "ext", "prebid", "passthrough") + passthrough, _, _, err := getExtPrebidValue(resolvedImp, "passthrough") if err != nil && err != jsonparser.KeyPathNotFoundError { return nil, nil, []error{err} } @@ -1814,7 +1814,7 @@ func (deps *endpointDeps) processStoredRequests(requestJson []byte, impInfo []Im func parseImpInfo(requestJson []byte) (impData []ImpExtPrebidData, errs []error) { if impArray, dataType, _, err := jsonparser.Get(requestJson, "imp"); err == nil && dataType == jsonparser.Array { _, _ = jsonparser.ArrayEach(impArray, func(imp []byte, _ jsonparser.ValueType, _ int, _ error) { - impExtData, _, _, _ := jsonparser.Get(imp, "ext", "prebid") + impExtData, _, _, _ := getExtPrebidValue(imp) var impExtPrebid openrtb_ext.ExtImpPrebid if impExtData != nil { if err := jsonutil.Unmarshal(impExtData, &impExtPrebid); err != nil { @@ -1828,6 +1828,20 @@ func parseImpInfo(requestJson []byte) (impData []ImpExtPrebidData, errs []error) return } +// getExtPrebidValue reads from the prebid block of the given +// JSON. Top-level alias resolution: when ext.openads exists it is used +// in full; otherwise ext.prebid is used. ext.prebid is never consulted +// when ext.openads exists, even for subkeys missing from openads. +// Outbound emission is unaffected. +func getExtPrebidValue(data []byte, subKeys ...string) ([]byte, jsonparser.ValueType, int, error) { + rootKey := openrtb_ext.PrebidExtKey + if _, dt, _, err := jsonparser.Get(data, "ext", openrtb_ext.OpenAdsExtKey); err == nil && dt != jsonparser.NotExist { + rootKey = openrtb_ext.OpenAdsExtKey + } + path := append([]string{"ext", rootKey}, subKeys...) + return jsonparser.Get(data, path...) +} + type ImpExtPrebidData struct { Imp json.RawMessage ImpExtPrebid openrtb_ext.ExtImpPrebid @@ -1838,7 +1852,7 @@ type ImpExtPrebidData struct { // (e.g. malformed json, id not a string, etc). func getStoredRequestId(data []byte) (string, bool, error) { // These keys must be kept in sync with openrtb_ext.ExtStoredRequest - storedRequestId, dataType, _, err := jsonparser.Get(data, "ext", openrtb_ext.PrebidExtKey, "storedrequest", "id") + storedRequestId, dataType, _, err := getExtPrebidValue(data, "storedrequest", "id") if dataType == jsonparser.NotExist { return "", false, nil diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go index d245d72b5..727b8dbea 100644 --- a/endpoints/openrtb2/auction_test.go +++ b/endpoints/openrtb2/auction_test.go @@ -1078,6 +1078,71 @@ func TestReferer(t *testing.T) { } } +func TestParseImpInfoOpenAdsAlias(t *testing.T) { + tests := []struct { + name string + input string + wantImp openrtb_ext.ExtImpPrebid + }{ + { + name: "openads only at imp.ext", + input: `{"imp":[{"id":"imp1","ext":{"openads":{"storedrequest":{"id":"42"},"options":{"echovideoattrs":true}}}}]}`, + wantImp: openrtb_ext.ExtImpPrebid{StoredRequest: &openrtb_ext.ExtStoredRequest{ID: "42"}, Options: &openrtb_ext.Options{EchoVideoAttrs: true}}, + }, + { + name: "both keys, openads wins", + input: `{"imp":[{"id":"imp1","ext":{"prebid":{"storedrequest":{"id":"FROM_PREBID"}},"openads":{"storedrequest":{"id":"FROM_OPENADS"}}}}]}`, + wantImp: openrtb_ext.ExtImpPrebid{StoredRequest: &openrtb_ext.ExtStoredRequest{ID: "FROM_OPENADS"}}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + impInfo, errs := parseImpInfo([]byte(tc.input)) + assert.Empty(t, errs, "no errors expected") + if assert.Len(t, impInfo, 1) { + assert.Equal(t, tc.wantImp, impInfo[0].ImpExtPrebid) + } + }) + } +} + +func TestGetStoredRequestIdOpenAdsAlias(t *testing.T) { + tests := []struct { + name string + input string + wantID string + wantHas bool + }{ + { + name: "openads alias", + input: `{"ext":{"openads":{"storedrequest":{"id":"sr-99"}}}}`, + wantID: "sr-99", + wantHas: true, + }, + { + name: "openads wins over prebid", + input: `{"ext":{"prebid":{"storedrequest":{"id":"FROM_PREBID"}},"openads":{"storedrequest":{"id":"FROM_OPENADS"}}}}`, + wantID: "FROM_OPENADS", + wantHas: true, + }, + { + name: "neither", + input: `{"ext":{"data":{}}}`, + wantID: "", + wantHas: false, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + id, has, err := getStoredRequestId([]byte(tc.input)) + assert.NoError(t, err) + assert.Equal(t, tc.wantHas, has) + assert.Equal(t, tc.wantID, id) + }) + } +} + func TestParseImpInfoSingleImpression(t *testing.T) { expectedRes := []ImpExtPrebidData{ diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index 16b52873d..e12a082f6 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -5828,17 +5828,26 @@ func parseRequestAliases(r openrtb2.BidRequest) (map[string]string, error) { return nil, nil } - ext := struct { - Prebid struct { - Aliases map[string]string `json:"aliases"` - } `json:"prebid"` - }{} - + // Top-level alias resolution: if ext.openads is present it is used + // in full; ext.prebid is ignored entirely in that case. + var ext map[string]json.RawMessage if err := jsonutil.Unmarshal(r.Ext, &ext); err != nil { return nil, err } - - return ext.Prebid.Aliases, nil + src, ok := ext[openrtb_ext.OpenAdsExtKey] + if !ok { + src, ok = ext[openrtb_ext.PrebidExtKey] + } + if !ok { + return nil, nil + } + var pe struct { + Aliases map[string]string `json:"aliases"` + } + if err := jsonutil.Unmarshal(src, &pe); err != nil { + return nil, err + } + return pe.Aliases, nil } func getInfoFromImp(req *openrtb_ext.RequestWrapper) (json.RawMessage, string, error) { @@ -5851,9 +5860,14 @@ func getInfoFromImp(req *openrtb_ext.RequestWrapper) (json.RawMessage, string, e return nil, "", err } + // Top-level alias resolution: openads wins outright when present. + prebidJSON := bidderExts[openrtb_ext.OpenAdsExtKey] + if prebidJSON == nil { + prebidJSON = bidderExts[openrtb_ext.PrebidExtKey] + } var extPrebid openrtb_ext.ExtImpPrebid - if bidderExts[openrtb_ext.PrebidExtKey] != nil { - if err := jsonutil.UnmarshalValid(bidderExts[openrtb_ext.PrebidExtKey], &extPrebid); err != nil { + if prebidJSON != nil { + if err := jsonutil.UnmarshalValid(prebidJSON, &extPrebid); err != nil { return nil, "", err } } diff --git a/exchange/exchangetest/aliases-openads-key.json b/exchange/exchangetest/aliases-openads-key.json new file mode 100644 index 000000000..fdb5a7527 --- /dev/null +++ b/exchange/exchangetest/aliases-openads-key.json @@ -0,0 +1,136 @@ +{ + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "openads": { + "bidder": { + "thetradedesk": { + "publisherId": "pub1", + "supplySourceId": "src1" + }, + "ttdalias": { + "publisherId": "pub2", + "supplySourceId": "src2" + } + } + } + } + } + ], + "ext": { + "openads": { + "aliases": { + "ttdalias": "thetradedesk" + } + } + } + }, + "usersyncs": { + "thetradedesk": "123" + } + }, + "outgoingRequests": { + "thetradedesk": { + "expectRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "user": { + "buyeruid": "123" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "bidder": { + "publisherId": "pub1", + "supplySourceId": "src1" + } + } + } + ] + } + }, + "mockResponse": { + "errors": [ + "thetradedesk-error" + ] + } + }, + "ttdalias": { + "expectRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "user": { + "buyeruid": "123" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "bidder": { + "publisherId": "pub2", + "supplySourceId": "src2" + } + } + } + ], + "ext": { + "prebid": { + "aliases": { + "ttdalias": "thetradedesk" + } + } + } + } + }, + "mockResponse": { + "errors": [ + "ttdalias-error" + ] + } + } + }, + "response": { + "bids": { + "id": "some-request-id", + "ext": { + "errors": { + "thetradedesk": [ + "thetradedesk-error" + ], + "ttdalias": [ + "ttdalias-error" + ] + } + } + } + } +} diff --git a/exchange/exchangetest/passthrough_root_and_imp_openads_key.json b/exchange/exchangetest/passthrough_root_and_imp_openads_key.json new file mode 100644 index 000000000..61670cb6b --- /dev/null +++ b/exchange/exchangetest/passthrough_root_and_imp_openads_key.json @@ -0,0 +1,132 @@ +{ + "passthrough_flag": true, + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "openads": { + "bidder": { + "thetradedesk": { + "publisherId": "pub1", + "supplySourceId": "src1" + } + }, + "passthrough": { + "imp_passthrough_val": 20 + } + } + } + } + ], + "ext": { + "openads": { + "passthrough": { + "bid_response_passthrough": 20 + } + } + } + } + }, + "outgoingRequests": { + "thetradedesk": { + "expectRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "bidder": { + "publisherId": "pub1", + "supplySourceId": "src1" + } + } + } + ] + } + }, + "mockResponse": { + "pbsSeatBids": [ + { + "pbsBids": [ + { + "ortbBid": { + "id": "ttd-bid", + "impid": "my-imp-id", + "price": 0.3, + "w": 200, + "h": 250, + "crid": "creative-1", + "ext": { + "someField": "someValue", + "origbidcpm": 0.3 + } + }, + "bidType": "video" + } + ], + "seat": "thetradedesk" + } + ] + } + } + }, + "response": { + "bids": { + "id": "some-request-id", + "seatbid": [ + { + "seat": "thetradedesk", + "bid": [ + { + "id": "ttd-bid", + "impid": "my-imp-id", + "price": 0.3, + "w": 200, + "h": 250, + "crid": "creative-1", + "ext": { + "someField": "someValue", + "origbidcpm": 0.3, + "prebid": { + "meta": { + }, + "type": "video", + "passthrough": { + "imp_passthrough_val": 20 + } + } + } + } + ] + } + ] + }, + "ext": { + "prebid": { + "passthrough": { + "bid_response_passthrough": 20 + } + } + } + } +} diff --git a/exchange/utils.go b/exchange/utils.go index 7606cd424..b8ae38b3a 100644 --- a/exchange/utils.go +++ b/exchange/utils.go @@ -660,7 +660,13 @@ func splitImps(imps []openrtb2.Imp, requestValidator ortb.RequestValidator, requ } var impExtPrebid map[string]json.RawMessage - if impExtPrebidJSON, exists := impExt[openrtb_ext.PrebidExtKey]; exists { + // Top-level alias resolution: when imp.ext.openads is present it is + // used in full; imp.ext.prebid is ignored entirely in that case. + impExtPrebidJSON, exists := impExt[openrtb_ext.OpenAdsExtKey] + if !exists { + impExtPrebidJSON, exists = impExt[openrtb_ext.PrebidExtKey] + } + if exists { // validation already performed by impExt unmarshal. no error is possible here, proven by tests. jsonutil.Unmarshal(impExtPrebidJSON, &impExtPrebid) } @@ -731,7 +737,8 @@ var allowedImpExtPrebidFields = map[string]interface{}{ } var deniedImpExtFields = map[string]interface{}{ - openrtb_ext.PrebidExtKey: struct{}{}, + openrtb_ext.PrebidExtKey: struct{}{}, + openrtb_ext.OpenAdsExtKey: struct{}{}, } func createSanitizedImpExt(impExt, impExtPrebid map[string]json.RawMessage) (map[string]json.RawMessage, error) { diff --git a/exchange/utils_test.go b/exchange/utils_test.go index 1139c2bda..98d6f7af0 100644 --- a/exchange/utils_test.go +++ b/exchange/utils_test.go @@ -303,6 +303,30 @@ func TestSplitImps(t *testing.T) { expectedImps: nil, expectedError: "merging bidder imp first party data for imp imp1 results in an invalid imp: [some error]", }, + { + description: "openads alias for prebid", + givenImps: []openrtb2.Imp{ + {ID: "imp1", Ext: json.RawMessage(`{"openads":{"bidder":{"bidderA":{"imp1ParamA":"imp1ValueA"}}}}`)}, + }, + expectedImps: map[string][]openrtb2.Imp{ + "bidderA": { + {ID: "imp1", Ext: json.RawMessage(`{"bidder":{"imp1ParamA":"imp1ValueA"}}`)}, + }, + }, + expectedError: "", + }, + { + description: "openads alias, both keys present, openads wins", + givenImps: []openrtb2.Imp{ + {ID: "imp1", Ext: json.RawMessage(`{"prebid":{"bidder":{"bidderA":{"fromPrebid":true}}},"openads":{"bidder":{"bidderB":{"fromOpenAds":true}}}}`)}, + }, + expectedImps: map[string][]openrtb2.Imp{ + "bidderB": { + {ID: "imp1", Ext: json.RawMessage(`{"bidder":{"fromOpenAds":true}}`)}, + }, + }, + expectedError: "", + }, } for _, test := range testCases { @@ -875,7 +899,7 @@ func TestExtractAdapterReqBidderParamsMap(t *testing.T) { name: "malformed req.ext", givenBidRequest: &openrtb2.BidRequest{Ext: json.RawMessage("malformed")}, want: nil, - wantErr: errors.New("error decoding Request.ext : expect { or n, but found m"), + wantErr: errors.New("error decoding Request.ext"), }, { name: "extract bidder params from req.Ext for input request in adapter code", @@ -887,7 +911,11 @@ func TestExtractAdapterReqBidderParamsMap(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := ExtractReqExtBidderParamsMap(tt.givenBidRequest) - assert.Equal(t, tt.wantErr, err, "err") + if tt.wantErr == nil { + assert.NoError(t, err, "err") + } else { + assert.ErrorContains(t, err, tt.wantErr.Error(), "err") + } assert.Equal(t, tt.want, got, "result") }) } diff --git a/modules/openads/signatures/module_test.go b/modules/openads/signatures/module_test.go index 512e11a64..06f59a67b 100644 --- a/modules/openads/signatures/module_test.go +++ b/modules/openads/signatures/module_test.go @@ -148,10 +148,10 @@ func TestBuilder(t *testing.T) { func TestHandleBidderRequestHook_Success(t *testing.T) { tests := []struct { - name string - initialExt json.RawMessage + name string + initialExt json.RawMessage mockResponse []SignatureWrapper - expectedSig Signature + expectedSig Signature }{ { name: "add int_sigs to nil ext", @@ -188,7 +188,7 @@ func TestHandleBidderRequestHook_Success(t *testing.T) { }, { name: "replace openads with int_sigs", - initialExt: json.RawMessage(`{"openads": 1, "prebid": {"debug": true}}`), + initialExt: json.RawMessage(`{"openads": {"version": 0, "int_sigs": []}, "prebid": {"debug": true}}`), mockResponse: []SignatureWrapper{ {Name: "testbidder", SIS: Signature{Envelope: "envelope-3", Source: "source-3"}}, }, @@ -658,7 +658,7 @@ func TestHandleBidderRequestHook_MutationTracking(t *testing.T) { assert.Equal(t, SchemaVersion, openadsExt.Version) require.Len(t, openadsExt.IntSigs, 1) - + expectedSig := Signature{ Envelope: "test-signature", Source: "test-source", @@ -832,7 +832,7 @@ func TestTCPIntegration(t *testing.T) { assert.Equal(t, SchemaVersion, openadsExt.Version) require.Len(t, openadsExt.IntSigs, 1) - + expectedSig := Signature{ Envelope: "sig-1", Source: "source-1", @@ -938,7 +938,7 @@ func TestUDSIntegration(t *testing.T) { assert.Equal(t, SchemaVersion, openadsExt.Version) require.Len(t, openadsExt.IntSigs, 1) - + expectedSig := Signature{ Envelope: "sig-1", Source: "source-1", diff --git a/openrtb_ext/device.go b/openrtb_ext/device.go index 2107a3081..b8e4cd483 100644 --- a/openrtb_ext/device.go +++ b/openrtb_ext/device.go @@ -12,6 +12,10 @@ import ( // PrebidExtKey represents the prebid extension key used in requests const PrebidExtKey = "prebid" +// OpenAdsExtKey is an alias for PrebidExtKey accepted on inbound request.ext +// and imp.ext. Outbound requests still use PrebidExtKey. +const OpenAdsExtKey = "openads" + // PrebidExtBidderKey represents the field name within request.imp.ext.prebid reserved for bidder params. const PrebidExtBidderKey = "bidder" diff --git a/openrtb_ext/request.go b/openrtb_ext/request.go index 7f8978e6e..292e1bcbc 100644 --- a/openrtb_ext/request.go +++ b/openrtb_ext/request.go @@ -40,6 +40,33 @@ type ExtRequest struct { SChain *openrtb2.SupplyChain `json:"schain,omitempty"` } +// UnmarshalJSON resolves the prebid configuration at the top level: if +// "openads" is present it is used in full, otherwise "prebid" is used. +// "prebid" is never consulted when "openads" exists, even for fields +// missing from openads. Unknown fields inside the chosen block are +// ignored. Outbound marshaling continues to emit "prebid". +func (e *ExtRequest) UnmarshalJSON(data []byte) error { + type alias ExtRequest + aux := &struct { + Prebid *json.RawMessage `json:"prebid,omitempty"` + OpenAds *json.RawMessage `json:"openads,omitempty"` + *alias + }{ + alias: (*alias)(e), + } + if err := jsonutil.Unmarshal(data, aux); err != nil { + return err + } + src := aux.OpenAds + if src == nil { + src = aux.Prebid + } + if src == nil { + return nil + } + return jsonutil.Unmarshal(*src, &e.Prebid) +} + // ExtRequestPrebid defines the contract for bidrequest.ext.prebid type ExtRequestPrebid struct { AdServerTargeting []AdServerTarget `json:"adservertargeting,omitempty"` diff --git a/openrtb_ext/request_test.go b/openrtb_ext/request_test.go index 92f148db4..9c6214640 100644 --- a/openrtb_ext/request_test.go +++ b/openrtb_ext/request_test.go @@ -777,3 +777,65 @@ func TestCloneExtRequestPrebid(t *testing.T) { } } + +func TestExtRequestUnmarshalOpenAdsAlias(t *testing.T) { + tests := []struct { + name string + input string + wantDebug bool + wantChannel *ExtRequestPrebidChannel + }{ + { + name: "prebid key only", + input: `{"prebid":{"debug":true,"channel":{"name":"pbjs","version":"v10"}}}`, + wantDebug: true, + wantChannel: &ExtRequestPrebidChannel{Name: "pbjs", Version: "v10"}, + }, + { + name: "openads key only", + input: `{"openads":{"debug":true,"channel":{"name":"pbjs","version":"v10"}}}`, + wantDebug: true, + wantChannel: &ExtRequestPrebidChannel{Name: "pbjs", Version: "v10"}, + }, + { + name: "both keys, openads wins", + input: `{"prebid":{"debug":true},"openads":{"debug":false}}`, + wantDebug: false, + wantChannel: nil, + }, + { + name: "openads with signing-style fields ignored", + input: `{"openads":{"debug":true,"version":"sig-v1","intSigs":["a","b"]}}`, + wantDebug: true, + wantChannel: nil, + }, + { + name: "neither key", + input: `{"schain":{"complete":1,"nodes":[],"ver":"1.0"}}`, + wantDebug: false, + wantChannel: nil, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var got ExtRequest + err := jsonutil.UnmarshalValid([]byte(tc.input), &got) + assert.NoError(t, err) + assert.Equal(t, tc.wantDebug, got.Prebid.Debug, "Prebid.Debug") + assert.Equal(t, tc.wantChannel, got.Prebid.Channel, "Prebid.Channel") + }) + } +} + +func TestExtRequestMarshalEmitsPrebid(t *testing.T) { + in := []byte(`{"openads":{"debug":true}}`) + var ext ExtRequest + err := jsonutil.UnmarshalValid(in, &ext) + assert.NoError(t, err) + + out, err := json.Marshal(&ext) + assert.NoError(t, err) + assert.Contains(t, string(out), `"prebid"`) + assert.NotContains(t, string(out), `"openads"`) +} diff --git a/openrtb_ext/request_wrapper.go b/openrtb_ext/request_wrapper.go index 7b73d3186..5cfad1b40 100644 --- a/openrtb_ext/request_wrapper.go +++ b/openrtb_ext/request_wrapper.go @@ -772,7 +772,16 @@ func (re *RequestExt) unmarshal(extJson json.RawMessage) error { return err } - prebidJson, hasPrebid := re.ext[prebidKey] + // Top-level alias resolution: when ext.openads is present it is used + // in full and ext.prebid is ignored entirely. The chosen block is + // stored under prebidKey so outbound marshaling emits "prebid". + prebidJson, hasPrebid := re.ext[OpenAdsExtKey] + if hasPrebid { + re.ext[prebidKey] = prebidJson + } else { + prebidJson, hasPrebid = re.ext[prebidKey] + } + delete(re.ext, OpenAdsExtKey) if hasPrebid { re.prebid = &ExtRequestPrebid{} } @@ -1709,7 +1718,16 @@ func (e *ImpExt) unmarshal(extJson json.RawMessage) error { return err } - prebidJson, hasPrebid := e.ext[prebidKey] + // Top-level alias resolution: when imp.ext.openads is present it is + // used in full and imp.ext.prebid is ignored entirely. The chosen + // block is stored under prebidKey so outbound marshaling emits "prebid". + prebidJson, hasPrebid := e.ext[OpenAdsExtKey] + if hasPrebid { + e.ext[prebidKey] = prebidJson + } else { + prebidJson, hasPrebid = e.ext[prebidKey] + } + delete(e.ext, OpenAdsExtKey) if hasPrebid { e.prebid = &ExtImpPrebid{} } diff --git a/openrtb_ext/request_wrapper_test.go b/openrtb_ext/request_wrapper_test.go index e4c14b603..4e17d4fbb 100644 --- a/openrtb_ext/request_wrapper_test.go +++ b/openrtb_ext/request_wrapper_test.go @@ -2476,3 +2476,68 @@ func TestRegExtGetGPCSetGPC(t *testing.T) { assert.Equal(t, regExtGPC, gpc) assert.NotSame(t, regExtGPC, gpc) } + +func TestRequestExtOpenAdsAlias(t *testing.T) { + t.Run("openads decodes into prebid and marshals as prebid", func(t *testing.T) { + re := &RequestExt{} + err := re.unmarshal([]byte(`{"openads":{"debug":true,"channel":{"name":"pbjs","version":"v10"}}}`)) + assert.NoError(t, err) + + got := re.GetPrebid() + if assert.NotNil(t, got) { + assert.True(t, got.Debug) + if assert.NotNil(t, got.Channel) { + assert.Equal(t, "pbjs", got.Channel.Name) + } + } + + // Re-marshal: outbound must use "prebid", not "openads". + re.SetPrebid(re.GetPrebid()) + out, err := re.marshal() + assert.NoError(t, err) + assert.Contains(t, string(out), `"prebid"`) + assert.NotContains(t, string(out), `"openads"`) + }) + + t.Run("both keys present, openads wins", func(t *testing.T) { + re := &RequestExt{} + err := re.unmarshal([]byte(`{"prebid":{"debug":true},"openads":{"debug":false}}`)) + assert.NoError(t, err) + assert.False(t, re.GetPrebid().Debug) + + re.SetPrebid(re.GetPrebid()) + out, err := re.marshal() + assert.NoError(t, err) + assert.NotContains(t, string(out), `"openads"`) + }) +} + +func TestImpExtOpenAdsAlias(t *testing.T) { + t.Run("openads decodes into prebid and marshals as prebid", func(t *testing.T) { + ie := &ImpExt{} + err := ie.unmarshal([]byte(`{"openads":{"storedrequest":{"id":"sr-7"}}}`)) + assert.NoError(t, err) + got := ie.GetPrebid() + if assert.NotNil(t, got) && assert.NotNil(t, got.StoredRequest) { + assert.Equal(t, "sr-7", got.StoredRequest.ID) + } + + ie.SetPrebid(ie.GetPrebid()) + out, err := ie.marshal() + assert.NoError(t, err) + assert.Contains(t, string(out), `"prebid"`) + assert.NotContains(t, string(out), `"openads"`) + }) + + t.Run("both keys present, openads wins", func(t *testing.T) { + ie := &ImpExt{} + err := ie.unmarshal([]byte(`{"prebid":{"storedrequest":{"id":"FROM_PREBID"}},"openads":{"storedrequest":{"id":"FROM_OPENADS"}}}`)) + assert.NoError(t, err) + assert.Equal(t, "FROM_OPENADS", ie.GetPrebid().StoredRequest.ID) + + ie.SetPrebid(ie.GetPrebid()) + out, err := ie.marshal() + assert.NoError(t, err) + assert.NotContains(t, string(out), `"openads"`) + }) +}