From 9d650148ae74a473f1517d666ed7faf38795866e Mon Sep 17 00:00:00 2001 From: Patryk Grzegorczyk Date: Tue, 28 Jul 2026 15:16:22 +0200 Subject: [PATCH 1/3] Squashed commit of the following: commit 1abd75bc06e8a11cbb9a79cd0ba632dbe88cd386 Author: Patryk Grzegorczyk Date: Tue Jul 28 15:11:23 2026 +0200 [ITADS-2318] Review changes commit a12b96e40ccd928fa7f7e723098299e15770a8f4 Author: Patryk Grzegorczyk Date: Mon Jul 27 11:11:16 2026 +0200 [ITADS-2318] No support for outstream commit dfa488e7a4d76261febad0462514bdf32f9be6b4 Author: Patryk Grzegorczyk Date: Mon Jul 27 10:15:25 2026 +0200 [ITADS-2318] AdOcean prebid adapter --- adapters/adocean/adocean.go | 379 ++++++++++++++++++ adapters/adocean/adocean_test.go | 63 +++ .../adoceantest/exemplary/simple-banner.json | 94 +++++ .../adoceantest/exemplary/simple-video.json | 77 ++++ .../supplemental/bad-response.json | 66 +++ .../supplemental/multiple-requests.json | 217 ++++++++++ .../supplemental/network-error.json | 66 +++ .../adoceantest/supplemental/no-bid.json | 62 +++ .../supplemental/no-impression.json | 36 ++ .../supplemental/video-outstream.json | 34 ++ adapters/adocean/params_test.go | 48 +++ exchange/adapter_builders.go | 2 + exchange/adapter_util.go | 1 - openrtb_ext/bidders.go | 2 + openrtb_ext/imp_adocean.go | 8 + static/bidder-info/adocean.yaml | 18 + static/bidder-params/adocean.json | 28 ++ 17 files changed, 1200 insertions(+), 1 deletion(-) create mode 100644 adapters/adocean/adocean.go create mode 100644 adapters/adocean/adocean_test.go create mode 100644 adapters/adocean/adoceantest/exemplary/simple-banner.json create mode 100644 adapters/adocean/adoceantest/exemplary/simple-video.json create mode 100644 adapters/adocean/adoceantest/supplemental/bad-response.json create mode 100644 adapters/adocean/adoceantest/supplemental/multiple-requests.json create mode 100644 adapters/adocean/adoceantest/supplemental/network-error.json create mode 100644 adapters/adocean/adoceantest/supplemental/no-bid.json create mode 100644 adapters/adocean/adoceantest/supplemental/no-impression.json create mode 100644 adapters/adocean/adoceantest/supplemental/video-outstream.json create mode 100644 adapters/adocean/params_test.go create mode 100644 openrtb_ext/imp_adocean.go create mode 100644 static/bidder-info/adocean.yaml create mode 100644 static/bidder-params/adocean.json diff --git a/adapters/adocean/adocean.go b/adapters/adocean/adocean.go new file mode 100644 index 00000000000..7fedd827670 --- /dev/null +++ b/adapters/adocean/adocean.go @@ -0,0 +1,379 @@ +package adocean + +import ( + "errors" + "fmt" + "math/rand" + "net/http" + "net/url" + "strconv" + "strings" + "text/template" + + "github.com/prebid/openrtb/v20/adcom1" + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v4/adapters" + "github.com/prebid/prebid-server/v4/config" + "github.com/prebid/prebid-server/v4/errortypes" + "github.com/prebid/prebid-server/v4/macros" + "github.com/prebid/prebid-server/v4/openrtb_ext" + "github.com/prebid/prebid-server/v4/util/jsonutil" +) + +const ( + adapterVersion = "2.0.0" + maxUriLength = 8000 + slaveIDLength = 10 +) + +type responseAdUnit struct { + ID string `json:"id"` + CrID string `json:"crid"` + Currency string `json:"currency"` + Price string `json:"price"` + TTL string `json:"ttl"` + Width string `json:"width"` + Height string `json:"height"` + IsVideo bool `json:"isVideo"` + Code string `json:"code"` + ADomain []string `json:"adomain"` + Error string `json:"error"` +} + +type adapter struct { + endpointTemplate *template.Template +} + +// Builder builds a new instance of the AdOcean adapter for the given bidder with the given config. +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + endpointTemplate, err := template.New("endpointTemplate").Parse(config.Endpoint) + if err != nil { + return nil, errors.New("unable to parse endpoint template") + } + + return &adapter{endpointTemplate: endpointTemplate}, nil +} + +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + if len(request.Imp) == 0 { + return nil, []error{&errortypes.BadInput{ + Message: "No impression in the bid request", + }} + } + requests := make([]*adapters.RequestData, 0, len(request.Imp)) + var errs []error + + for index := range request.Imp { + imp := &request.Imp[index] + if err := validateImp(imp); err != nil { + errs = append(errs, err) + continue + } + params, err := parseImpExt(imp) + if err != nil { + errs = append(errs, err) + continue + } + + requestData, err := a.makeRequest(request, imp, params) + if err != nil { + errs = append(errs, err) + continue + } + requests = append(requests, requestData) + } + + return requests, errs +} + +func validateImp(imp *openrtb2.Imp) error { + if imp.Banner == nil && imp.Video == nil { + return &errortypes.BadInput{ + Message: fmt.Sprintf("ignoring imp id=%s: AdOcean supports only banner and instream video", imp.ID), + } + } + if imp.Video != nil && (imp.Video.Plcmt == adcom1.VideoPlcmtAccompanyingContent || + imp.Video.Plcmt == adcom1.VideoPlcmtNoContent || + imp.Video.Placement == adcom1.VideoPlacementInBanner) { + return &errortypes.BadInput{ + Message: fmt.Sprintf("ignoring imp id=%s: AdOcean doesn't support outstream video", imp.ID), + } + } + return nil +} + +func parseImpExt(imp *openrtb2.Imp) (*openrtb_ext.ExtImpAdOcean, error) { + var bidderExt adapters.ExtImpBidder + if err := jsonutil.Unmarshal(imp.Ext, &bidderExt); err != nil { + return nil, &errortypes.BadInput{ + Message: fmt.Sprintf("ignoring imp id=%s: failed to parse ext.bidder: %v", imp.ID, err), + } + } + + var params openrtb_ext.ExtImpAdOcean + if err := jsonutil.Unmarshal(bidderExt.Bidder, ¶ms); err != nil { + return nil, &errortypes.BadInput{ + Message: fmt.Sprintf("ignoring imp id=%s: failed to parse AdOcean parameters: %v", imp.ID, err), + } + } + + return ¶ms, nil +} + +func (a *adapter) resolveEndpointTemplate(emitterPrefix string) (string, error) { + endpoint, err := macros.ResolveMacros(a.endpointTemplate, macros.EndpointTemplateParams{Host: emitterPrefix}) + if err != nil { + return "", &errortypes.BadInput{Message: "unable to resolve endpoint template: " + err.Error()} + } + return endpoint, nil +} + +func (a *adapter) makeRequest(request *openrtb2.BidRequest, imp *openrtb2.Imp, params *openrtb_ext.ExtImpAdOcean) (*adapters.RequestData, error) { + endpoint, err := a.resolveEndpointTemplate(params.EmitterPrefix) + if err != nil { + return nil, err + } + + requestURL, err := url.Parse(endpoint) + if err != nil { + return nil, &errortypes.BadInput{Message: "malformed endpoint URL: " + err.Error()} + } + + randomizedPart := rand.Intn(90000000) + 10000000 + if request.Test == 1 { + randomizedPart = 10000000 + } + requestURL.Path = "/_" + strconv.Itoa(randomizedPart) + "/ad.json" + // RFC 3986 requires that spaces in query parameters be encoded as %20, + // but the Go standard library encodes them as + + requestURL.RawQuery = strings.ReplaceAll(buildQuery(request, imp, params).Encode(), "+", "%20") + if len(requestURL.String()) >= maxUriLength { + return nil, &errortypes.BadInput{ + Message: fmt.Sprintf("AdOcean request URL exceeds maximum length of %d characters", maxUriLength), + } + } + + return &adapters.RequestData{ + Method: http.MethodGet, + Uri: requestURL.String(), + Headers: buildHeaders(request), + ImpIDs: []string{imp.ID}, + }, nil +} + +func buildQuery(request *openrtb2.BidRequest, imp *openrtb2.Imp, params *openrtb_ext.ExtImpAdOcean) url.Values { + query := url.Values{} + query.Set("pbsrv_v", adapterVersion) + query.Set("id", params.MasterID) + query.Set("slaves", shortSlaveID(params.SlaveID)) + + if request.Regs != nil && request.Regs.GDPR != nil { + query.Set("gdpr", strconv.Itoa(int(*request.Regs.GDPR))) + } + if request.User != nil { + if request.User.Consent != "" { + query.Set("gdpr_consent", request.User.Consent) + } + if request.User.BuyerUID != "" { + query.Set("aouserid", request.User.BuyerUID) + } + } + + for key, value := range params.EmitterRequestParams { + query.Add(key, fmt.Sprint(value)) + } + + if imp.Video != nil { + query.Set("spots", "1") + if imp.Video.MaxDuration > 0 { + maxDuration := strconv.FormatInt(imp.Video.MaxDuration, 10) + query.Set("dur", maxDuration) + query.Set("maxdur", maxDuration) + } + if imp.Video.MinDuration > 0 { + query.Set("mindur", strconv.FormatInt(imp.Video.MinDuration, 10)) + } + } else if imp.Banner != nil { + if sizes := getBannerSizes(imp.Banner); len(sizes) > 0 { + query.Set("aosize", strings.Join(sizes, ",")) + } + } + + return query +} + +func shortSlaveID(slaveID string) string { + if len(slaveID) <= slaveIDLength { + return slaveID + } + return slaveID[len(slaveID)-slaveIDLength:] +} + +func getBannerSizes(banner *openrtb2.Banner) []string { + if len(banner.Format) > 0 { + sizes := make([]string, 0, len(banner.Format)) + for _, format := range banner.Format { + sizes = append(sizes, strconv.FormatInt(format.W, 10)+"x"+strconv.FormatInt(format.H, 10)) + } + return sizes + } + + if banner.W != nil && banner.H != nil { + return []string{strconv.FormatInt(*banner.W, 10) + "x" + strconv.FormatInt(*banner.H, 10)} + } + return nil +} + +func buildHeaders(request *openrtb2.BidRequest) http.Header { + headers := http.Header{ + "Accept": []string{"application/json"}, + "Content-Type": []string{"application/json;charset=utf-8"}, + } + if request.Device != nil { + if request.Device.UA != "" { + headers.Set("User-Agent", request.Device.UA) + } + if request.Device.IP != "" { + headers.Set("X-Forwarded-For", request.Device.IP) + } else if request.Device.IPv6 != "" { + headers.Set("X-Forwarded-For", request.Device.IPv6) + } + } + if request.Site != nil && request.Site.Page != "" { + headers.Set("Referer", request.Site.Page) + } + return headers +} + +func (a *adapter) MakeBids( + internalRequest *openrtb2.BidRequest, + externalRequest *adapters.RequestData, + response *adapters.ResponseData, +) (*adapters.BidderResponse, []error) { + if response.StatusCode == http.StatusNoContent { + return nil, nil + } + if response.StatusCode == http.StatusBadRequest { + return nil, []error{&errortypes.BadInput{ + Message: "unexpected status code: 400", + }} + } + if response.StatusCode != http.StatusOK { + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("unexpected status code: %d", response.StatusCode), + }} + } + + var adUnits []responseAdUnit + if err := jsonutil.Unmarshal(response.Body, &adUnits); err != nil { + return nil, []error{&errortypes.BadServerResponse{ + Message: "failed to decode AdOcean response: " + err.Error(), + }} + } + + bidderResponse := adapters.NewBidderResponseWithBidsCapacity(len(adUnits)) + var lastCurrency *string = nil + var errs []error + for _, adUnit := range adUnits { + if adUnit.Error == "true" { + continue + } + impID, found := findImpID(internalRequest, adUnit.ID) + if !found { + continue + } + + typedBid, currency, err := makeBid(adUnit, impID) + if err != nil { + errs = append(errs, err) + continue + } + bidderResponse.Bids = append(bidderResponse.Bids, typedBid) + if lastCurrency == nil { + lastCurrency = ¤cy + } else if *lastCurrency != currency { + errs = append(errs, &errortypes.BadServerResponse{ + Message: fmt.Sprintf("inconsistent currencies in AdOcean response: %s and %s", *lastCurrency, currency), + }) + continue + } + bidderResponse.Currency = currency + } + + return bidderResponse, errs +} + +func findImpID(internalRequest *openrtb2.BidRequest, placementID string) (string, bool) { + for index := range internalRequest.Imp { + imp := &internalRequest.Imp[index] + + params, err := parseImpExt(imp) + if err == nil && params.SlaveID == placementID { + return imp.ID, true + } + } + return "", false +} + +func makeBid(adUnit responseAdUnit, impID string) (*adapters.TypedBid, string, error) { + if adUnit.Code == "" || adUnit.Height == "" || adUnit.Width == "" || adUnit.Price == "" { + return nil, "", &errortypes.BadServerResponse{ + Message: fmt.Sprintf("incomplete bid for AdOcean placement %q", adUnit.ID), + } + } + + price, err := strconv.ParseFloat(adUnit.Price, 64) + if err != nil { + return nil, "", invalidBidField(adUnit.ID, "price", err) + } + width, err := strconv.ParseInt(adUnit.Width, 10, 64) + if err != nil { + return nil, "", invalidBidField(adUnit.ID, "width", err) + } + height, err := strconv.ParseInt(adUnit.Height, 10, 64) + if err != nil { + return nil, "", invalidBidField(adUnit.ID, "height", err) + } + ttl, err := strconv.ParseInt(adUnit.TTL, 10, 64) + if err != nil && adUnit.TTL != "" { + return nil, "", invalidBidField(adUnit.ID, "ttl", err) + } + adMarkup, err := url.PathUnescape(adUnit.Code) + if err != nil { + return nil, "", invalidBidField(adUnit.ID, "code", err) + } + + bidType := openrtb_ext.BidTypeBanner + if adUnit.IsVideo { + bidType = openrtb_ext.BidTypeVideo + } + + aDomain := adUnit.ADomain + if aDomain == nil { + aDomain = []string{} + } + + return &adapters.TypedBid{ + Bid: &openrtb2.Bid{ + ID: adUnit.ID, + ImpID: impID, + Price: price, + AdM: adMarkup, + CrID: adUnit.CrID, + ADomain: aDomain, + W: width, + H: height, + Exp: ttl, + }, + BidMeta: &openrtb_ext.ExtBidPrebidMeta{ + AdvertiserDomains: aDomain, + }, + BidType: bidType, + }, adUnit.Currency, nil +} + +func invalidBidField(placementID string, field string, err error) error { + return &errortypes.BadServerResponse{ + Message: fmt.Sprintf("invalid %s in bid for AdOcean placement %q: %v", field, placementID, err), + } +} diff --git a/adapters/adocean/adocean_test.go b/adapters/adocean/adocean_test.go new file mode 100644 index 00000000000..b5ab50540d5 --- /dev/null +++ b/adapters/adocean/adocean_test.go @@ -0,0 +1,63 @@ +package adocean + +import ( + "strings" + "testing" + "text/template" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v4/adapters/adapterstest" + "github.com/prebid/prebid-server/v4/config" + "github.com/prebid/prebid-server/v4/openrtb_ext" +) + +func TestEndpointTemplateMalformed(t *testing.T) { + _, err := Builder(openrtb_ext.BidderAdOcean, config.Adapter{Endpoint: "{{Malformed}}"}, config.Server{}) + if err == nil { + t.Fatal("Builder should reject a malformed endpoint template") + } +} + +func TestJsonSamples(t *testing.T) { + bidder, err := Builder(openrtb_ext.BidderAdOcean, config.Adapter{ + Endpoint: "https://{{.Host}}.adocean.pl", + }, config.Server{}) + if err != nil { + t.Fatalf("Builder returned unexpected error: %v", err) + } + + adapterstest.RunJSONBidderTest(t, "adoceantest", bidder) +} + +func TestMakeRequestRejectsLongURL(t *testing.T) { + endpointTemplate := template.Must(template.New("endpoint").Parse("https://{{.Host}}.adocean.pl")) + bidder := adapter{endpointTemplate: endpointTemplate} + width, height := int64(300), int64(250) + _, err := bidder.makeRequest(&openrtb2.BidRequest{}, &openrtb2.Imp{ + ID: "test-imp", + Banner: &openrtb2.Banner{ + W: &width, + H: &height, + }, + }, &openrtb_ext.ExtImpAdOcean{ + EmitterPrefix: "myao", + MasterID: strings.Repeat("a", maxUriLength), + SlaveID: "adoceanmyaozpniqismex", + }) + if err == nil { + t.Fatal("makeRequest should reject a URL that exceeds maxUriLength") + } +} + +func TestResolveEndpointTemplate(t *testing.T) { + endpointTemplate := template.Must(template.New("endpoint").Parse("https://{{.Host}}.adocean.pl")) + bidder := adapter{endpointTemplate: endpointTemplate} + url, err := bidder.resolveEndpointTemplate("myao") + if err != nil { + t.Fatalf("resolveEndpointTemplate returned unexpected error: %v", err) + } + expectedURL := "https://myao.adocean.pl" + if url != expectedURL { + t.Fatalf("resolveEndpointTemplate returned %v, expected %v", url, expectedURL) + } +} diff --git a/adapters/adocean/adoceantest/exemplary/simple-banner.json b/adapters/adocean/adoceantest/exemplary/simple-banner.json new file mode 100644 index 00000000000..28ef214673a --- /dev/null +++ b/adapters/adocean/adoceantest/exemplary/simple-banner.json @@ -0,0 +1,94 @@ +{ + "mockBidRequest": { + "id": "banner-request", + "test": 1, + "regs": { + "gdpr": 1 + }, + "user": { + "buyeruid": "gemius-user-id", + "consent": "BOQHk-4OSlWKFBoABBPLBd-AAAAgWAHAACAAsAPQBSACmgFTAOkA" + }, + "device": { + "ua": "test-user-agent", + "ip": "1.0.0.0" + }, + "site": { + "page": "https://example.com/publisher_page" + }, + "imp": [ + { + "id": "banner-imp", + "banner": { + "format": [ + {"w": 300, "h": 250}, + {"w": 400, "h": 600} + ] + }, + "ext": { + "bidder": { + "emitterPrefix": "myao", + "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", + "slaveId": "adoceanmyaozpniqismex", + "emitterRequestParams": { + "special key": "special +value" + } + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://myao.adocean.pl/_10000000/ad.json?aosize=300x250%2C400x600&aouserid=gemius-user-id&gdpr=1&gdpr_consent=BOQHk-4OSlWKFBoABBPLBd-AAAAgWAHAACAAsAPQBSACmgFTAOkA&id=tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7&pbsrv_v=2.0.0&slaves=zpniqismex&special%20key=special%20%2Bvalue", + "headers": { + "Accept": ["application/json"], + "Content-Type": ["application/json;charset=utf-8"], + "Referer": ["https://example.com/publisher_page"], + "User-Agent": ["test-user-agent"], + "X-Forwarded-For": ["1.0.0.0"] + }, + "impIDs": ["banner-imp"] + }, + "mockResponse": { + "status": 200, + "body": [ + { + "id": "adoceanmyaozpniqismex", + "price": "0.019000", + "ttl": "360", + "crid": "veeinoriep", + "currency": "EUR", + "width": "300", + "height": "250", + "isVideo": false, + "code": "%3C!--%20Creative%20--%3E", + "adomain": ["adocean.pl"] + } + ] + } + } + ], + "expectedBidResponses": [ + { + "currency": "EUR", + "bids": [ + { + "bid": { + "id": "adoceanmyaozpniqismex", + "impid": "banner-imp", + "price": 0.019, + "adm": "", + "crid": "veeinoriep", + "adomain": ["adocean.pl"], + "w": 300, + "h": 250, + "exp": 360 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/adocean/adoceantest/exemplary/simple-video.json b/adapters/adocean/adoceantest/exemplary/simple-video.json new file mode 100644 index 00000000000..7cd272d040b --- /dev/null +++ b/adapters/adocean/adoceantest/exemplary/simple-video.json @@ -0,0 +1,77 @@ +{ + "mockBidRequest": { + "id": "video-request", + "test": 1, + "device": { + "ua": "test-user-agent", + "ip": "1.0.0.0" + }, + "imp": [ + { + "id": "video-imp", + "video": { + "mimes": ["video/mp4"], + "minduration": 10, + "maxduration": 60 + }, + "ext": { + "bidder": { + "emitterPrefix": "myao", + "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", + "slaveId": "adoceanmyaolifgmvmpfj" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://myao.adocean.pl/_10000000/ad.json?dur=60&id=tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7&maxdur=60&mindur=10&pbsrv_v=2.0.0&slaves=lifgmvmpfj&spots=1", + "headers": { + "Accept": ["application/json"], + "Content-Type": ["application/json;charset=utf-8"], + "User-Agent": ["test-user-agent"], + "X-Forwarded-For": ["1.0.0.0"] + }, + "impIDs": ["video-imp"] + }, + "mockResponse": { + "status": 200, + "body": [ + { + "id": "adoceanmyaolifgmvmpfj", + "price": "0.019000", + "ttl": "360", + "crid": "qpqhltkgpu", + "currency": "EUR", + "width": "300", + "height": "250", + "isVideo": true, + "code": "%3C!--%20Video%20Creative%20--%3E" + } + ] + } + } + ], + "expectedBidResponses": [ + { + "currency": "EUR", + "bids": [ + { + "bid": { + "id": "adoceanmyaolifgmvmpfj", + "impid": "video-imp", + "price": 0.019, + "adm": "", + "crid": "qpqhltkgpu", + "w": 300, + "h": 250, + "exp": 360 + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/adocean/adoceantest/supplemental/bad-response.json b/adapters/adocean/adoceantest/supplemental/bad-response.json new file mode 100644 index 00000000000..2cc9e4778c5 --- /dev/null +++ b/adapters/adocean/adoceantest/supplemental/bad-response.json @@ -0,0 +1,66 @@ +{ + "mockBidRequest": { + "id": "banner-request", + "test": 1, + "regs": { + "gdpr": 1 + }, + "user": { + "buyeruid": "gemius-user-id", + "consent": "BOQHk-4OSlWKFBoABBPLBd-AAAAgWAHAACAAsAPQBSACmgFTAOkA" + }, + "device": { + "ua": "test-user-agent", + "ip": "1.0.0.0" + }, + "site": { + "page": "https://example.com/publisher_page" + }, + "imp": [ + { + "id": "banner-imp", + "banner": { + "format": [ + {"w": 300, "h": 250}, + {"w": 400, "h": 600} + ] + }, + "ext": { + "bidder": { + "emitterPrefix": "myao", + "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", + "slaveId": "adoceanmyaozpniqismex", + "emitterRequestParams": { + "special key": "special +value" + } + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://myao.adocean.pl/_10000000/ad.json?aosize=300x250%2C400x600&aouserid=gemius-user-id&gdpr=1&gdpr_consent=BOQHk-4OSlWKFBoABBPLBd-AAAAgWAHAACAAsAPQBSACmgFTAOkA&id=tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7&pbsrv_v=2.0.0&slaves=zpniqismex&special%20key=special%20%2Bvalue", + "headers": { + "Accept": ["application/json"], + "Content-Type": ["application/json;charset=utf-8"], + "Referer": ["https://example.com/publisher_page"], + "User-Agent": ["test-user-agent"], + "X-Forwarded-For": ["1.0.0.0"] + }, + "impIDs": ["banner-imp"] + }, + "mockResponse": { + "status": 200, + "body": "anything" + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "failed to decode AdOcean response: decode slice: expect [ or n, but found \"", + "comparison": "literal" + } + ] +} diff --git a/adapters/adocean/adoceantest/supplemental/multiple-requests.json b/adapters/adocean/adoceantest/supplemental/multiple-requests.json new file mode 100644 index 00000000000..91e77fcb282 --- /dev/null +++ b/adapters/adocean/adoceantest/supplemental/multiple-requests.json @@ -0,0 +1,217 @@ +{ + "mockBidRequest": { + "id": "multiple-requests", + "test": 1, + "regs": { + "gdpr": 1 + }, + "user": { + "buyeruid": "gemius-user-id", + "consent": "BOQHk-4OSlWKFBoABBPLBd-AAAAgWAHAACAAsAPQBSACmgFTAOkA" + }, + "device": { + "ua": "test-user-agent", + "ip": "1.0.0.0" + }, + "site": { + "page": "https://example.com/publisher_page" + }, + "imp": [ + { + "id": "banner-imp-one", + "banner": { + "format": [ + {"w": 300, "h": 250} + ] + }, + "ext": { + "bidder": { + "emitterPrefix": "myao", + "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", + "slaveId": "adoceanmyaozpniqismex" + } + } + }, + { + "id": "banner-imp-two", + "banner": { + "format": [ + {"w": 400, "h": 600} + ] + }, + "ext": { + "bidder": { + "emitterPrefix": "myao", + "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", + "slaveId": "adoceanmyaowafpdwlrks" + } + } + }, + { + "id": "video-imp", + "video": { + "mimes": ["video/mp4"], + "minduration": 10, + "maxduration": 60, + "plcmt": 1 + }, + "ext": { + "bidder": { + "emitterPrefix": "myao", + "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", + "slaveId": "adoceanmyaolifgmvmpfj" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://myao.adocean.pl/_10000000/ad.json?aosize=300x250&aouserid=gemius-user-id&gdpr=1&gdpr_consent=BOQHk-4OSlWKFBoABBPLBd-AAAAgWAHAACAAsAPQBSACmgFTAOkA&id=tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7&pbsrv_v=2.0.0&slaves=zpniqismex", + "headers": { + "Accept": ["application/json"], + "Content-Type": ["application/json;charset=utf-8"], + "Referer": ["https://example.com/publisher_page"], + "User-Agent": ["test-user-agent"], + "X-Forwarded-For": ["1.0.0.0"] + }, + "impIDs": ["banner-imp-one"] + }, + "mockResponse": { + "status": 200, + "body": [ + { + "id": "adoceanmyaozpniqismex", + "price": "0.019000", + "ttl": "360", + "crid": "banner-one-creative", + "currency": "EUR", + "width": "300", + "height": "250", + "isVideo": false, + "code": "%3C!--%20Banner%20one%20--%3E", + "adomain": ["adocean.pl"] + } + ] + } + }, + { + "expectedRequest": { + "uri": "https://myao.adocean.pl/_10000000/ad.json?aosize=400x600&aouserid=gemius-user-id&gdpr=1&gdpr_consent=BOQHk-4OSlWKFBoABBPLBd-AAAAgWAHAACAAsAPQBSACmgFTAOkA&id=tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7&pbsrv_v=2.0.0&slaves=wafpdwlrks", + "headers": { + "Accept": ["application/json"], + "Content-Type": ["application/json;charset=utf-8"], + "Referer": ["https://example.com/publisher_page"], + "User-Agent": ["test-user-agent"], + "X-Forwarded-For": ["1.0.0.0"] + }, + "impIDs": ["banner-imp-two"] + }, + "mockResponse": { + "status": 200, + "body": [ + { + "id": "adoceanmyaowafpdwlrks", + "price": "0.020000", + "ttl": "240", + "crid": "banner-two-creative", + "currency": "EUR", + "width": "400", + "height": "600", + "isVideo": false, + "code": "%3C!--%20Banner%20two%20--%3E", + "adomain": ["adocean.pl"] + } + ] + } + }, + { + "expectedRequest": { + "uri": "https://myao.adocean.pl/_10000000/ad.json?aouserid=gemius-user-id&dur=60&gdpr=1&gdpr_consent=BOQHk-4OSlWKFBoABBPLBd-AAAAgWAHAACAAsAPQBSACmgFTAOkA&id=tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7&maxdur=60&mindur=10&pbsrv_v=2.0.0&slaves=lifgmvmpfj&spots=1", + "headers": { + "Accept": ["application/json"], + "Content-Type": ["application/json;charset=utf-8"], + "Referer": ["https://example.com/publisher_page"], + "User-Agent": ["test-user-agent"], + "X-Forwarded-For": ["1.0.0.0"] + }, + "impIDs": ["video-imp"] + }, + "mockResponse": { + "status": 200, + "body": [ + { + "id": "adoceanmyaolifgmvmpfj", + "price": "0.021000", + "ttl": "120", + "crid": "video-creative", + "currency": "EUR", + "width": "640", + "height": "360", + "isVideo": true, + "code": "%3C!--%20Video%20--%3E" + } + ] + } + } + ], + "expectedBidResponses": [ + { + "currency": "EUR", + "bids": [ + { + "bid": { + "id": "adoceanmyaozpniqismex", + "impid": "banner-imp-one", + "price": 0.019, + "adm": "", + "crid": "banner-one-creative", + "adomain": ["adocean.pl"], + "w": 300, + "h": 250, + "exp": 360 + }, + "type": "banner" + } + ] + }, + { + "currency": "EUR", + "bids": [ + { + "bid": { + "id": "adoceanmyaowafpdwlrks", + "impid": "banner-imp-two", + "price": 0.02, + "adm": "", + "crid": "banner-two-creative", + "adomain": ["adocean.pl"], + "w": 400, + "h": 600, + "exp": 240 + }, + "type": "banner" + } + ] + }, + { + "currency": "EUR", + "bids": [ + { + "bid": { + "id": "adoceanmyaolifgmvmpfj", + "impid": "video-imp", + "price": 0.021, + "adm": "", + "crid": "video-creative", + "w": 640, + "h": 360, + "exp": 120 + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/adocean/adoceantest/supplemental/network-error.json b/adapters/adocean/adoceantest/supplemental/network-error.json new file mode 100644 index 00000000000..16aa780287e --- /dev/null +++ b/adapters/adocean/adoceantest/supplemental/network-error.json @@ -0,0 +1,66 @@ +{ + "mockBidRequest": { + "id": "banner-request", + "test": 1, + "regs": { + "gdpr": 1 + }, + "user": { + "buyeruid": "gemius-user-id", + "consent": "BOQHk-4OSlWKFBoABBPLBd-AAAAgWAHAACAAsAPQBSACmgFTAOkA" + }, + "device": { + "ua": "test-user-agent", + "ip": "1.0.0.0" + }, + "site": { + "page": "https://example.com/publisher_page" + }, + "imp": [ + { + "id": "banner-imp", + "banner": { + "format": [ + {"w": 300, "h": 250}, + {"w": 400, "h": 600} + ] + }, + "ext": { + "bidder": { + "emitterPrefix": "myao", + "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", + "slaveId": "adoceanmyaozpniqismex", + "emitterRequestParams": { + "special key": "special +value" + } + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://myao.adocean.pl/_10000000/ad.json?aosize=300x250%2C400x600&aouserid=gemius-user-id&gdpr=1&gdpr_consent=BOQHk-4OSlWKFBoABBPLBd-AAAAgWAHAACAAsAPQBSACmgFTAOkA&id=tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7&pbsrv_v=2.0.0&slaves=zpniqismex&special%20key=special%20%2Bvalue", + "headers": { + "Accept": ["application/json"], + "Content-Type": ["application/json;charset=utf-8"], + "Referer": ["https://example.com/publisher_page"], + "User-Agent": ["test-user-agent"], + "X-Forwarded-For": ["1.0.0.0"] + }, + "impIDs": ["banner-imp"] + }, + "mockResponse": { + "status": 500, + "body": {} + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "unexpected status code: 500", + "comparison": "literal" + } + ] +} diff --git a/adapters/adocean/adoceantest/supplemental/no-bid.json b/adapters/adocean/adoceantest/supplemental/no-bid.json new file mode 100644 index 00000000000..fc961a7c12f --- /dev/null +++ b/adapters/adocean/adoceantest/supplemental/no-bid.json @@ -0,0 +1,62 @@ +{ + "mockBidRequest": { + "id": "banner-request", + "test": 1, + "regs": { + "gdpr": 1 + }, + "user": { + "buyeruid": "gemius-user-id", + "consent": "BOQHk-4OSlWKFBoABBPLBd-AAAAgWAHAACAAsAPQBSACmgFTAOkA" + }, + "device": { + "ua": "test-user-agent", + "ip": "1.0.0.0" + }, + "site": { + "page": "https://example.com/publisher_page" + }, + "imp": [ + { + "id": "banner-imp", + "banner": { + "format": [ + {"w": 300, "h": 250} + ] + }, + "ext": { + "bidder": { + "emitterPrefix": "myao", + "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", + "slaveId": "adoceanmyaozpniqismex" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://myao.adocean.pl/_10000000/ad.json?aosize=300x250&aouserid=gemius-user-id&gdpr=1&gdpr_consent=BOQHk-4OSlWKFBoABBPLBd-AAAAgWAHAACAAsAPQBSACmgFTAOkA&id=tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7&pbsrv_v=2.0.0&slaves=zpniqismex", + "headers": { + "Accept": ["application/json"], + "Content-Type": ["application/json;charset=utf-8"], + "Referer": ["https://example.com/publisher_page"], + "User-Agent": ["test-user-agent"], + "X-Forwarded-For": ["1.0.0.0"] + }, + "impIDs": ["banner-imp"] + }, + "mockResponse": { + "status": 200, + "body": [] + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [] + } + ] +} diff --git a/adapters/adocean/adoceantest/supplemental/no-impression.json b/adapters/adocean/adoceantest/supplemental/no-impression.json new file mode 100644 index 00000000000..8f2a8eef351 --- /dev/null +++ b/adapters/adocean/adoceantest/supplemental/no-impression.json @@ -0,0 +1,36 @@ +{ + "mockBidRequest": { + "id": "9ed903f4-383d-406b-8011-4f06526cb02c", + "source": { + "tid": "9ed903f4-383d-406b-8011-4f06526cb02c" + }, + "tmax": 1000, + "imp": [], + "test": 1, + "ext": { + "prebid": { + "targeting": { + "includewinners": true, + "includebidderkeys": false + } + } + }, + "site": { + "publisher": { + "id": "1" + }, + "page": "http://example.com/test.html" + }, + "device": { + "w": 1280, + "h": 720, + "ip": "192.168.1.1" + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "No impression in the bid request", + "comparison": "literal" + } + ] +} diff --git a/adapters/adocean/adoceantest/supplemental/video-outstream.json b/adapters/adocean/adoceantest/supplemental/video-outstream.json new file mode 100644 index 00000000000..f71cbdcb637 --- /dev/null +++ b/adapters/adocean/adoceantest/supplemental/video-outstream.json @@ -0,0 +1,34 @@ +{ + "mockBidRequest": { + "id": "video-request", + "test": 1, + "device": { + "ua": "test-user-agent", + "ip": "1.0.0.0" + }, + "imp": [ + { + "id": "video-imp", + "video": { + "mimes": ["video/mp4"], + "minduration": 10, + "maxduration": 60, + "placement": 2 + }, + "ext": { + "bidder": { + "emitterPrefix": "myao", + "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", + "slaveId": "adoceanmyaolifgmvmpfj" + } + } + } + ] + }, + "expectedMakeRequestsErrors": [ + { + "value": "ignoring imp id=video-imp: AdOcean doesn't support outstream video", + "comparison": "literal" + } + ] +} diff --git a/adapters/adocean/params_test.go b/adapters/adocean/params_test.go new file mode 100644 index 00000000000..80b7e396a1f --- /dev/null +++ b/adapters/adocean/params_test.go @@ -0,0 +1,48 @@ +package adocean + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/v4/openrtb_ext" +) + +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json schemas: %v", err) + } + + for _, params := range validParams { + if err := validator.Validate(openrtb_ext.BidderAdOcean, json.RawMessage(params)); err != nil { + t.Errorf("Schema rejected valid params: %s", params) + } + } +} + +func TestInvalidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json schemas: %v", err) + } + + for _, params := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderAdOcean, json.RawMessage(params)); err == nil { + t.Errorf("Schema allowed invalid params: %s", params) + } + } +} + +var validParams = []string{ + `{"emitterPrefix":"myao","masterId":"tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7","slaveId":"adoceanmyaozpniqismex"}`, + `{"emitterPrefix":"myao-test","masterId":"master_id.1","slaveId":"adoceanmyaozpniqismex","emitterRequestParams":{"test_parameter":"1"}}`, +} + +var invalidParams = []string{ + `{}`, + `{"masterId":"tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7","slaveId":"adoceanmyaozpniqismex"}`, + `{"emitterPrefix":"myao.adocean.pl","masterId":"tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7","slaveId":"adoceanmyaozpniqismex"}`, + `{"emitterPrefix":"myao","masterId":"master/id","slaveId":"adoceanmyaozpniqismex"}`, + `{"emitterPrefix":"myao","masterId":"tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7","slaveId":"myaozpniqismex"}`, + `{"emitterPrefix":"myao","masterId":"tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7","slaveId":"adoceanmyaozpniqismex","emitterRequestParams":["invalid"]}`, +} diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go index 8e6e0cd061d..cf8a2fe3da7 100755 --- a/exchange/adapter_builders.go +++ b/exchange/adapter_builders.go @@ -17,6 +17,7 @@ import ( "github.com/prebid/prebid-server/v4/adapters/admatic" "github.com/prebid/prebid-server/v4/adapters/admixer" "github.com/prebid/prebid-server/v4/adapters/adnuntius" + "github.com/prebid/prebid-server/v4/adapters/adocean" "github.com/prebid/prebid-server/v4/adapters/adot" "github.com/prebid/prebid-server/v4/adapters/adpone" "github.com/prebid/prebid-server/v4/adapters/adprime" @@ -290,6 +291,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderAdmatic: admatic.Builder, openrtb_ext.BidderAdmixer: admixer.Builder, openrtb_ext.BidderAdnuntius: adnuntius.Builder, + openrtb_ext.BidderAdOcean: adocean.Builder, openrtb_ext.BidderAdot: adot.Builder, openrtb_ext.BidderAdpone: adpone.Builder, openrtb_ext.BidderAdprime: adprime.Builder, diff --git a/exchange/adapter_util.go b/exchange/adapter_util.go index bddbaed5fce..d9ecfb5f60b 100644 --- a/exchange/adapter_util.go +++ b/exchange/adapter_util.go @@ -128,7 +128,6 @@ func GetDisabledBidderWarningMessages(infos config.BidderInfos) map[string]strin "liftoff": `Bidder "liftoff" is no longer available in Prebid Server. If you're looking to use the Vungle Exchange adapter, please rename it to "vungle" in your configuration.`, "gothamads": `Bidder "gothamads" is no longer available in Prebid Server. Please rename it to "intenze" in your configuration.`, "intertech": `Bidder "intertech" is no longer available in Prebid Server. Please update your configuration.`, - "adocean": `Bidder "adocean" is no longer available in Prebid Server. Please update your configuration.`, "dxkulture": `Bidder "dxkulture" is no longer available in Prebid Server. Please update your configuration.`, "mobupps": `Bidder "mobupps" is no longer available in Prebid Server. Please update your configuration.`, "vimayx": `Bidder "vimayx" is no longer available in Prebid Server. Please update your configuration.`, diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 0c8e92672c4..360d1043fe5 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -33,6 +33,7 @@ var coreBidderNames []BidderName = []BidderName{ BidderAdmatic, BidderAdmixer, BidderAdnuntius, + BidderAdOcean, BidderAdot, BidderAdpone, BidderAdprime, @@ -413,6 +414,7 @@ const ( BidderAdmatic BidderName = "admatic" BidderAdmixer BidderName = "admixer" BidderAdnuntius BidderName = "adnuntius" + BidderAdOcean BidderName = "adocean" BidderAdot BidderName = "adot" BidderAdpone BidderName = "adpone" BidderAdprime BidderName = "adprime" diff --git a/openrtb_ext/imp_adocean.go b/openrtb_ext/imp_adocean.go new file mode 100644 index 00000000000..4b58d551753 --- /dev/null +++ b/openrtb_ext/imp_adocean.go @@ -0,0 +1,8 @@ +package openrtb_ext + +type ExtImpAdOcean struct { + EmitterPrefix string `json:"emitterPrefix"` + MasterID string `json:"masterId"` + SlaveID string `json:"slaveId"` + EmitterRequestParams map[string]any `json:"emitterRequestParams"` +} diff --git a/static/bidder-info/adocean.yaml b/static/bidder-info/adocean.yaml new file mode 100644 index 00000000000..586c74f959c --- /dev/null +++ b/static/bidder-info/adocean.yaml @@ -0,0 +1,18 @@ +endpoint: "https://{{.Host}}.adocean.pl" +geoscope: + - global +maintainer: + email: "prebid@gemius.com" +gvlVendorID: 328 +modifyingVastXmlAllowed: true +openrtb: + version: 2.6 +capabilities: + app: + mediaTypes: + - banner + - video + site: + mediaTypes: + - banner + - video diff --git a/static/bidder-params/adocean.json b/static/bidder-params/adocean.json new file mode 100644 index 00000000000..e62db2b9d55 --- /dev/null +++ b/static/bidder-params/adocean.json @@ -0,0 +1,28 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "AdOcean Adapter Params", + "description": "A schema which validates params accepted by the AdOcean adapter", + "type": "object", + "properties": { + "emitterPrefix": { + "type": "string", + "description": "AdOcean emitter prefix", + "pattern": "^[\\w\\-]+$" + }, + "masterId": { + "type": "string", + "description": "Master's id", + "pattern": "^[\\w.]+$" + }, + "slaveId": { + "type": "string", + "description": "Slave's id", + "pattern": "^adocean[\\w.]+$" + }, + "emitterRequestParams": { + "type": "object", + "description": "Extra targeting parameters" + } + }, + "required": ["emitterPrefix", "masterId", "slaveId"] +} From a60f0fd84975534e0d227e0d0f8f29a6a9e2f9dd Mon Sep 17 00:00:00 2001 From: Patryk Grzegorczyk Date: Tue, 28 Jul 2026 16:44:09 +0200 Subject: [PATCH 2/3] [ITADS-2318] More tests --- adapters/adocean/adocean.go | 19 +++- adapters/adocean/adocean_test.go | 87 +++++++++++++++++++ .../supplemental/incomplete-bid.json | 78 +++++++++++++++++ .../supplemental/invalid-request-ext.json | 43 +++++++++ .../adoceantest/supplemental/no-bid.json | 7 +- .../adoceantest/supplemental/no-type.json | 28 ++++++ .../adoceantest/supplemental/status_204.json | 61 +++++++++++++ .../adoceantest/supplemental/status_400.json | 66 ++++++++++++++ .../{network-error.json => status_500.json} | 0 adapters/adocean/params_test.go | 1 + 10 files changed, 381 insertions(+), 9 deletions(-) create mode 100644 adapters/adocean/adoceantest/supplemental/incomplete-bid.json create mode 100644 adapters/adocean/adoceantest/supplemental/invalid-request-ext.json create mode 100644 adapters/adocean/adoceantest/supplemental/no-type.json create mode 100644 adapters/adocean/adoceantest/supplemental/status_204.json create mode 100644 adapters/adocean/adoceantest/supplemental/status_400.json rename adapters/adocean/adoceantest/supplemental/{network-error.json => status_500.json} (100%) diff --git a/adapters/adocean/adocean.go b/adapters/adocean/adocean.go index 7fedd827670..342e486c06c 100644 --- a/adapters/adocean/adocean.go +++ b/adapters/adocean/adocean.go @@ -146,7 +146,11 @@ func (a *adapter) makeRequest(request *openrtb2.BidRequest, imp *openrtb2.Imp, p requestURL.Path = "/_" + strconv.Itoa(randomizedPart) + "/ad.json" // RFC 3986 requires that spaces in query parameters be encoded as %20, // but the Go standard library encodes them as + - requestURL.RawQuery = strings.ReplaceAll(buildQuery(request, imp, params).Encode(), "+", "%20") + query, err := buildQuery(request, imp, params) + if err != nil { + return nil, err + } + requestURL.RawQuery = strings.ReplaceAll(query.Encode(), "+", "%20") if len(requestURL.String()) >= maxUriLength { return nil, &errortypes.BadInput{ Message: fmt.Sprintf("AdOcean request URL exceeds maximum length of %d characters", maxUriLength), @@ -161,9 +165,14 @@ func (a *adapter) makeRequest(request *openrtb2.BidRequest, imp *openrtb2.Imp, p }, nil } -func buildQuery(request *openrtb2.BidRequest, imp *openrtb2.Imp, params *openrtb_ext.ExtImpAdOcean) url.Values { +func buildQuery(request *openrtb2.BidRequest, imp *openrtb2.Imp, params *openrtb_ext.ExtImpAdOcean) (url.Values, error) { query := url.Values{} query.Set("pbsrv_v", adapterVersion) + if params.MasterID == "" || params.SlaveID == "" { + return nil, &errortypes.BadInput{ + Message: "missing required AdOcean parameters: masterId and slaveId must be provided", + } + } query.Set("id", params.MasterID) query.Set("slaves", shortSlaveID(params.SlaveID)) @@ -199,7 +208,7 @@ func buildQuery(request *openrtb2.BidRequest, imp *openrtb2.Imp, params *openrtb } } - return query + return query, nil } func shortSlaveID(slaveID string) string { @@ -300,6 +309,10 @@ func (a *adapter) MakeBids( bidderResponse.Currency = currency } + if len(bidderResponse.Bids) == 0 { + return nil, errs + } + return bidderResponse, errs } diff --git a/adapters/adocean/adocean_test.go b/adapters/adocean/adocean_test.go index b5ab50540d5..5d4d2b03a5e 100644 --- a/adapters/adocean/adocean_test.go +++ b/adapters/adocean/adocean_test.go @@ -1,6 +1,7 @@ package adocean import ( + "encoding/json" "strings" "testing" "text/template" @@ -61,3 +62,89 @@ func TestResolveEndpointTemplate(t *testing.T) { t.Fatalf("resolveEndpointTemplate returned %v, expected %v", url, expectedURL) } } + +func TestMakeRequestsRejectsEmptyRequest(t *testing.T) { + bidder := adapter{} + requests, errs := bidder.MakeRequests(&openrtb2.BidRequest{}, nil) + if requests != nil { + t.Fatal("MakeRequests should not return requests for an empty bid request") + } + if len(errs) != 1 || errs[0].Error() != "No impression in the bid request" { + t.Fatalf("MakeRequests returned unexpected errors: %v", errs) + } +} + +func TestParseImpExt(t *testing.T) { + params, err := parseImpExt(&openrtb2.Imp{ID: "valid", Ext: json.RawMessage(`{"bidder":{"emitterPrefix":"myao","masterId":"master","slaveId":"placement"}}`)}) + if err != nil { + t.Fatalf("parseImpExt returned unexpected error: %v", err) + } + if params.EmitterPrefix != "myao" || params.MasterID != "master" || params.SlaveID != "placement" { + t.Fatalf("parseImpExt returned unexpected params: %+v", params) + } + + _, err = parseImpExt(&openrtb2.Imp{ID: "invalid", Ext: json.RawMessage(`{`)}) + if err == nil { + t.Fatal("parseImpExt should reject an invalid extension") + } +} + +func TestHelpers(t *testing.T) { + if got := shortSlaveID("short"); got != "short" { + t.Fatalf("shortSlaveID returned %q, expected short ID", got) + } + if got := shortSlaveID("adoceanmyaozpniqismex"); got != "zpniqismex" { + t.Fatalf("shortSlaveID returned %q, expected last %d characters", got, slaveIDLength) + } + + width, height := int64(300), int64(250) + if sizes := getBannerSizes(&openrtb2.Banner{W: &width, H: &height}); len(sizes) != 1 || sizes[0] != "300x250" { + t.Fatalf("getBannerSizes returned unexpected dimensions: %v", sizes) + } + if sizes := getBannerSizes(&openrtb2.Banner{}); sizes != nil { + t.Fatalf("getBannerSizes returned %v for a banner without dimensions", sizes) + } +} + +func TestMakeBid(t *testing.T) { + valid := responseAdUnit{ + ID: "placement", + Currency: "EUR", + Price: "1.25", + TTL: "", + Width: "300", + Height: "250", + IsVideo: true, + Code: "creative%20markup", + } + bid, currency, err := makeBid(valid, "imp-1") + if err != nil { + t.Fatalf("makeBid returned unexpected error: %v", err) + } + if currency != "EUR" || bid.Bid.AdM != "creative markup" || bid.BidType != openrtb_ext.BidTypeVideo || bid.Bid.ImpID != "imp-1" { + t.Fatalf("makeBid returned unexpected bid: %+v, currency=%q", bid, currency) + } + if bid.Bid.ADomain == nil || bid.BidMeta.AdvertiserDomains == nil { + t.Fatal("makeBid should provide empty advertiser-domain slices") + } + + for _, test := range []struct { + name string + adUnit responseAdUnit + field string + }{ + {name: "incomplete", adUnit: responseAdUnit{ID: "placement"}, field: "incomplete bid"}, + {name: "invalid price", adUnit: responseAdUnit{ID: "placement", Price: "invalid", Width: "300", Height: "250", Code: "markup"}, field: "invalid price"}, + {name: "invalid width", adUnit: responseAdUnit{ID: "placement", Price: "1", Width: "invalid", Height: "250", Code: "markup"}, field: "invalid width"}, + {name: "invalid height", adUnit: responseAdUnit{ID: "placement", Price: "1", Width: "300", Height: "invalid", Code: "markup"}, field: "invalid height"}, + {name: "invalid ttl", adUnit: responseAdUnit{ID: "placement", Price: "1", TTL: "invalid", Width: "300", Height: "250", Code: "markup"}, field: "invalid ttl"}, + {name: "invalid code", adUnit: responseAdUnit{ID: "placement", Price: "1", Width: "300", Height: "250", Code: "%"}, field: "invalid code"}, + } { + t.Run(test.name, func(t *testing.T) { + _, _, err := makeBid(test.adUnit, "imp-1") + if err == nil || !strings.Contains(err.Error(), test.field) { + t.Fatalf("makeBid returned unexpected error: %v", err) + } + }) + } +} diff --git a/adapters/adocean/adoceantest/supplemental/incomplete-bid.json b/adapters/adocean/adoceantest/supplemental/incomplete-bid.json new file mode 100644 index 00000000000..a250475f0d0 --- /dev/null +++ b/adapters/adocean/adoceantest/supplemental/incomplete-bid.json @@ -0,0 +1,78 @@ +{ + "mockBidRequest": { + "id": "banner-request", + "test": 1, + "regs": { + "gdpr": 1 + }, + "user": { + "buyeruid": "gemius-user-id", + "consent": "BOQHk-4OSlWKFBoABBPLBd-AAAAgWAHAACAAsAPQBSACmgFTAOkA" + }, + "device": { + "ua": "test-user-agent", + "ip": "1.0.0.0" + }, + "site": { + "page": "https://example.com/publisher_page" + }, + "imp": [ + { + "id": "banner-imp", + "banner": { + "format": [ + {"w": 300, "h": 250}, + {"w": 400, "h": 600} + ] + }, + "ext": { + "bidder": { + "emitterPrefix": "myao", + "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", + "slaveId": "adoceanmyaozpniqismex", + "emitterRequestParams": { + "special key": "special +value" + } + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://myao.adocean.pl/_10000000/ad.json?aosize=300x250%2C400x600&aouserid=gemius-user-id&gdpr=1&gdpr_consent=BOQHk-4OSlWKFBoABBPLBd-AAAAgWAHAACAAsAPQBSACmgFTAOkA&id=tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7&pbsrv_v=2.0.0&slaves=zpniqismex&special%20key=special%20%2Bvalue", + "headers": { + "Accept": ["application/json"], + "Content-Type": ["application/json;charset=utf-8"], + "Referer": ["https://example.com/publisher_page"], + "User-Agent": ["test-user-agent"], + "X-Forwarded-For": ["1.0.0.0"] + }, + "impIDs": ["banner-imp"] + }, + "mockResponse": { + "status": 200, + "body": [ + { + "id": "adoceanmyaozpniqismex", + "ttl": "360", + "crid": "veeinoriep", + "currency": "EUR", + "width": "300", + "height": "250", + "isVideo": false, + "code": "%3C!--%20Creative%20--%3E", + "adomain": ["adocean.pl"] + } + ] + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "incomplete bid for AdOcean placement \"adoceanmyaozpniqismex\"", + "comparison": "literal" + } + ] +} diff --git a/adapters/adocean/adoceantest/supplemental/invalid-request-ext.json b/adapters/adocean/adoceantest/supplemental/invalid-request-ext.json new file mode 100644 index 00000000000..50af61cdfb2 --- /dev/null +++ b/adapters/adocean/adoceantest/supplemental/invalid-request-ext.json @@ -0,0 +1,43 @@ +{ + "mockBidRequest": { + "id": "banner-request", + "test": 1, + "regs": { + "gdpr": 1 + }, + "user": { + "buyeruid": "gemius-user-id", + "consent": "BOQHk-4OSlWKFBoABBPLBd-AAAAgWAHAACAAsAPQBSACmgFTAOkA" + }, + "device": { + "ua": "test-user-agent", + "ip": "1.0.0.0" + }, + "site": { + "page": "https://example.com/publisher_page" + }, + "imp": [ + { + "id": "banner-imp", + "banner": { + "format": [ + {"w": 300, "h": 250}, + {"w": 400, "h": 600} + ] + }, + "ext": { + "bidder": { + "emitterPrefix": "myao", + "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7" + } + } + } + ] + }, + "expectedMakeRequestsErrors": [ + { + "value": "missing required AdOcean parameters: masterId and slaveId must be provided", + "comparison": "literal" + } + ] +} diff --git a/adapters/adocean/adoceantest/supplemental/no-bid.json b/adapters/adocean/adoceantest/supplemental/no-bid.json index fc961a7c12f..7a18ab6e323 100644 --- a/adapters/adocean/adoceantest/supplemental/no-bid.json +++ b/adapters/adocean/adoceantest/supplemental/no-bid.json @@ -53,10 +53,5 @@ } } ], - "expectedBidResponses": [ - { - "currency": "USD", - "bids": [] - } - ] + "expectedBidResponses": [] } diff --git a/adapters/adocean/adoceantest/supplemental/no-type.json b/adapters/adocean/adoceantest/supplemental/no-type.json new file mode 100644 index 00000000000..5034c924036 --- /dev/null +++ b/adapters/adocean/adoceantest/supplemental/no-type.json @@ -0,0 +1,28 @@ +{ + "mockBidRequest": { + "id": "some-request", + "test": 1, + "device": { + "ua": "test-user-agent", + "ip": "1.0.0.0" + }, + "imp": [ + { + "id": "unknown-imp", + "ext": { + "bidder": { + "emitterPrefix": "myao", + "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", + "slaveId": "adoceanmyaolifgmvmpfj" + } + } + } + ] + }, + "expectedMakeRequestsErrors": [ + { + "value": "ignoring imp id=unknown-imp: AdOcean supports only banner and instream video", + "comparison": "literal" + } + ] +} diff --git a/adapters/adocean/adoceantest/supplemental/status_204.json b/adapters/adocean/adoceantest/supplemental/status_204.json new file mode 100644 index 00000000000..5a261255531 --- /dev/null +++ b/adapters/adocean/adoceantest/supplemental/status_204.json @@ -0,0 +1,61 @@ +{ + "mockBidRequest": { + "id": "banner-request", + "test": 1, + "regs": { + "gdpr": 1 + }, + "user": { + "buyeruid": "gemius-user-id", + "consent": "BOQHk-4OSlWKFBoABBPLBd-AAAAgWAHAACAAsAPQBSACmgFTAOkA" + }, + "device": { + "ua": "test-user-agent", + "ipv6": "1000:ab8::1" + }, + "site": { + "page": "https://example.com/publisher_page" + }, + "imp": [ + { + "id": "banner-imp", + "banner": { + "format": [ + {"w": 300, "h": 250}, + {"w": 400, "h": 600} + ] + }, + "ext": { + "bidder": { + "emitterPrefix": "myao", + "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", + "slaveId": "adoceanmyaozpniqismex", + "emitterRequestParams": { + "special key": "special +value" + } + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://myao.adocean.pl/_10000000/ad.json?aosize=300x250%2C400x600&aouserid=gemius-user-id&gdpr=1&gdpr_consent=BOQHk-4OSlWKFBoABBPLBd-AAAAgWAHAACAAsAPQBSACmgFTAOkA&id=tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7&pbsrv_v=2.0.0&slaves=zpniqismex&special%20key=special%20%2Bvalue", + "headers": { + "Accept": ["application/json"], + "Content-Type": ["application/json;charset=utf-8"], + "Referer": ["https://example.com/publisher_page"], + "User-Agent": ["test-user-agent"], + "X-Forwarded-For": ["1000:ab8::1"] + }, + "impIDs": ["banner-imp"] + }, + "mockResponse": { + "status": 204, + "body": {} + } + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/adocean/adoceantest/supplemental/status_400.json b/adapters/adocean/adoceantest/supplemental/status_400.json new file mode 100644 index 00000000000..8ec04bf9080 --- /dev/null +++ b/adapters/adocean/adoceantest/supplemental/status_400.json @@ -0,0 +1,66 @@ +{ + "mockBidRequest": { + "id": "banner-request", + "test": 1, + "regs": { + "gdpr": 1 + }, + "user": { + "buyeruid": "gemius-user-id", + "consent": "BOQHk-4OSlWKFBoABBPLBd-AAAAgWAHAACAAsAPQBSACmgFTAOkA" + }, + "device": { + "ua": "test-user-agent", + "ip": "1.0.0.0" + }, + "site": { + "page": "https://example.com/publisher_page" + }, + "imp": [ + { + "id": "banner-imp", + "banner": { + "format": [ + {"w": 300, "h": 250}, + {"w": 400, "h": 600} + ] + }, + "ext": { + "bidder": { + "emitterPrefix": "myao", + "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", + "slaveId": "adoceanmyaozpniqismex", + "emitterRequestParams": { + "special key": "special +value" + } + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://myao.adocean.pl/_10000000/ad.json?aosize=300x250%2C400x600&aouserid=gemius-user-id&gdpr=1&gdpr_consent=BOQHk-4OSlWKFBoABBPLBd-AAAAgWAHAACAAsAPQBSACmgFTAOkA&id=tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7&pbsrv_v=2.0.0&slaves=zpniqismex&special%20key=special%20%2Bvalue", + "headers": { + "Accept": ["application/json"], + "Content-Type": ["application/json;charset=utf-8"], + "Referer": ["https://example.com/publisher_page"], + "User-Agent": ["test-user-agent"], + "X-Forwarded-For": ["1.0.0.0"] + }, + "impIDs": ["banner-imp"] + }, + "mockResponse": { + "status": 400, + "body": {} + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "unexpected status code: 400", + "comparison": "literal" + } + ] +} diff --git a/adapters/adocean/adoceantest/supplemental/network-error.json b/adapters/adocean/adoceantest/supplemental/status_500.json similarity index 100% rename from adapters/adocean/adoceantest/supplemental/network-error.json rename to adapters/adocean/adoceantest/supplemental/status_500.json diff --git a/adapters/adocean/params_test.go b/adapters/adocean/params_test.go index 80b7e396a1f..fbc6649a046 100644 --- a/adapters/adocean/params_test.go +++ b/adapters/adocean/params_test.go @@ -44,5 +44,6 @@ var invalidParams = []string{ `{"emitterPrefix":"myao.adocean.pl","masterId":"tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7","slaveId":"adoceanmyaozpniqismex"}`, `{"emitterPrefix":"myao","masterId":"master/id","slaveId":"adoceanmyaozpniqismex"}`, `{"emitterPrefix":"myao","masterId":"tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7","slaveId":"myaozpniqismex"}`, + `{"emitterPrefix":"myao","masterId":"tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7"}`, `{"emitterPrefix":"myao","masterId":"tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7","slaveId":"adoceanmyaozpniqismex","emitterRequestParams":["invalid"]}`, } From 70d776aa7084e22a36ef8ab223e83374797dd5f8 Mon Sep 17 00:00:00 2001 From: Patryk Grzegorczyk Date: Tue, 28 Jul 2026 16:50:27 +0200 Subject: [PATCH 3/3] [ITADS-2318] Coverage --- .../supplemental/invalid-response-id.json | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 adapters/adocean/adoceantest/supplemental/invalid-response-id.json diff --git a/adapters/adocean/adoceantest/supplemental/invalid-response-id.json b/adapters/adocean/adoceantest/supplemental/invalid-response-id.json new file mode 100644 index 00000000000..921f8a977d1 --- /dev/null +++ b/adapters/adocean/adoceantest/supplemental/invalid-response-id.json @@ -0,0 +1,74 @@ +{ + "mockBidRequest": { + "id": "banner-request", + "test": 1, + "regs": { + "gdpr": 1 + }, + "user": { + "buyeruid": "gemius-user-id", + "consent": "BOQHk-4OSlWKFBoABBPLBd-AAAAgWAHAACAAsAPQBSACmgFTAOkA" + }, + "device": { + "ua": "test-user-agent", + "ip": "1.0.0.0" + }, + "site": { + "page": "https://example.com/publisher_page" + }, + "imp": [ + { + "id": "banner-imp", + "banner": { + "format": [ + {"w": 300, "h": 250}, + {"w": 400, "h": 600} + ] + }, + "ext": { + "bidder": { + "emitterPrefix": "myao", + "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", + "slaveId": "adoceanmyaozpniqismex", + "emitterRequestParams": { + "special key": "special +value" + } + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://myao.adocean.pl/_10000000/ad.json?aosize=300x250%2C400x600&aouserid=gemius-user-id&gdpr=1&gdpr_consent=BOQHk-4OSlWKFBoABBPLBd-AAAAgWAHAACAAsAPQBSACmgFTAOkA&id=tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7&pbsrv_v=2.0.0&slaves=zpniqismex&special%20key=special%20%2Bvalue", + "headers": { + "Accept": ["application/json"], + "Content-Type": ["application/json;charset=utf-8"], + "Referer": ["https://example.com/publisher_page"], + "User-Agent": ["test-user-agent"], + "X-Forwarded-For": ["1.0.0.0"] + }, + "impIDs": ["banner-imp"] + }, + "mockResponse": { + "status": 200, + "body": [ + { + "id": "invalid-response-id", + "price": "0.019000", + "ttl": "360", + "crid": "veeinoriep", + "currency": "EUR", + "width": "300", + "height": "250", + "isVideo": false, + "code": "%3C!--%20Creative%20--%3E", + "adomain": ["adocean.pl"] + } + ] + } + } + ], + "expectedBidResponses": [] +}