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