github.com/prebid/prebid-server/v2@v2.18.0/analytics/pubstack/pubstack_module_test.go (about)

     1  package pubstack
     2  
     3  import (
     4  	"net/http"
     5  	"net/http/httptest"
     6  	"os"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/benbjohnson/clock"
    11  	"github.com/prebid/prebid-server/v2/analytics"
    12  	"github.com/prebid/prebid-server/v2/openrtb_ext"
    13  	"github.com/stretchr/testify/assert"
    14  )
    15  
    16  func TestNewModuleErrors(t *testing.T) {
    17  	tests := []struct {
    18  		description  string
    19  		refreshDelay string
    20  		maxByteSize  string
    21  		maxTime      string
    22  	}{
    23  		{
    24  			description:  "refresh delay is in an invalid format",
    25  			refreshDelay: "1invalid",
    26  			maxByteSize:  "90MB",
    27  			maxTime:      "15m",
    28  		},
    29  		{
    30  			description:  "max byte size is in an invalid format",
    31  			refreshDelay: "1h",
    32  			maxByteSize:  "90invalid",
    33  			maxTime:      "15m",
    34  		},
    35  		{
    36  			description:  "max time is in an invalid format",
    37  			refreshDelay: "1h",
    38  			maxByteSize:  "90MB",
    39  			maxTime:      "15invalid",
    40  		},
    41  	}
    42  
    43  	for _, tt := range tests {
    44  		_, err := NewModule(&http.Client{}, "scope", "http://example.com", tt.refreshDelay, 100, tt.maxByteSize, tt.maxTime, clock.NewMock())
    45  		assert.Error(t, err, tt.description)
    46  	}
    47  }
    48  
    49  func TestNewModuleSuccess(t *testing.T) {
    50  	tests := []struct {
    51  		description string
    52  		feature     string
    53  		logObject   func(analytics.Module)
    54  	}{
    55  		{
    56  			description: "auction events are only published when logging an auction object with auction feature on",
    57  			feature:     auction,
    58  			logObject: func(module analytics.Module) {
    59  				module.LogAuctionObject(&analytics.AuctionObject{Status: http.StatusOK})
    60  			},
    61  		},
    62  		{
    63  			description: "AMP events are only published when logging an AMP object with AMP feature on",
    64  			feature:     amp,
    65  			logObject: func(module analytics.Module) {
    66  				module.LogAmpObject(&analytics.AmpObject{Status: http.StatusOK})
    67  			},
    68  		},
    69  		{
    70  			description: "video events are only published when logging a video object with video feature on",
    71  			feature:     video,
    72  			logObject: func(module analytics.Module) {
    73  				module.LogVideoObject(&analytics.VideoObject{Status: http.StatusOK})
    74  			},
    75  		},
    76  		{
    77  			description: "cookie events are only published when logging a cookie object with cookie feature on",
    78  			feature:     cookieSync,
    79  			logObject: func(module analytics.Module) {
    80  				module.LogCookieSyncObject(&analytics.CookieSyncObject{Status: http.StatusOK})
    81  			},
    82  		},
    83  		{
    84  			description: "setUID events are only published when logging a setUID object with setUID feature on",
    85  			feature:     setUID,
    86  			logObject: func(module analytics.Module) {
    87  				module.LogSetUIDObject(&analytics.SetUIDObject{Status: http.StatusOK})
    88  			},
    89  		},
    90  		{
    91  			description: "Ignore excluded fields from marshal",
    92  			feature:     auction,
    93  			logObject: func(module analytics.Module) {
    94  				module.LogAuctionObject(&analytics.AuctionObject{
    95  					RequestWrapper: &openrtb_ext.RequestWrapper{},
    96  					SeatNonBid: []openrtb_ext.SeatNonBid{
    97  						{
    98  							NonBid: []openrtb_ext.NonBid{
    99  								{
   100  									ImpId:      "123",
   101  									StatusCode: 34,
   102  									Ext:        openrtb_ext.NonBidExt{Prebid: openrtb_ext.ExtResponseNonBidPrebid{Bid: openrtb_ext.NonBidObject{}}},
   103  								},
   104  							},
   105  						},
   106  					},
   107  				})
   108  			},
   109  		},
   110  	}
   111  
   112  	for _, tt := range tests {
   113  		// original config with the feature disabled so no events should be sent
   114  		origConfig := &Configuration{
   115  			Features: map[string]bool{
   116  				tt.feature: false,
   117  			},
   118  		}
   119  
   120  		// updated config with the feature enabled so events should be sent
   121  		updatedConfig := &Configuration{
   122  			Features: map[string]bool{
   123  				tt.feature: true,
   124  			},
   125  		}
   126  
   127  		// create server with an intake endpoint that PBS hits when events are sent
   128  		mux := http.NewServeMux()
   129  		intakeChannel := make(chan int)
   130  		mux.HandleFunc("/intake/"+tt.feature+"/", func(res http.ResponseWriter, req *http.Request) {
   131  			intakeChannel <- 1
   132  		})
   133  		server := httptest.NewServer(mux)
   134  		client := server.Client()
   135  
   136  		// set the event server url on each of the configs
   137  		origConfig.Endpoint = server.URL
   138  		updatedConfig.Endpoint = server.URL
   139  
   140  		// instantiate module with a manual config update task
   141  		clockMock := clock.NewMock()
   142  		configTask := fakeConfigUpdateTask{}
   143  		module, err := NewModuleWithConfigTask(client, "scope", server.URL, 100, "1B", "1s", &configTask, clockMock)
   144  		assert.NoError(t, err, tt.description)
   145  
   146  		pubstack, _ := module.(*PubstackModule)
   147  
   148  		// original config
   149  		configTask.Push(origConfig)
   150  		time.Sleep(10 * time.Millisecond)                            // allow time for the module to load the original config
   151  		tt.logObject(pubstack)                                       // attempt to log; no event channel created because feature is disabled in original config
   152  		clockMock.Add(1 * time.Second)                               // trigger event channel sending
   153  		assertChanNone(t, intakeChannel, tt.description+":original") // verify no event was received
   154  
   155  		// updated config
   156  		configTask.Push(updatedConfig)
   157  		time.Sleep(10 * time.Millisecond)                          // allow time for the server to start serving the updated config
   158  		tt.logObject(pubstack)                                     // attempt to log; event channel should be created because feature is enabled in updated config
   159  		clockMock.Add(1 * time.Second)                             // trigger event channel sending
   160  		assertChanOne(t, intakeChannel, tt.description+":updated") // verify an event was received
   161  
   162  		// no config change
   163  		configTask.Push(updatedConfig)
   164  		time.Sleep(10 * time.Millisecond)                            // allow time for the server to determine no config change
   165  		tt.logObject(pubstack)                                       // attempt to log; event channel should still be created from loading updated config
   166  		clockMock.Add(1 * time.Second)                               // trigger event channel sending
   167  		assertChanOne(t, intakeChannel, tt.description+":no_change") // verify an event was received
   168  
   169  		// shutdown
   170  		pubstack.sigTermCh <- os.Kill                                // simulate os shutdown signal
   171  		time.Sleep(10 * time.Millisecond)                            // allow time for the server to switch to shutdown generated config
   172  		tt.logObject(pubstack)                                       // attempt to log; event channel should be closed from the os kill signal
   173  		clockMock.Add(1 * time.Second)                               // trigger event channel sending
   174  		assertChanNone(t, intakeChannel, tt.description+":shutdown") // verify no event was received
   175  	}
   176  }
   177  
   178  func assertChanNone(t *testing.T, c <-chan int, msgAndArgs ...interface{}) bool {
   179  	t.Helper()
   180  	select {
   181  	case <-c:
   182  		return assert.Fail(t, "Should NOT receive an event, but did", msgAndArgs...)
   183  	case <-time.After(100 * time.Millisecond):
   184  		return true
   185  	}
   186  }
   187  
   188  func assertChanOne(t *testing.T, c <-chan int, msgAndArgs ...interface{}) bool {
   189  	t.Helper()
   190  	select {
   191  	case <-c:
   192  		return true
   193  	case <-time.After(200 * time.Millisecond):
   194  		return assert.Fail(t, "Should receive an event, but did NOT", msgAndArgs...)
   195  	}
   196  }
   197  
   198  type fakeConfigUpdateTask struct {
   199  	configChan chan *Configuration
   200  }
   201  
   202  func (f *fakeConfigUpdateTask) Start(stop <-chan struct{}) <-chan *Configuration {
   203  	f.configChan = make(chan *Configuration)
   204  	return f.configChan
   205  }
   206  
   207  func (f *fakeConfigUpdateTask) Push(c *Configuration) {
   208  	f.configChan <- c
   209  }