github.com/prebid/prebid-server@v0.275.0/adapters/adapterstest/test_json.go (about) 1 package adapterstest 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io/fs" 7 "net/http" 8 "os" 9 "path/filepath" 10 "regexp" 11 "testing" 12 13 "github.com/mitchellh/copystructure" 14 "github.com/prebid/openrtb/v19/openrtb2" 15 "github.com/prebid/prebid-server/adapters" 16 "github.com/prebid/prebid-server/currency" 17 "github.com/prebid/prebid-server/openrtb_ext" 18 "github.com/stretchr/testify/assert" 19 "github.com/yudai/gojsondiff" 20 "github.com/yudai/gojsondiff/formatter" 21 ) 22 23 const jsonFileExtension string = ".json" 24 25 var supportedDirs = map[string]struct{}{ 26 "exemplary": {}, 27 "supplemental": {}, 28 "amp": {}, 29 "video": {}, 30 "videosupplemental": {}, 31 } 32 33 // RunJSONBidderTest is a helper method intended to unit test Bidders' adapters. 34 // It requires that: 35 // 36 // 1. Bidders communicate with external servers over HTTP. 37 // 2. The HTTP request bodies are legal JSON. 38 // 39 // Although the project does not require it, we _strongly_ recommend that all Bidders write tests using this. 40 // Doing so has the following benefits: 41 // 42 // 1. This includes some basic tests which confirm that your Bidder is "well-behaved" for all the input samples. 43 // For example, "no nil bids are allowed in the returned array". 44 // These tests are tedious to write, but help prevent bugs during auctions. 45 // 46 // 2. In the future, we plan to auto-generate documentation from the "exemplary" test files. 47 // Those docs will teach publishers how to use your Bidder, which should encourage adoption. 48 // 49 // To use this method, create *.json files in the following directories: 50 // 51 // adapters/{bidder}/{bidder}test/exemplary: 52 // 53 // These show "ideal" BidRequests for your Bidder. If possible, configure your servers to return the same 54 // expected responses forever. If your server responds appropriately, our future auto-generated documentation 55 // can guarantee Publishers that your adapter works as documented. 56 // 57 // adapters/{bidder}/{bidder}test/supplemental: 58 // 59 // Fill this with *.json files which are useful test cases, but are not appropriate for public example docs. 60 // For example, a file in this directory might make sure that a mobile-only Bidder returns errors on non-mobile requests. 61 // 62 // Then create a test in your adapters/{bidder}/{bidder}_test.go file like so: 63 // 64 // func TestJsonSamples(t *testing.T) { 65 // adapterstest.RunJSONBidderTest(t, "{bidder}test", instanceOfYourBidder) 66 // } 67 func RunJSONBidderTest(t *testing.T, rootDir string, bidder adapters.Bidder) { 68 err := filepath.WalkDir(rootDir, func(path string, info fs.DirEntry, err error) error { 69 if err != nil { 70 return err 71 } 72 73 isJsonFile := !info.IsDir() && filepath.Ext(info.Name()) == jsonFileExtension 74 RunSingleJSONBidderTest(t, bidder, path, isJsonFile) 75 return nil 76 }) 77 assert.NoError(t, err, "Error reading files from directory %s \n", rootDir) 78 } 79 80 func RunSingleJSONBidderTest(t *testing.T, bidder adapters.Bidder, path string, isJsonFile bool) { 81 base := filepath.Base(filepath.Dir(path)) 82 if _, ok := supportedDirs[base]; !ok { 83 return 84 } 85 86 allowErrors := base != "exemplary" && base != "video" 87 if isJsonFile { 88 specData, err := loadFile(path) 89 if err != nil { 90 t.Fatalf("Failed to load contents of file %s: %v", path, err) 91 } 92 93 if !allowErrors && specData.expectsErrors() { 94 t.Fatalf("Exemplary spec %s must not expect errors.", path) 95 } 96 97 runSpec(t, path, specData, bidder, base == "amp", base == "videosupplemental" || base == "video") 98 } 99 } 100 101 // LoadFile reads and parses a file as a test case. If something goes wrong, it returns an error. 102 func loadFile(filename string) (*testSpec, error) { 103 specData, err := os.ReadFile(filename) 104 if err != nil { 105 return nil, fmt.Errorf("Failed to read file %s: %v", filename, err) 106 } 107 108 var spec testSpec 109 if err := json.Unmarshal(specData, &spec); err != nil { 110 return nil, fmt.Errorf("Failed to unmarshal JSON from file: %v", err) 111 } 112 113 return &spec, nil 114 } 115 116 // runSpec runs a single test case. It will make sure: 117 // 118 // - That the Bidder does not return nil HTTP requests, bids, or errors inside their lists 119 // - That the Bidder's HTTP calls match the spec's expectations. 120 // - That the Bidder's Bids match the spec's expectations 121 // - That the Bidder's errors match the spec's expectations 122 // 123 // More assertions will almost certainly be added in the future, as bugs come up. 124 func runSpec(t *testing.T, filename string, spec *testSpec, bidder adapters.Bidder, isAmpTest, isVideoTest bool) { 125 reqInfo := getTestExtraRequestInfo(t, filename, spec, isAmpTest, isVideoTest) 126 requests := testMakeRequestsImpl(t, filename, spec, bidder, reqInfo) 127 128 testMakeBidsImpl(t, filename, spec, bidder, requests) 129 } 130 131 // getTestExtraRequestInfo builds the ExtraRequestInfo object that will be passed to testMakeRequestsImpl 132 func getTestExtraRequestInfo(t *testing.T, filename string, spec *testSpec, isAmpTest, isVideoTest bool) *adapters.ExtraRequestInfo { 133 t.Helper() 134 135 var reqInfo adapters.ExtraRequestInfo 136 137 // If test request.ext defines its own currency rates, add currency conversion to reqInfo 138 reqWrapper := &openrtb_ext.RequestWrapper{} 139 reqWrapper.BidRequest = &spec.BidRequest 140 141 reqExt, err := reqWrapper.GetRequestExt() 142 assert.NoError(t, err, "Could not unmarshall test request ext. %s", filename) 143 144 reqPrebid := reqExt.GetPrebid() 145 if reqPrebid != nil && reqPrebid.CurrencyConversions != nil && len(reqPrebid.CurrencyConversions.ConversionRates) > 0 { 146 err = currency.ValidateCustomRates(reqPrebid.CurrencyConversions) 147 assert.NoError(t, err, "Error validating currency rates in the test request: %s", filename) 148 149 // Get currency rates conversions from the test request.ext 150 conversions := currency.NewRates(reqPrebid.CurrencyConversions.ConversionRates) 151 152 // Create return adapters.ExtraRequestInfo object 153 reqInfo = adapters.NewExtraRequestInfo(conversions) 154 } else { 155 reqInfo = adapters.ExtraRequestInfo{} 156 } 157 158 // Set PbsEntryPoint if either isAmpTest or isVideoTest is true 159 if isAmpTest { 160 // simulates AMP entry point 161 reqInfo.PbsEntryPoint = "amp" 162 } else if isVideoTest { 163 reqInfo.PbsEntryPoint = "video" 164 } 165 166 return &reqInfo 167 } 168 169 type testSpec struct { 170 BidRequest openrtb2.BidRequest `json:"mockBidRequest"` 171 HttpCalls []httpCall `json:"httpCalls"` 172 BidResponses []expectedBidResponse `json:"expectedBidResponses"` 173 MakeRequestErrors []testSpecExpectedError `json:"expectedMakeRequestsErrors"` 174 MakeBidsErrors []testSpecExpectedError `json:"expectedMakeBidsErrors"` 175 } 176 177 type testSpecExpectedError struct { 178 Value string `json:"value"` 179 Comparison string `json:"comparison"` 180 } 181 182 func (spec *testSpec) expectsErrors() bool { 183 return len(spec.MakeRequestErrors) > 0 || len(spec.MakeBidsErrors) > 0 184 } 185 186 type httpCall struct { 187 Request httpRequest `json:"expectedRequest"` 188 Response httpResponse `json:"mockResponse"` 189 } 190 191 func (req *httpRequest) ToRequestData(t *testing.T) *adapters.RequestData { 192 return &adapters.RequestData{ 193 Method: "POST", 194 Uri: req.Uri, 195 Body: req.Body, 196 } 197 } 198 199 type httpRequest struct { 200 Body json.RawMessage `json:"body"` 201 Uri string `json:"uri"` 202 Headers http.Header `json:"headers"` 203 } 204 205 type httpResponse struct { 206 Status int `json:"status"` 207 Body json.RawMessage `json:"body"` 208 Headers http.Header `json:"headers"` 209 } 210 211 func (resp *httpResponse) ToResponseData(t *testing.T) *adapters.ResponseData { 212 return &adapters.ResponseData{ 213 StatusCode: resp.Status, 214 Body: resp.Body, 215 Headers: resp.Headers, 216 } 217 } 218 219 type expectedBidResponse struct { 220 Bids []expectedBid `json:"bids"` 221 Currency string `json:"currency"` 222 FledgeAuctionConfigs json.RawMessage `json:"fledgeauctionconfigs,omitempty"` 223 } 224 225 type expectedBid struct { 226 Bid json.RawMessage `json:"bid"` 227 Type string `json:"type"` 228 Seat string `json:"seat"` 229 } 230 231 // --------------------------------------- 232 // Lots of ugly, repetitive code below here. 233 // 234 // reflect.DeepEquals doesn't work because each OpenRTB field has an `ext []byte`, but we really care if those are JSON-equal 235 // 236 // Marshalling the structs and then using a JSON-diff library isn't great either, since 237 238 // assertMakeRequestsOutput compares the actual http requests to the expected ones. 239 func assertMakeRequestsOutput(t *testing.T, filename string, actual []*adapters.RequestData, expected []httpCall) { 240 t.Helper() 241 242 if len(expected) != len(actual) { 243 t.Fatalf("%s: MakeRequests had wrong request count. Expected %d, got %d", filename, len(expected), len(actual)) 244 } 245 246 for i := 0; i < len(expected); i++ { 247 var err error 248 for j := 0; j < len(actual); j++ { 249 if err = diffHttpRequests(fmt.Sprintf("%s: httpRequest[%d]", filename, i), actual[j], &(expected[i].Request)); err == nil { 250 break 251 } 252 } 253 assert.NoError(t, err, fmt.Sprintf("%s Expected RequestData was not returned by adapters' MakeRequests() implementation: httpRequest[%d]", filename, i)) 254 } 255 } 256 257 func assertErrorList(t *testing.T, description string, actual []error, expected []testSpecExpectedError) { 258 t.Helper() 259 260 if len(expected) != len(actual) { 261 t.Fatalf("%s had wrong error count. Expected %d, got %d (%v)", description, len(expected), len(actual), actual) 262 } 263 for i := 0; i < len(actual); i++ { 264 if expected[i].Comparison == "literal" { 265 if expected[i].Value != actual[i].Error() { 266 t.Errorf(`%s error[%d] had wrong message. Expected "%s", got "%s"`, description, i, expected[i].Value, actual[i].Error()) 267 } 268 } else if expected[i].Comparison == "regex" { 269 if matched, _ := regexp.MatchString(expected[i].Value, actual[i].Error()); !matched { 270 t.Errorf(`%s error[%d] had wrong message. Expected match with regex "%s", got "%s"`, description, i, expected[i].Value, actual[i].Error()) 271 } 272 } else { 273 t.Fatalf(`invalid comparison type "%s"`, expected[i].Comparison) 274 } 275 } 276 } 277 278 func assertMakeBidsOutput(t *testing.T, filename string, bidderResponse *adapters.BidderResponse, expected expectedBidResponse) { 279 t.Helper() 280 if !assert.Len(t, bidderResponse.Bids, len(expected.Bids), "%s: Wrong MakeBids bidderResponse.Bids count. len(bidderResponse.Bids) = %d vs len(spec.BidResponses.Bids) = %d", filename, len(bidderResponse.Bids), len(expected.Bids)) { 281 return 282 } 283 for i := 0; i < len(bidderResponse.Bids); i++ { 284 diffBids(t, fmt.Sprintf("%s: typedBid[%d]", filename, i), bidderResponse.Bids[i], &(expected.Bids[i])) 285 } 286 if expected.FledgeAuctionConfigs != nil { 287 assert.NotNilf(t, bidderResponse.FledgeAuctionConfigs, "%s: expected fledgeauctionconfigs in bidderResponse", filename) 288 fledgeAuctionConfigsJson, err := json.Marshal(bidderResponse.FledgeAuctionConfigs) 289 assert.NoErrorf(t, err, "%s: failed to marshal actual FledgeAuctionConfig response into JSON.", filename) 290 assert.JSONEqf(t, string(expected.FledgeAuctionConfigs), string(fledgeAuctionConfigsJson), "%s: incorrect fledgeauctionconfig", filename) 291 } else { 292 assert.Nilf(t, bidderResponse.FledgeAuctionConfigs, "%s: unexpected fledgeauctionconfigs in bidderResponse", filename) 293 } 294 } 295 296 // diffHttpRequests compares the actual HTTP request data to the expected one. 297 // It assumes that the request bodies are JSON 298 func diffHttpRequests(description string, actual *adapters.RequestData, expected *httpRequest) error { 299 300 if actual == nil { 301 return fmt.Errorf("Bidders cannot return nil HTTP calls. %s was nil.", description) 302 } 303 304 if expected.Uri != actual.Uri { 305 return fmt.Errorf(`%s.uri "%s" does not match expected "%s."`, description, actual.Uri, expected.Uri) 306 } 307 308 if expected.Headers != nil { 309 actualHeader, err := json.Marshal(actual.Headers) 310 if err != nil { 311 return fmt.Errorf(`%s actual.Headers could not be marshalled. Error: %s"`, description, err.Error()) 312 } 313 expectedHeader, err := json.Marshal(expected.Headers) 314 if err != nil { 315 return fmt.Errorf(`%s expected.Headers could not be marshalled. Error: %s"`, description, err.Error()) 316 } 317 if err := diffJson(description, actualHeader, expectedHeader); err != nil { 318 return err 319 } 320 } 321 return diffJson(description, actual.Body, expected.Body) 322 } 323 324 func diffBids(t *testing.T, description string, actual *adapters.TypedBid, expected *expectedBid) { 325 if actual == nil { 326 t.Errorf("Bidders cannot return nil TypedBids. %s was nil.", description) 327 return 328 } 329 330 assert.Equal(t, string(expected.Seat), string(actual.Seat), fmt.Sprintf(`%s.seat "%s" does not match expected "%s."`, description, string(actual.Seat), string(expected.Seat))) 331 assert.Equal(t, string(expected.Type), string(actual.BidType), fmt.Sprintf(`%s.type "%s" does not match expected "%s."`, description, string(actual.BidType), string(expected.Type))) 332 assert.NoError(t, diffOrtbBids(fmt.Sprintf("%s.bid", description), actual.Bid, expected.Bid)) 333 } 334 335 // diffOrtbBids compares the actual Bid made by the adapter to the expectation from the JSON file. 336 func diffOrtbBids(description string, actual *openrtb2.Bid, expected json.RawMessage) error { 337 if actual == nil { 338 return fmt.Errorf("Bidders cannot return nil Bids. %s was nil.", description) 339 } 340 341 actualJson, err := json.Marshal(actual) 342 if err != nil { 343 return fmt.Errorf("%s failed to marshal actual Bid into JSON. %v", description, err) 344 } 345 346 return diffJson(description, actualJson, expected) 347 } 348 349 // diffJson compares two JSON byte arrays for structural equality. It will produce an error if either 350 // byte array is not actually JSON. 351 func diffJson(description string, actual []byte, expected []byte) error { 352 if len(actual) == 0 && len(expected) == 0 { 353 return nil 354 } 355 if len(actual) == 0 || len(expected) == 0 { 356 return fmt.Errorf("%s json diff failed. Expected %d bytes in body, but got %d.", description, len(expected), len(actual)) 357 } 358 diff, err := gojsondiff.New().Compare(actual, expected) 359 if err != nil { 360 return fmt.Errorf("%s json diff failed. %v", description, err) 361 } 362 363 if diff.Modified() { 364 var left interface{} 365 if err := json.Unmarshal(actual, &left); err != nil { 366 return fmt.Errorf("%s json did not match, but unmarshalling failed. %v", description, err) 367 } 368 printer := formatter.NewAsciiFormatter(left, formatter.AsciiFormatterConfig{ 369 ShowArrayIndex: true, 370 }) 371 output, err := printer.Format(diff) 372 if err != nil { 373 return fmt.Errorf("%s did not match, but diff formatting failed. %v", description, err) 374 } else { 375 return fmt.Errorf("%s json did not match expected.\n\n%s", description, output) 376 } 377 } 378 return nil 379 } 380 381 // testMakeRequestsImpl asserts the resulting values of the bidder's `MakeRequests()` implementation 382 // against the expected JSON-defined results and ensures we do not encounter data races in the process. 383 // To assert no data races happen we make use of: 384 // 1. A shallow copy of the unmarshalled openrtb2.BidRequest that will provide reference values to 385 // shared memory that we don't want the adapters' implementation of `MakeRequests()` to modify. 386 // 2. A deep copy that will preserve the original values of all the fields. This copy remains untouched 387 // by the adapters' processes and serves as reference of what the shared memory values should still 388 // be after the `MakeRequests()` call. 389 func testMakeRequestsImpl(t *testing.T, filename string, spec *testSpec, bidder adapters.Bidder, reqInfo *adapters.ExtraRequestInfo) []*adapters.RequestData { 390 t.Helper() 391 392 deepBidReqCopy, shallowBidReqCopy, err := getDataRaceTestCopies(&spec.BidRequest) 393 assert.NoError(t, err, "Could not create request copies. %s", filename) 394 395 // Run MakeRequests 396 requests, errs := bidder.MakeRequests(&spec.BidRequest, reqInfo) 397 398 // Compare MakeRequests actual output versus expected values found in JSON file 399 assertErrorList(t, fmt.Sprintf("%s: MakeRequests", filename), errs, spec.MakeRequestErrors) 400 assertMakeRequestsOutput(t, filename, requests, spec.HttpCalls) 401 402 // Assert no data races occur using original bidRequest copies of references and values 403 assert.Equal(t, deepBidReqCopy, shallowBidReqCopy, "Data race found. Test: %s", filename) 404 405 return requests 406 } 407 408 // getDataRaceTestCopies returns a deep copy and a shallow copy of the original bidRequest that will get 409 // compared to verify no data races occur. 410 func getDataRaceTestCopies(original *openrtb2.BidRequest) (*openrtb2.BidRequest, *openrtb2.BidRequest, error) { 411 cpy, err := copystructure.Copy(original) 412 if err != nil { 413 return nil, nil, err 414 } 415 deepReqCopy := cpy.(*openrtb2.BidRequest) 416 417 shallowReqCopy := *original 418 419 // Prebid Server core makes shallow copies of imp elements and adapters are allowed to make changes 420 // to them. Therefore, we need shallow copies of Imp elements here so our test replicates that 421 // functionality and only fail when actual shared momory gets modified. 422 if original.Imp != nil { 423 shallowReqCopy.Imp = make([]openrtb2.Imp, len(original.Imp)) 424 copy(shallowReqCopy.Imp, original.Imp) 425 } 426 427 return deepReqCopy, &shallowReqCopy, nil 428 } 429 430 // testMakeBidsImpl asserts the results of the bidder MakeBids implementation against the expected JSON-defined results 431 func testMakeBidsImpl(t *testing.T, filename string, spec *testSpec, bidder adapters.Bidder, makeRequestsOut []*adapters.RequestData) { 432 t.Helper() 433 434 bidResponses := make([]*adapters.BidderResponse, 0) 435 var bidsErrs = make([]error, 0, len(spec.MakeBidsErrors)) 436 437 // We should have as many bids as number of adapters.RequestData found in MakeRequests output 438 for i := 0; i < len(makeRequestsOut); i++ { 439 // Run MakeBids with JSON refined spec.HttpCalls info that was asserted to match MakeRequests 440 // output inside testMakeRequestsImpl 441 thisBidResponse, theseErrs := bidder.MakeBids(&spec.BidRequest, spec.HttpCalls[i].Request.ToRequestData(t), spec.HttpCalls[i].Response.ToResponseData(t)) 442 443 if theseErrs != nil { 444 bidsErrs = append(bidsErrs, theseErrs...) 445 } 446 if thisBidResponse != nil { 447 bidResponses = append(bidResponses, thisBidResponse) 448 } 449 } 450 451 // Assert actual errors thrown by MakeBids implementation versus expected JSON-defined spec.MakeBidsErrors 452 assertErrorList(t, fmt.Sprintf("%s: MakeBids", filename), bidsErrs, spec.MakeBidsErrors) 453 454 // Assert MakeBids implementation BidResponses with expected JSON-defined spec.BidResponses[i].Bids 455 if assert.Len(t, bidResponses, len(spec.BidResponses), "%s: MakeBids len(bidResponses) = %d vs len(spec.BidResponses) = %d", filename, len(bidResponses), len(spec.BidResponses)) { 456 for i := 0; i < len(spec.BidResponses); i++ { 457 assertMakeBidsOutput(t, filename, bidResponses[i], spec.BidResponses[i]) 458 } 459 } 460 }