github.com/prebid/prebid-server/v2@v2.18.0/usersync/syncer_test.go (about)

     1  package usersync
     2  
     3  import (
     4  	"testing"
     5  	"text/template"
     6  
     7  	"github.com/prebid/prebid-server/v2/config"
     8  	"github.com/prebid/prebid-server/v2/macros"
     9  	"github.com/stretchr/testify/assert"
    10  )
    11  
    12  func TestNewSyncer(t *testing.T) {
    13  	var (
    14  		supportCORS      = true
    15  		hostConfig       = config.UserSync{ExternalURL: "http://host.com", RedirectURL: "{{.ExternalURL}}/host"}
    16  		macroValues      = macros.UserSyncPrivacy{GDPR: "A", GDPRConsent: "B", USPrivacy: "C"}
    17  		iframeConfig     = &config.SyncerEndpoint{URL: "https://bidder.com/iframe?redirect={{.RedirectURL}}"}
    18  		redirectConfig   = &config.SyncerEndpoint{URL: "https://bidder.com/redirect?redirect={{.RedirectURL}}"}
    19  		errParseConfig   = &config.SyncerEndpoint{URL: "{{malformed}}"}
    20  		errInvalidConfig = &config.SyncerEndpoint{URL: "notAURL:{{.RedirectURL}}"}
    21  	)
    22  
    23  	testCases := []struct {
    24  		description         string
    25  		givenKey            string
    26  		givenBidderName     string
    27  		givenIFrameConfig   *config.SyncerEndpoint
    28  		givenRedirectConfig *config.SyncerEndpoint
    29  		givenExternalURL    string
    30  		givenForceType      string
    31  		expectedError       string
    32  		expectedDefault     SyncType
    33  		expectedIFrame      string
    34  		expectedRedirect    string
    35  	}{
    36  		{
    37  			description:         "Missing Key",
    38  			givenKey:            "",
    39  			givenBidderName:     "",
    40  			givenIFrameConfig:   iframeConfig,
    41  			givenRedirectConfig: nil,
    42  			expectedError:       "key is required",
    43  		},
    44  		{
    45  			description:         "Missing Endpoints",
    46  			givenKey:            "a",
    47  			givenBidderName:     "bidderA",
    48  			givenIFrameConfig:   nil,
    49  			givenRedirectConfig: nil,
    50  			expectedError:       "at least one endpoint (iframe and/or redirect) is required",
    51  		},
    52  		{
    53  			description:         "IFrame & Redirect Endpoints",
    54  			givenKey:            "a",
    55  			givenBidderName:     "bidderA",
    56  			givenIFrameConfig:   iframeConfig,
    57  			givenRedirectConfig: redirectConfig,
    58  			expectedDefault:     SyncTypeIFrame,
    59  			expectedIFrame:      "https://bidder.com/iframe?redirect=http%3A%2F%2Fhost.com%2Fhost",
    60  			expectedRedirect:    "https://bidder.com/redirect?redirect=http%3A%2F%2Fhost.com%2Fhost",
    61  		},
    62  		{
    63  			description:         "IFrame - Parse Error",
    64  			givenKey:            "a",
    65  			givenBidderName:     "bidderA",
    66  			givenIFrameConfig:   errParseConfig,
    67  			givenRedirectConfig: nil,
    68  			expectedError:       "iframe template: biddera_usersync_url:1: function \"malformed\" not defined",
    69  		},
    70  		{
    71  			description:         "IFrame - Validation Error",
    72  			givenKey:            "a",
    73  			givenBidderName:     "bidderA",
    74  			givenIFrameConfig:   errInvalidConfig,
    75  			givenRedirectConfig: nil,
    76  			expectedError:       "iframe composed url: \"notAURL:http%3A%2F%2Fhost.com%2Fhost\" is invalid",
    77  		},
    78  		{
    79  			description:         "Redirect - Parse Error",
    80  			givenKey:            "a",
    81  			givenBidderName:     "bidderA",
    82  			givenIFrameConfig:   nil,
    83  			givenRedirectConfig: errParseConfig,
    84  			expectedError:       "redirect template: biddera_usersync_url:1: function \"malformed\" not defined",
    85  		},
    86  		{
    87  			description:         "Redirect - Validation Error",
    88  			givenKey:            "a",
    89  			givenBidderName:     "bidderA",
    90  			givenIFrameConfig:   nil,
    91  			givenRedirectConfig: errInvalidConfig,
    92  			expectedError:       "redirect composed url: \"notAURL:http%3A%2F%2Fhost.com%2Fhost\" is invalid",
    93  		},
    94  		{
    95  			description:         "Syncer Level External URL",
    96  			givenKey:            "a",
    97  			givenBidderName:     "bidderA",
    98  			givenExternalURL:    "http://syncer.com",
    99  			givenIFrameConfig:   iframeConfig,
   100  			givenRedirectConfig: redirectConfig,
   101  			expectedDefault:     SyncTypeIFrame,
   102  			expectedIFrame:      "https://bidder.com/iframe?redirect=http%3A%2F%2Fsyncer.com%2Fhost",
   103  			expectedRedirect:    "https://bidder.com/redirect?redirect=http%3A%2F%2Fsyncer.com%2Fhost",
   104  		},
   105  	}
   106  
   107  	for _, test := range testCases {
   108  		syncerConfig := config.Syncer{
   109  			Key:         test.givenKey,
   110  			SupportCORS: &supportCORS,
   111  			IFrame:      test.givenIFrameConfig,
   112  			Redirect:    test.givenRedirectConfig,
   113  			ExternalURL: test.givenExternalURL,
   114  		}
   115  
   116  		result, err := NewSyncer(hostConfig, syncerConfig, test.givenBidderName)
   117  
   118  		if test.expectedError == "" {
   119  			assert.NoError(t, err, test.description+":err")
   120  			if assert.IsType(t, standardSyncer{}, result, test.description+":result_type") {
   121  				result := result.(standardSyncer)
   122  				assert.Equal(t, test.givenKey, result.key, test.description+":key")
   123  				assert.Equal(t, supportCORS, result.supportCORS, test.description+":cors")
   124  				assert.Equal(t, test.expectedDefault, result.defaultSyncType, test.description+":default_sync")
   125  
   126  				if test.expectedIFrame == "" {
   127  					assert.Nil(t, result.iframe, test.description+":iframe")
   128  				} else {
   129  					iframeRendered, err := macros.ResolveMacros(result.iframe, macroValues)
   130  					if assert.NoError(t, err, test.description+":iframe_render") {
   131  						assert.Equal(t, test.expectedIFrame, iframeRendered, test.description+":iframe")
   132  					}
   133  				}
   134  
   135  				if test.expectedRedirect == "" {
   136  					assert.Nil(t, result.redirect, test.description+":redirect")
   137  				} else {
   138  					redirectRendered, err := macros.ResolveMacros(result.redirect, macroValues)
   139  					if assert.NoError(t, err, test.description+":redirect_render") {
   140  						assert.Equal(t, test.expectedRedirect, redirectRendered, test.description+":redirect")
   141  					}
   142  				}
   143  			}
   144  		} else {
   145  			assert.EqualError(t, err, test.expectedError, test.description+":err")
   146  			assert.Empty(t, result)
   147  		}
   148  	}
   149  }
   150  
   151  func TestResolveDefaultSyncType(t *testing.T) {
   152  	anyEndpoint := &config.SyncerEndpoint{}
   153  
   154  	testCases := []struct {
   155  		description      string
   156  		givenConfig      config.Syncer
   157  		expectedSyncType SyncType
   158  	}{
   159  		{
   160  			description:      "IFrame & Redirect",
   161  			givenConfig:      config.Syncer{IFrame: anyEndpoint, Redirect: anyEndpoint},
   162  			expectedSyncType: SyncTypeIFrame,
   163  		},
   164  		{
   165  			description:      "IFrame Only",
   166  			givenConfig:      config.Syncer{IFrame: anyEndpoint, Redirect: nil},
   167  			expectedSyncType: SyncTypeIFrame,
   168  		},
   169  		{
   170  			description:      "Redirect Only - Redirect Default",
   171  			givenConfig:      config.Syncer{IFrame: nil, Redirect: anyEndpoint},
   172  			expectedSyncType: SyncTypeRedirect,
   173  		},
   174  	}
   175  
   176  	for _, test := range testCases {
   177  		result := resolveDefaultSyncType(test.givenConfig)
   178  		assert.Equal(t, test.expectedSyncType, result, test.description+":result")
   179  	}
   180  }
   181  
   182  func TestBuildTemplate(t *testing.T) {
   183  	var (
   184  		key           = "anyKey"
   185  		syncTypeValue = "x"
   186  		hostConfig    = config.UserSync{ExternalURL: "http://host.com", RedirectURL: "{{.ExternalURL}}/host"}
   187  		macroValues   = macros.UserSyncPrivacy{GDPR: "A", GDPRConsent: "B", USPrivacy: "C", GPP: "D", GPPSID: "1"}
   188  	)
   189  
   190  	testCases := []struct {
   191  		description            string
   192  		givenHostConfig        config.UserSync
   193  		givenSyncerExternalURL string
   194  		givenSyncerEndpoint    config.SyncerEndpoint
   195  		expectedError          string
   196  		expectedRendered       string
   197  	}{
   198  		{
   199  			description: "No Composed Macros",
   200  			givenSyncerEndpoint: config.SyncerEndpoint{
   201  				URL: "hasNoComposedMacros,gdpr={{.GDPR}}",
   202  			},
   203  			expectedRendered: "hasNoComposedMacros,gdpr=A",
   204  		},
   205  		{
   206  			description: "All Composed Macros - SyncerKey",
   207  			givenSyncerEndpoint: config.SyncerEndpoint{
   208  				URL:         "https://bidder.com/sync?redirect={{.RedirectURL}}",
   209  				RedirectURL: "{{.ExternalURL}}/setuid?bidder={{.SyncerKey}}&f={{.SyncType}}&gdpr={{.GDPR}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&uid={{.UserMacro}}",
   210  				ExternalURL: "http://syncer.com",
   211  				UserMacro:   "$UID$",
   212  			},
   213  			expectedRendered: "https://bidder.com/sync?redirect=http%3A%2F%2Fsyncer.com%2Fsetuid%3Fbidder%3DanyKey%26f%3Dx%26gdpr%3DA%26gpp%3DD%26gpp_sid%3D1%26uid%3D%24UID%24",
   214  		},
   215  		{
   216  			description: "All Composed Macros - BidderName",
   217  			givenSyncerEndpoint: config.SyncerEndpoint{
   218  				URL:         "https://bidder.com/sync?redirect={{.RedirectURL}}",
   219  				RedirectURL: "{{.ExternalURL}}/setuid?bidder={{.BidderName}}&f={{.SyncType}}&gdpr={{.GDPR}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&uid={{.UserMacro}}",
   220  				ExternalURL: "http://syncer.com",
   221  				UserMacro:   "$UID$",
   222  			},
   223  			expectedRendered: "https://bidder.com/sync?redirect=http%3A%2F%2Fsyncer.com%2Fsetuid%3Fbidder%3DanyKey%26f%3Dx%26gdpr%3DA%26gpp%3DD%26gpp_sid%3D1%26uid%3D%24UID%24",
   224  		},
   225  		{
   226  			description: "Redirect URL + External URL From Host",
   227  			givenSyncerEndpoint: config.SyncerEndpoint{
   228  				URL: "https://bidder.com/sync?redirect={{.RedirectURL}}",
   229  			},
   230  			expectedRendered: "https://bidder.com/sync?redirect=http%3A%2F%2Fhost.com%2Fhost",
   231  		},
   232  		{
   233  			description: "Redirect URL From Syncer",
   234  			givenSyncerEndpoint: config.SyncerEndpoint{
   235  				URL:         "https://bidder.com/sync?redirect={{.RedirectURL}}",
   236  				RedirectURL: "{{.ExternalURL}}/syncer",
   237  			},
   238  			expectedRendered: "https://bidder.com/sync?redirect=http%3A%2F%2Fhost.com%2Fsyncer",
   239  		},
   240  		{
   241  			description: "External URL From Host",
   242  			givenSyncerEndpoint: config.SyncerEndpoint{
   243  				URL:         "https://bidder.com/sync?redirect={{.RedirectURL}}",
   244  				ExternalURL: "http://syncer.com",
   245  			},
   246  			expectedRendered: "https://bidder.com/sync?redirect=http%3A%2F%2Fsyncer.com%2Fhost",
   247  		},
   248  		{
   249  			description:            "External URL From Syncer Config",
   250  			givenSyncerExternalURL: "http://syncershared.com",
   251  			givenSyncerEndpoint: config.SyncerEndpoint{
   252  				URL: "https://bidder.com/sync?redirect={{.RedirectURL}}",
   253  			},
   254  			expectedRendered: "https://bidder.com/sync?redirect=http%3A%2F%2Fsyncershared.com%2Fhost",
   255  		},
   256  		{
   257  			description:            "External URL From Syncer Config (Most Specific Wins)",
   258  			givenSyncerExternalURL: "http://syncershared.com",
   259  			givenSyncerEndpoint: config.SyncerEndpoint{
   260  				URL:         "https://bidder.com/sync?redirect={{.RedirectURL}}",
   261  				ExternalURL: "http://syncer.com",
   262  			},
   263  			expectedRendered: "https://bidder.com/sync?redirect=http%3A%2F%2Fsyncer.com%2Fhost",
   264  		},
   265  		{
   266  			description: "Template Parse Error",
   267  			givenSyncerEndpoint: config.SyncerEndpoint{
   268  				URL: "{{malformed}}",
   269  			},
   270  			expectedError: "template: anykey_usersync_url:1: function \"malformed\" not defined",
   271  		},
   272  		{
   273  			description: "User Macro Is Go Template Macro-Like",
   274  			givenSyncerEndpoint: config.SyncerEndpoint{
   275  				URL:         "https://bidder.com/sync?redirect={{.RedirectURL}}",
   276  				RedirectURL: "{{.ExternalURL}}/setuid?bidder={{.SyncerKey}}&f={{.SyncType}}&gdpr={{.GDPR}}&uid={{.UserMacro}}",
   277  				UserMacro:   "{{UID}}",
   278  			},
   279  			expectedRendered: "https://bidder.com/sync?redirect=http%3A%2F%2Fhost.com%2Fsetuid%3Fbidder%3DanyKey%26f%3Dx%26gdpr%3DA%26uid%3D%7B%7BUID%7D%7D",
   280  		},
   281  
   282  		// The following tests protect against the "\"." literal character vs the "." character class in regex. Literal
   283  		// value which use {{ }} but do not match Go's naming pattern of {{ .Name }} are escaped.
   284  		{
   285  			description: "Invalid Macro - Redirect URL",
   286  			givenSyncerEndpoint: config.SyncerEndpoint{
   287  				URL: "https://bidder.com/sync?redirect={{xRedirectURL}}",
   288  			},
   289  			expectedError: "template: anykey_usersync_url:1: function \"xRedirectURL\" not defined",
   290  		},
   291  		{
   292  			description: "Macro-Like Literal Value - External URL",
   293  			givenSyncerEndpoint: config.SyncerEndpoint{
   294  				URL:         "https://bidder.com/sync?redirect={{.RedirectURL}}",
   295  				RedirectURL: "{{xExternalURL}}",
   296  			},
   297  			expectedRendered: "https://bidder.com/sync?redirect=%7B%7BxExternalURL%7D%7D",
   298  		},
   299  		{
   300  			description: "Macro-Like Literal Value - Syncer Key",
   301  			givenSyncerEndpoint: config.SyncerEndpoint{
   302  				URL:         "https://bidder.com/sync?redirect={{.RedirectURL}}",
   303  				RedirectURL: "{{xSyncerKey}}",
   304  			},
   305  			expectedRendered: "https://bidder.com/sync?redirect=%7B%7BxSyncerKey%7D%7D",
   306  		},
   307  		{
   308  			description: "Macro-Like Literal Value - Sync Type",
   309  			givenSyncerEndpoint: config.SyncerEndpoint{
   310  				URL:         "https://bidder.com/sync?redirect={{.RedirectURL}}",
   311  				RedirectURL: "{{xSyncType}}",
   312  			},
   313  			expectedRendered: "https://bidder.com/sync?redirect=%7B%7BxSyncType%7D%7D",
   314  		},
   315  		{
   316  			description: "Macro-Like Literal Value - User Macro",
   317  			givenSyncerEndpoint: config.SyncerEndpoint{
   318  				URL:         "https://bidder.com/sync?redirect={{.RedirectURL}}",
   319  				RedirectURL: "{{xUserMacro}}",
   320  			},
   321  			expectedRendered: "https://bidder.com/sync?redirect=%7B%7BxUserMacro%7D%7D",
   322  		},
   323  	}
   324  
   325  	for _, test := range testCases {
   326  		result, err := buildTemplate(key, syncTypeValue, hostConfig, test.givenSyncerExternalURL, test.givenSyncerEndpoint, "")
   327  
   328  		if test.expectedError == "" {
   329  			assert.NoError(t, err, test.description+":err")
   330  			resultRendered, err := macros.ResolveMacros(result, macroValues)
   331  			if assert.NoError(t, err, test.description+":template_render") {
   332  				assert.Equal(t, test.expectedRendered, resultRendered, test.description+":template")
   333  			}
   334  		} else {
   335  			assert.EqualError(t, err, test.expectedError, test.description+":err")
   336  		}
   337  	}
   338  }
   339  
   340  func TestChooseExternalURL(t *testing.T) {
   341  	testCases := []struct {
   342  		description            string
   343  		givenSyncerEndpointURL string
   344  		givenSyncerURL         string
   345  		givenHostConfigURL     string
   346  		expected               string
   347  	}{
   348  		{
   349  			description:            "Syncer Endpoint Chosen",
   350  			givenSyncerEndpointURL: "a",
   351  			givenSyncerURL:         "b",
   352  			givenHostConfigURL:     "c",
   353  			expected:               "a",
   354  		},
   355  		{
   356  			description:            "Syncer Chosen",
   357  			givenSyncerEndpointURL: "",
   358  			givenSyncerURL:         "b",
   359  			givenHostConfigURL:     "c",
   360  			expected:               "b",
   361  		},
   362  		{
   363  			description:            "Host Config Chosen",
   364  			givenSyncerEndpointURL: "",
   365  			givenSyncerURL:         "",
   366  			givenHostConfigURL:     "c",
   367  			expected:               "c",
   368  		},
   369  		{
   370  			description:            "All Empty",
   371  			givenSyncerEndpointURL: "",
   372  			givenSyncerURL:         "",
   373  			givenHostConfigURL:     "",
   374  			expected:               "",
   375  		},
   376  	}
   377  
   378  	for _, test := range testCases {
   379  		result := chooseExternalURL(test.givenSyncerEndpointURL, test.givenSyncerURL, test.givenHostConfigURL)
   380  		assert.Equal(t, test.expected, result, test.description)
   381  	}
   382  }
   383  
   384  func TestEscapeTemplate(t *testing.T) {
   385  	testCases := []struct {
   386  		description string
   387  		given       string
   388  		expected    string
   389  	}{
   390  		{
   391  			description: "Macro Only",
   392  			given:       "{{.Macro}}",
   393  			expected:    "{{.Macro}}",
   394  		},
   395  		{
   396  			description: "Text Only",
   397  			given:       "/a",
   398  			expected:    "%2Fa",
   399  		},
   400  		{
   401  			description: "Start Only",
   402  			given:       "&a{{.Macro1}}",
   403  			expected:    "%26a{{.Macro1}}",
   404  		},
   405  		{
   406  			description: "Middle Only",
   407  			given:       "{{.Macro1}}&a{{.Macro2}}",
   408  			expected:    "{{.Macro1}}%26a{{.Macro2}}",
   409  		},
   410  		{
   411  			description: "End Only",
   412  			given:       "{{.Macro1}}&a",
   413  			expected:    "{{.Macro1}}%26a",
   414  		},
   415  		{
   416  			description: "Start / Middle / End",
   417  			given:       "&a{{.Macro1}}/b{{.Macro2}}&c",
   418  			expected:    "%26a{{.Macro1}}%2Fb{{.Macro2}}%26c",
   419  		},
   420  		{
   421  			description: "Characters In Macro Not Escaped",
   422  			given:       "{{.Macro&}}",
   423  			expected:    "{{.Macro&}}",
   424  		},
   425  		{
   426  			description: "Macro Whitespace Insensitive",
   427  			given:       " &a {{ .Macro1  }} /b ",
   428  			expected:    "+%26a+{{ .Macro1  }}+%2Fb+",
   429  		},
   430  		{
   431  			description: "Double Curly Braces, But Not Macro",
   432  			given:       "{{Macro}}",
   433  			expected:    "%7B%7BMacro%7D%7D",
   434  		},
   435  	}
   436  
   437  	for _, test := range testCases {
   438  		result := escapeTemplate(test.given)
   439  		assert.Equal(t, test.expected, result, test.description)
   440  	}
   441  }
   442  
   443  func TestValidateTemplate(t *testing.T) {
   444  	testCases := []struct {
   445  		description   string
   446  		given         *template.Template
   447  		expectedError string
   448  	}{
   449  		{
   450  			description:   "Contains Unrecognized Macro",
   451  			given:         template.Must(template.New("test").Parse("invalid:{{.DoesNotExist}}")),
   452  			expectedError: "template: test:1:10: executing \"test\" at <.DoesNotExist>: can't evaluate field DoesNotExist in type macros.UserSyncPrivacy",
   453  		},
   454  		{
   455  			description:   "Not A Url",
   456  			given:         template.Must(template.New("test").Parse("not-a-url,gdpr:{{.GDPR}},gdprconsent:{{.GDPRConsent}},ccpa:{{.USPrivacy}}")),
   457  			expectedError: "composed url: \"not-a-url,gdpr:anyGDPR,gdprconsent:anyGDPRConsent,ccpa:anyCCPAConsent\" is invalid",
   458  		},
   459  		{
   460  			description:   "Valid",
   461  			given:         template.Must(template.New("test").Parse("http://server.com/sync?gdpr={{.GDPR}}&gdprconsent={{.GDPRConsent}}&ccpa={{.USPrivacy}}")),
   462  			expectedError: "",
   463  		},
   464  	}
   465  
   466  	for _, test := range testCases {
   467  		err := validateTemplate(test.given)
   468  
   469  		if test.expectedError == "" {
   470  			assert.NoError(t, err, test.description)
   471  		} else {
   472  			assert.EqualError(t, err, test.expectedError, test.description)
   473  		}
   474  	}
   475  }
   476  
   477  func TestSyncerKey(t *testing.T) {
   478  	syncer := standardSyncer{key: "a"}
   479  	assert.Equal(t, "a", syncer.Key())
   480  }
   481  
   482  func TestSyncerDefaultSyncType(t *testing.T) {
   483  	syncer := standardSyncer{defaultSyncType: SyncTypeRedirect}
   484  	assert.Equal(t, SyncTypeRedirect, syncer.DefaultResponseFormat())
   485  }
   486  
   487  func TestSyncerDefaultResponseFormat(t *testing.T) {
   488  	testCases := []struct {
   489  		description      string
   490  		givenSyncer      standardSyncer
   491  		expectedSyncType SyncType
   492  	}{
   493  		{
   494  			description:      "IFrame",
   495  			givenSyncer:      standardSyncer{formatOverride: config.SyncResponseFormatIFrame},
   496  			expectedSyncType: SyncTypeIFrame,
   497  		},
   498  		{
   499  			description:      "Default with Redirect Override",
   500  			givenSyncer:      standardSyncer{defaultSyncType: SyncTypeIFrame, formatOverride: config.SyncResponseFormatRedirect},
   501  			expectedSyncType: SyncTypeRedirect,
   502  		},
   503  		{
   504  			description:      "Default with no override",
   505  			givenSyncer:      standardSyncer{defaultSyncType: SyncTypeRedirect},
   506  			expectedSyncType: SyncTypeRedirect,
   507  		},
   508  	}
   509  
   510  	for _, test := range testCases {
   511  		syncType := test.givenSyncer.DefaultResponseFormat()
   512  		assert.Equal(t, test.expectedSyncType, syncType, test.description)
   513  	}
   514  }
   515  
   516  func TestSyncerSupportsType(t *testing.T) {
   517  	endpointTemplate := template.Must(template.New("test").Parse("iframe"))
   518  
   519  	testCases := []struct {
   520  		description           string
   521  		givenSyncTypes        []SyncType
   522  		givenIFrameTemplate   *template.Template
   523  		givenRedirectTemplate *template.Template
   524  		expectedResult        bool
   525  	}{
   526  		{
   527  			description:           "All Available - None",
   528  			givenSyncTypes:        []SyncType{},
   529  			givenIFrameTemplate:   endpointTemplate,
   530  			givenRedirectTemplate: endpointTemplate,
   531  			expectedResult:        false,
   532  		},
   533  		{
   534  			description:           "All Available - One",
   535  			givenSyncTypes:        []SyncType{SyncTypeIFrame},
   536  			givenIFrameTemplate:   endpointTemplate,
   537  			givenRedirectTemplate: endpointTemplate,
   538  			expectedResult:        true,
   539  		},
   540  		{
   541  			description:           "All Available - Many",
   542  			givenSyncTypes:        []SyncType{SyncTypeIFrame, SyncTypeRedirect},
   543  			givenIFrameTemplate:   endpointTemplate,
   544  			givenRedirectTemplate: endpointTemplate,
   545  			expectedResult:        true,
   546  		},
   547  		{
   548  			description:           "One Available - None",
   549  			givenSyncTypes:        []SyncType{},
   550  			givenIFrameTemplate:   endpointTemplate,
   551  			givenRedirectTemplate: nil,
   552  			expectedResult:        false,
   553  		},
   554  		{
   555  			description:           "One Available - One - Supported",
   556  			givenSyncTypes:        []SyncType{SyncTypeIFrame},
   557  			givenIFrameTemplate:   endpointTemplate,
   558  			givenRedirectTemplate: nil,
   559  			expectedResult:        true,
   560  		},
   561  		{
   562  			description:           "One Available - One - Not Supported",
   563  			givenSyncTypes:        []SyncType{SyncTypeRedirect},
   564  			givenIFrameTemplate:   endpointTemplate,
   565  			givenRedirectTemplate: nil,
   566  			expectedResult:        false,
   567  		},
   568  		{
   569  			description:           "One Available - Many",
   570  			givenSyncTypes:        []SyncType{SyncTypeIFrame, SyncTypeRedirect},
   571  			givenIFrameTemplate:   endpointTemplate,
   572  			givenRedirectTemplate: nil,
   573  			expectedResult:        true,
   574  		},
   575  	}
   576  
   577  	for _, test := range testCases {
   578  		syncer := standardSyncer{
   579  			iframe:   test.givenIFrameTemplate,
   580  			redirect: test.givenRedirectTemplate,
   581  		}
   582  		result := syncer.SupportsType(test.givenSyncTypes)
   583  		assert.Equal(t, test.expectedResult, result, test.description)
   584  	}
   585  }
   586  
   587  func TestSyncerFilterSupportedSyncTypes(t *testing.T) {
   588  	endpointTemplate := template.Must(template.New("test").Parse("iframe"))
   589  
   590  	testCases := []struct {
   591  		description           string
   592  		givenSyncTypes        []SyncType
   593  		givenIFrameTemplate   *template.Template
   594  		givenRedirectTemplate *template.Template
   595  		expectedSyncTypes     []SyncType
   596  	}{
   597  		{
   598  			description:           "All Available - None",
   599  			givenSyncTypes:        []SyncType{},
   600  			givenIFrameTemplate:   endpointTemplate,
   601  			givenRedirectTemplate: endpointTemplate,
   602  			expectedSyncTypes:     []SyncType{},
   603  		},
   604  		{
   605  			description:           "All Available - One",
   606  			givenSyncTypes:        []SyncType{SyncTypeIFrame},
   607  			givenIFrameTemplate:   endpointTemplate,
   608  			givenRedirectTemplate: endpointTemplate,
   609  			expectedSyncTypes:     []SyncType{SyncTypeIFrame},
   610  		},
   611  		{
   612  			description:           "All Available - Many",
   613  			givenSyncTypes:        []SyncType{SyncTypeIFrame, SyncTypeRedirect},
   614  			givenIFrameTemplate:   endpointTemplate,
   615  			givenRedirectTemplate: endpointTemplate,
   616  			expectedSyncTypes:     []SyncType{SyncTypeIFrame, SyncTypeRedirect},
   617  		},
   618  		{
   619  			description:           "One Available - None",
   620  			givenSyncTypes:        []SyncType{},
   621  			givenIFrameTemplate:   nil,
   622  			givenRedirectTemplate: endpointTemplate,
   623  			expectedSyncTypes:     []SyncType{},
   624  		},
   625  		{
   626  			description:           "One Available - One - Not Supported",
   627  			givenSyncTypes:        []SyncType{SyncTypeIFrame},
   628  			givenIFrameTemplate:   nil,
   629  			givenRedirectTemplate: endpointTemplate,
   630  			expectedSyncTypes:     []SyncType{},
   631  		},
   632  		{
   633  			description:           "One Available - One - Supported",
   634  			givenSyncTypes:        []SyncType{SyncTypeRedirect},
   635  			givenIFrameTemplate:   nil,
   636  			givenRedirectTemplate: endpointTemplate,
   637  			expectedSyncTypes:     []SyncType{SyncTypeRedirect},
   638  		},
   639  		{
   640  			description:           "One Available - Many",
   641  			givenSyncTypes:        []SyncType{SyncTypeIFrame, SyncTypeRedirect},
   642  			givenIFrameTemplate:   nil,
   643  			givenRedirectTemplate: endpointTemplate,
   644  			expectedSyncTypes:     []SyncType{SyncTypeRedirect},
   645  		},
   646  	}
   647  
   648  	for _, test := range testCases {
   649  		syncer := standardSyncer{
   650  			iframe:   test.givenIFrameTemplate,
   651  			redirect: test.givenRedirectTemplate,
   652  		}
   653  		result := syncer.filterSupportedSyncTypes(test.givenSyncTypes)
   654  		assert.ElementsMatch(t, test.expectedSyncTypes, result, test.description)
   655  	}
   656  }
   657  
   658  func TestSyncerGetSync(t *testing.T) {
   659  	var (
   660  		iframeTemplate    = template.Must(template.New("test").Parse("iframe,gdpr:{{.GDPR}},gdprconsent:{{.GDPRConsent}},ccpa:{{.USPrivacy}}"))
   661  		redirectTemplate  = template.Must(template.New("test").Parse("redirect,gdpr:{{.GDPR}},gdprconsent:{{.GDPRConsent}},ccpa:{{.USPrivacy}}"))
   662  		malformedTemplate = template.Must(template.New("test").Parse("malformed,invalid:{{.DoesNotExist}}"))
   663  	)
   664  
   665  	testCases := []struct {
   666  		description    string
   667  		givenSyncer    standardSyncer
   668  		givenSyncTypes []SyncType
   669  		givenMacros    macros.UserSyncPrivacy
   670  		expectedError  string
   671  		expectedSync   Sync
   672  	}{
   673  		{
   674  			description:    "No Sync Types",
   675  			givenSyncer:    standardSyncer{iframe: iframeTemplate, redirect: redirectTemplate},
   676  			givenSyncTypes: []SyncType{},
   677  			givenMacros:    macros.UserSyncPrivacy{GDPR: "A", GDPRConsent: "B", USPrivacy: "C"},
   678  			expectedError:  "no sync types provided",
   679  		},
   680  		{
   681  			description:    "IFrame",
   682  			givenSyncer:    standardSyncer{iframe: iframeTemplate, redirect: redirectTemplate},
   683  			givenSyncTypes: []SyncType{SyncTypeIFrame},
   684  			givenMacros:    macros.UserSyncPrivacy{GDPR: "A", GDPRConsent: "B", USPrivacy: "C"},
   685  			expectedSync:   Sync{URL: "iframe,gdpr:A,gdprconsent:B,ccpa:C", Type: SyncTypeIFrame, SupportCORS: false},
   686  		},
   687  		{
   688  			description:    "Redirect",
   689  			givenSyncer:    standardSyncer{iframe: iframeTemplate, redirect: redirectTemplate},
   690  			givenSyncTypes: []SyncType{SyncTypeRedirect},
   691  			givenMacros:    macros.UserSyncPrivacy{GDPR: "A", GDPRConsent: "B", USPrivacy: "C"},
   692  			expectedSync:   Sync{URL: "redirect,gdpr:A,gdprconsent:B,ccpa:C", Type: SyncTypeRedirect, SupportCORS: false},
   693  		},
   694  		{
   695  			description:    "Macro Error",
   696  			givenSyncer:    standardSyncer{iframe: malformedTemplate},
   697  			givenSyncTypes: []SyncType{SyncTypeIFrame},
   698  			givenMacros:    macros.UserSyncPrivacy{GDPR: "A", GDPRConsent: "B", USPrivacy: "C"},
   699  			expectedError:  "template: test:1:20: executing \"test\" at <.DoesNotExist>: can't evaluate field DoesNotExist in type macros.UserSyncPrivacy",
   700  		},
   701  	}
   702  
   703  	for _, test := range testCases {
   704  		result, err := test.givenSyncer.GetSync(test.givenSyncTypes, test.givenMacros)
   705  
   706  		if test.expectedError == "" {
   707  			assert.NoError(t, err, test.description+":err")
   708  			assert.Equal(t, test.expectedSync, result, test.description+":sync")
   709  		} else {
   710  			assert.EqualError(t, err, test.expectedError, test.description+":err")
   711  		}
   712  	}
   713  }
   714  
   715  func TestSyncerChooseSyncType(t *testing.T) {
   716  	endpointTemplate := template.Must(template.New("test").Parse("iframe"))
   717  
   718  	testCases := []struct {
   719  		description           string
   720  		givenSyncTypes        []SyncType
   721  		givenDefaultSyncType  SyncType
   722  		givenIFrameTemplate   *template.Template
   723  		givenRedirectTemplate *template.Template
   724  		expectedError         string
   725  		expectedSyncType      SyncType
   726  	}{
   727  		{
   728  			description:           "None Available - Error",
   729  			givenSyncTypes:        []SyncType{},
   730  			givenDefaultSyncType:  SyncTypeRedirect,
   731  			givenIFrameTemplate:   endpointTemplate,
   732  			givenRedirectTemplate: endpointTemplate,
   733  			expectedError:         "no sync types provided",
   734  		},
   735  		{
   736  			description:           "All Available - Choose Default",
   737  			givenSyncTypes:        []SyncType{SyncTypeIFrame, SyncTypeRedirect},
   738  			givenDefaultSyncType:  SyncTypeRedirect,
   739  			givenIFrameTemplate:   endpointTemplate,
   740  			givenRedirectTemplate: endpointTemplate,
   741  			expectedSyncType:      SyncTypeRedirect,
   742  		},
   743  		{
   744  			description:           "Default Not Available - Choose Other One",
   745  			givenSyncTypes:        []SyncType{SyncTypeIFrame},
   746  			givenDefaultSyncType:  SyncTypeRedirect,
   747  			givenIFrameTemplate:   endpointTemplate,
   748  			givenRedirectTemplate: endpointTemplate,
   749  			expectedSyncType:      SyncTypeIFrame,
   750  		},
   751  		{
   752  			description:           "None Supported - Error",
   753  			givenSyncTypes:        []SyncType{SyncTypeIFrame},
   754  			givenDefaultSyncType:  SyncTypeRedirect,
   755  			givenIFrameTemplate:   nil,
   756  			givenRedirectTemplate: endpointTemplate,
   757  			expectedError:         "no sync types supported",
   758  		},
   759  	}
   760  
   761  	for _, test := range testCases {
   762  		syncer := standardSyncer{
   763  			defaultSyncType: test.givenDefaultSyncType,
   764  			iframe:          test.givenIFrameTemplate,
   765  			redirect:        test.givenRedirectTemplate,
   766  		}
   767  		result, err := syncer.chooseSyncType(test.givenSyncTypes)
   768  
   769  		if test.expectedError == "" {
   770  			assert.NoError(t, err, test.description+":err")
   771  			assert.Equal(t, test.expectedSyncType, result, test.description+":sync_type")
   772  		} else {
   773  			assert.EqualError(t, err, test.expectedError, test.description+":err")
   774  		}
   775  	}
   776  }
   777  
   778  func TestSyncerChooseTemplate(t *testing.T) {
   779  	var (
   780  		iframeTemplate   = template.Must(template.New("test").Parse("iframe"))
   781  		redirectTemplate = template.Must(template.New("test").Parse("redirect"))
   782  	)
   783  
   784  	testCases := []struct {
   785  		description      string
   786  		givenSyncType    SyncType
   787  		expectedTemplate *template.Template
   788  	}{
   789  		{
   790  			description:      "IFrame",
   791  			givenSyncType:    SyncTypeIFrame,
   792  			expectedTemplate: iframeTemplate,
   793  		},
   794  		{
   795  			description:      "Redirect",
   796  			givenSyncType:    SyncTypeRedirect,
   797  			expectedTemplate: redirectTemplate,
   798  		},
   799  		{
   800  			description:      "Invalid",
   801  			givenSyncType:    SyncType("invalid"),
   802  			expectedTemplate: nil,
   803  		},
   804  	}
   805  
   806  	for _, test := range testCases {
   807  		syncer := standardSyncer{iframe: iframeTemplate, redirect: redirectTemplate}
   808  		result := syncer.chooseTemplate(test.givenSyncType)
   809  		assert.Equal(t, test.expectedTemplate, result, test.description)
   810  	}
   811  }