diff --git a/adapters/smartx/params_test.go b/adapters/smartx/params_test.go new file mode 100644 index 00000000000..fd28f4ead9b --- /dev/null +++ b/adapters/smartx/params_test.go @@ -0,0 +1,58 @@ +package smartx + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/openrtb_ext" +) + +var validParams = []string{ + `{"tagId":"Nu68JuOWAvrbzoyrOR9a7A", "publisherId":"11986", "siteId":"22860"}`, + `{"tagId":"Nu68JuOWAvrbzoyrOR9a7A", "publisherId":"11986", "appId":"22860"}`, +} + +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 _, validParam := range validParams { + if err := validator.Validate(openrtb_ext.BidderSmartx, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected smartx params: %s", validParam) + } + } +} + +var invalidParams = []string{ + ``, + `null`, + `true`, + `5`, + `[]`, + `{}`, + `{"anyparam": "anyvalue"}`, + `{"tagId":"Nu68JuOWAvrbzoyrOR9a7A"}`, + `{"publisherId":"11986"}`, + `{"siteId":"22860"}`, + `{"appId":"22860"}`, + `{"tagId":"Nu68JuOWAvrbzoyrOR9a7A", "publisherId":"11986"}`, + `{"tagId":"Nu68JuOWAvrbzoyrOR9a7A", "siteId":"22860"}`, + `{"tagId":"Nu68JuOWAvrbzoyrOR9a7A", "appId":"22860"}`, + `{"publisherId":"11986", "appId":"22860"}`, + `{"publisherId":"11986", "appId":"22860"}`, +} + +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 _, invalidParam := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderSmartHub, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} diff --git a/adapters/smartx/smartx.go b/adapters/smartx/smartx.go new file mode 100644 index 00000000000..6fbc94968ca --- /dev/null +++ b/adapters/smartx/smartx.go @@ -0,0 +1,90 @@ +package smartx + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" +) + +type adapter struct { + endpointURL string +} + +func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) { + return &adapter{ + endpointURL: config.Endpoint, + }, nil +} + +func (a *adapter) MakeRequests(openRTBRequest *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) (requestsToBidder []*adapters.RequestData, errs []error) { + openRTBRequestJSON, err := json.Marshal(openRTBRequest) + if err != nil { + errs = append(errs, fmt.Errorf("marshal bidRequest: %w", err)) + return nil, errs + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("x-openrtb-version", "2.5") + + if openRTBRequest.Device != nil { + if openRTBRequest.Device.UA != "" { + headers.Set("User-Agent", openRTBRequest.Device.UA) + } + + if openRTBRequest.Device.IP != "" { + headers.Set("Forwarded", "for="+openRTBRequest.Device.IP) + headers.Set("X-Forwarded-For", openRTBRequest.Device.IP) + } + } + + return append(requestsToBidder, &adapters.RequestData{ + Method: http.MethodPost, + Uri: a.endpointURL, + Body: openRTBRequestJSON, + Headers: headers, + }), nil +} + +func (a *adapter) MakeBids(request *openrtb2.BidRequest, _ *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if adapters.IsResponseStatusCodeNoContent(responseData) { + return nil, nil + } + + if err := adapters.CheckResponseStatusCodeForErrors(responseData); err != nil { + return nil, []error{err} + } + + var response openrtb2.BidResponse + if err := json.Unmarshal(responseData.Body, &response); err != nil { + return nil, []error{err} + } + + if len(response.SeatBid) == 0 { + return nil, []error{errors.New("no bidders found in JSON response")} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp)) + if response.Cur != "" { + bidResponse.Currency = response.Cur + } + + var errs []error + + for _, seatBid := range response.SeatBid { + for i := range seatBid.Bid { + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &seatBid.Bid[i], + BidType: openrtb_ext.BidTypeVideo, + }) + } + } + + return bidResponse, errs +} diff --git a/adapters/smartx/smartx_test.go b/adapters/smartx/smartx_test.go new file mode 100644 index 00000000000..ba6f6ba7762 --- /dev/null +++ b/adapters/smartx/smartx_test.go @@ -0,0 +1,24 @@ +package smartx + +import ( + "testing" + + "github.com/prebid/prebid-server/adapters/adapterstest" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" +) + +const testsDir = "smartxtest" +const testsBidderEndpoint = "https://bid.smartclip.net/bid/1005" + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder( + openrtb_ext.BidderRise, + config.Adapter{Endpoint: testsBidderEndpoint}, + config.Server{ExternalUrl: "http://hosturl.com", GvlID: 115, DataCenter: "2"}) + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, testsDir, bidder) +} diff --git a/adapters/smartx/smartxtest/exemplary/01-video.json b/adapters/smartx/smartxtest/exemplary/01-video.json new file mode 100644 index 00000000000..c4d3f9c5b31 --- /dev/null +++ b/adapters/smartx/smartxtest/exemplary/01-video.json @@ -0,0 +1,77 @@ +{ + "mockBidRequest":{ + "id":"test-request-id-video", + "imp":[ + { + "id":"test-imp-id", + "video":{ + "mimes":[ + "video/mp4" + ] + } + } + ] + }, + "httpCalls":[ + { + "expectedRequest":{ + "uri":"https://bid.smartclip.net/bid/1005", + "body":{ + "id":"test-request-id-video", + "imp":[ + { + "id":"test-imp-id", + "video":{ + "mimes":[ + "video/mp4" + ] + } + } + ] + } + }, + "mockResponse":{ + "status":200, + "body":{ + "id":"test-request-id-video", + "seatbid":[ + { + "seat":"smartadserver", + "bid":[ + { + "id":"8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid":"test-imp-id-video", + "price":0.500000, + "adm":"some-test-ad", + "crid":"crid_10", + "h":576, + "w":1024, + "mtype":2 + } + ] + } + ], + "cur":"EUR" + } + } + } + ], + "expectedBidResponses":[ + { + "bids":[{ + "bid":{ + "id":"8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid":"test-imp-id-video", + "price":0.500000, + "adm":"some-test-ad", + "crid":"crid_10", + "h":576, + "w":1024, + "mtype":2 + }, + "currency":"EUR", + "type": "video" + }] + } + ] +} \ No newline at end of file diff --git a/adapters/smartx/smartxtest/exemplary/02-consent.json b/adapters/smartx/smartxtest/exemplary/02-consent.json new file mode 100644 index 00000000000..bc0e5604f4e --- /dev/null +++ b/adapters/smartx/smartxtest/exemplary/02-consent.json @@ -0,0 +1,124 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "prebid": { + "bidder": { + "smartx": { + "publisherId": "11986", + "tagId": "Nu68JuOWAvrbzoyrOR9a7A", + "siteId": "22860" + } + } + } + } + } + ], + "user": { + "ext": { + "consent": "COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://bid.smartclip.net/bid/1005", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "prebid": { + "bidder": { + "smartx": { + "publisherId": "11986", + "tagId": "Nu68JuOWAvrbzoyrOR9a7A", + "siteId": "22860" + } + } + } + } + } + ], + "user": { + "ext": { + "consent": "COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "h": 250, + "w": 300, + "ext": { + "ix": {} + } + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250, + "ext": { + "ix": {} + } + }, + "type": "video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/smartx/smartxtest/exemplary/03-device.json b/adapters/smartx/smartxtest/exemplary/03-device.json new file mode 100644 index 00000000000..f45cc76c99a --- /dev/null +++ b/adapters/smartx/smartxtest/exemplary/03-device.json @@ -0,0 +1,121 @@ +{ + "mockBidRequest": { + "id": "test-request-id-video", + "device": { + "ua": "Mozilla/5.0 (Linux; Android 12; SAMSUNG SM-G780G) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/17.0 Chrome/96.0.4664.104 Mobile Safari/537.36", + "geo": { + "lat": 48.1663, + "lon": 11.5683, + "type": 2, + "country": "DEU", + "region": "BY", + "city": "Munich", + "zip": "81249", + "ipservice": 3 + }, + "dnt": 0, + "lmt": 0, + "ip": "0.0.0.0", + "devicetype": 4, + "make": "Samsung", + "model": "SM-G780G", + "os": "Android", + "language": "en" + }, + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://bid.smartclip.net/bid/1005", + "body": { + "id": "test-request-id-video", + "device": { + "ua": "Mozilla/5.0 (Linux; Android 12; SAMSUNG SM-G780G) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/17.0 Chrome/96.0.4664.104 Mobile Safari/537.36", + "geo": { + "lat": 48.1663, + "lon": 11.5683, + "type": 2, + "country": "DEU", + "region": "BY", + "city": "Munich", + "zip": "81249", + "ipservice": 3 + }, + "dnt": 0, + "lmt": 0, + "ip": "0.0.0.0", + "devicetype": 4, + "make": "Samsung", + "model": "SM-G780G", + "os": "Android", + "language": "en" + }, + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id-video", + "seatbid": [ + { + "seat": "smartadserver", + "bid": [ + { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id-video", + "price": 0.500000, + "adm": "some-test-ad", + "crid": "crid_10", + "h": 576, + "w": 1024, + "mtype": 2 + } + ] + } + ], + "cur": "EUR" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id-video", + "price": 0.500000, + "adm": "some-test-ad", + "crid": "crid_10", + "h": 576, + "w": 1024, + "mtype": 2 + }, + "currency": "EUR", + "type": "video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/smartx/smartxtest/supplemental/02-internal-server-error.json b/adapters/smartx/smartxtest/supplemental/02-internal-server-error.json new file mode 100644 index 00000000000..d44ef7f77e1 --- /dev/null +++ b/adapters/smartx/smartxtest/supplemental/02-internal-server-error.json @@ -0,0 +1,45 @@ +{ + "mockBidRequest": { + "id": "test-internal-server-error-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://bid.smartclip.net/bid/1005", + "body": { + "id": "test-internal-server-error-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + } + } + ] + } + }, + "mockResponse": { + "status": 400, + "body": {} + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 400. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/smartx/smartxtest/supplemental/03-missing-bidder-in-response.json b/adapters/smartx/smartxtest/supplemental/03-missing-bidder-in-response.json new file mode 100644 index 00000000000..71875f12809 --- /dev/null +++ b/adapters/smartx/smartxtest/supplemental/03-missing-bidder-in-response.json @@ -0,0 +1,47 @@ +{ + "mockBidRequest": { + "id": "test-missing-bidder-in-response-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://bid.smartclip.net/bid/1005", + "body": { + "id": "test-missing-bidder-in-response-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + } + } + ] + } + }, + "mockResponse": { + "body": { + "seatbid": [] + }, + "status": 200 + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "no bidders found in JSON response", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go index c5b4b1134cc..95cb1626085 100755 --- a/exchange/adapter_builders.go +++ b/exchange/adapter_builders.go @@ -148,6 +148,7 @@ import ( "github.com/prebid/prebid-server/adapters/smartadserver" "github.com/prebid/prebid-server/adapters/smarthub" "github.com/prebid/prebid-server/adapters/smartrtb" + "github.com/prebid/prebid-server/adapters/smartx" "github.com/prebid/prebid-server/adapters/smartyads" "github.com/prebid/prebid-server/adapters/smilewanted" "github.com/prebid/prebid-server/adapters/sonobi" @@ -348,6 +349,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderSmartAdserver: smartadserver.Builder, openrtb_ext.BidderSmartHub: smarthub.Builder, openrtb_ext.BidderSmartRTB: smartrtb.Builder, + openrtb_ext.BidderSmartx: smartx.Builder, openrtb_ext.BidderSmartyAds: smartyads.Builder, openrtb_ext.BidderSmileWanted: smilewanted.Builder, openrtb_ext.BidderSonobi: sonobi.Builder, diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 67f43ec62dd..475d56a7ba8 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -178,6 +178,7 @@ var coreBidderNames []BidderName = []BidderName{ BidderSmartAdserver, BidderSmartHub, BidderSmartRTB, + BidderSmartx, BidderSmartyAds, BidderSmileWanted, BidderSonobi, @@ -466,6 +467,7 @@ const ( BidderSmartAdserver BidderName = "smartadserver" BidderSmartHub BidderName = "smarthub" BidderSmartRTB BidderName = "smartrtb" + BidderSmartx BidderName = "smartx" BidderSmartyAds BidderName = "smartyads" BidderSmileWanted BidderName = "smilewanted" BidderSonobi BidderName = "sonobi" diff --git a/openrtb_ext/imp_smartx.go b/openrtb_ext/imp_smartx.go new file mode 100644 index 00000000000..9a2975b01de --- /dev/null +++ b/openrtb_ext/imp_smartx.go @@ -0,0 +1,10 @@ +package openrtb_ext + +type ExtImpSmartclip struct { + TagID string `json:"tagId"` + PublisherID string `json:"publisherId"` + SiteID string `json:"siteId"` + AppID string `json:"appId"` + BundleID string `json:"bundleId"` + StoreURL string `json:"storeUrl"` +} diff --git a/static/bidder-info/smartx.yaml b/static/bidder-info/smartx.yaml new file mode 100644 index 00000000000..9a387ecfbd2 --- /dev/null +++ b/static/bidder-info/smartx.yaml @@ -0,0 +1,12 @@ +endpoint: "https://bid.smartclip.net/bid/1005" +maintainer: + email: "bidding@smartclip.tv" +gvlVendorID: 115 +modifyingVastXmlAllowed: false +capabilities: + site: + mediaTypes: + - video + app: + mediaTypes: + - video diff --git a/static/bidder-params/smartx.json b/static/bidder-params/smartx.json new file mode 100644 index 00000000000..3bd97456770 --- /dev/null +++ b/static/bidder-params/smartx.json @@ -0,0 +1,37 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "smartclip.tv Adapter Params", + "description": "A schema which validates params accepted by the smartclip.tv adapter", + "type": "object", + "properties": { + "tagId": { + "type": "string", + "description": "Ad tag ID" + }, + "publisherId": { + "type": "string", + "description": "Publisher ID" + }, + "siteId": { + "type": "string", + "description": "Site ID" + }, + "appId": { + "type": "string", + "description": "App ID" + }, + "bundleId": { + "type": "string", + "description": "Bundle ID" + }, + "storeUrl": { + "type": "string", + "description": "AppStore URL" + } + }, + "oneOf": [ + { "required": ["siteId"] }, + { "required": ["appId"] } + ], + "required": ["tagId", "publisherId"] +} \ No newline at end of file