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  }