github.com/prebid/prebid-server/v2@v2.18.0/hooks/hookexecution/enricher_test.go (about) 1 package hookexecution 2 3 import ( 4 "encoding/json" 5 "errors" 6 "os" 7 "testing" 8 9 "github.com/prebid/openrtb/v20/openrtb2" 10 "github.com/prebid/prebid-server/v2/config" 11 "github.com/prebid/prebid-server/v2/hooks/hookanalytics" 12 "github.com/prebid/prebid-server/v2/openrtb_ext" 13 "github.com/prebid/prebid-server/v2/util/jsonutil" 14 "github.com/stretchr/testify/assert" 15 "github.com/stretchr/testify/require" 16 ) 17 18 // StageOutcomeTest is used for test purpose instead of the original structure, 19 // so we can unmarshal hidden fields such as StageOutcomeTest.Stage and HookOutcomeTest.Errors/Warnings 20 type StageOutcomeTest struct { 21 ExecutionTime 22 Entity entity `json:"entity"` 23 Groups []GroupOutcomeTest `json:"groups"` 24 Stage string `json:"stage"` 25 } 26 27 type GroupOutcomeTest struct { 28 ExecutionTime 29 InvocationResults []HookOutcomeTest `json:"invocation_results"` 30 } 31 32 type HookOutcomeTest struct { 33 ExecutionTime 34 AnalyticsTags hookanalytics.Analytics `json:"analytics_tags"` 35 HookID HookID `json:"hook_id"` 36 Status Status `json:"status"` 37 Action Action `json:"action"` 38 Message string `json:"message"` 39 DebugMessages []string `json:"debug_messages"` 40 Errors []string `json:"errors"` 41 Warnings []string `json:"warnings"` 42 } 43 44 func TestEnrichBidResponse(t *testing.T) { 45 testCases := []struct { 46 description string 47 expectedWarnings []error 48 expectedBidResponseFile string 49 stageOutcomesFile string 50 bidResponse *openrtb2.BidResponse 51 bidRequest *openrtb2.BidRequest 52 account *config.Account 53 }{ 54 { 55 description: "BidResponse enriched with verbose trace and debug info when bidRequest.test=1 and trace=verbose", 56 expectedWarnings: nil, 57 expectedBidResponseFile: "test/complete-stage-outcomes/expected-verbose-debug-response.json", 58 stageOutcomesFile: "test/complete-stage-outcomes/stage-outcomes.json", 59 bidResponse: &openrtb2.BidResponse{Ext: []byte(`{"prebid": {"foo": "bar"}}`)}, 60 bidRequest: &openrtb2.BidRequest{Test: 1, Ext: []byte(`{"prebid": {"trace": "verbose"}}`)}, 61 account: &config.Account{DebugAllow: true}, 62 }, 63 { 64 description: "BidResponse enriched with basic trace and debug info when bidRequest.ext.prebid.debug=true and trace=basic", 65 expectedWarnings: nil, 66 expectedBidResponseFile: "test/complete-stage-outcomes/expected-basic-debug-response.json", 67 stageOutcomesFile: "test/complete-stage-outcomes/stage-outcomes.json", 68 bidResponse: &openrtb2.BidResponse{Ext: []byte(`{"prebid": {"foo": "bar"}}`)}, 69 bidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid": {"debug": true, "trace": "basic"}}`)}, 70 account: &config.Account{DebugAllow: true}, 71 }, 72 { 73 description: "BidResponse enriched with debug info when bidRequest.ext.prebid.debug=true", 74 expectedWarnings: nil, 75 expectedBidResponseFile: "test/complete-stage-outcomes/expected-debug-response.json", 76 stageOutcomesFile: "test/complete-stage-outcomes/stage-outcomes.json", 77 bidResponse: &openrtb2.BidResponse{Ext: []byte(`{"prebid": {"foo": "bar"}}`)}, 78 bidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid": {"debug": true, "trace": ""}}`)}, 79 account: &config.Account{DebugAllow: true}, 80 }, 81 { 82 description: "BidResponse not enriched when bidRequest.ext.prebid.debug=false", 83 expectedWarnings: nil, 84 expectedBidResponseFile: "test/complete-stage-outcomes/expected-empty-response.json", 85 stageOutcomesFile: "test/complete-stage-outcomes/stage-outcomes.json", 86 bidResponse: &openrtb2.BidResponse{Ext: []byte(`{"prebid": {"foo": "bar"}}`)}, 87 bidRequest: &openrtb2.BidRequest{}, 88 account: &config.Account{DebugAllow: true}, 89 }, 90 { 91 description: "BidResponse enriched only with verbose trace when bidRequest.ext.prebid.trace=verbose and account.DebugAllow=false", 92 expectedWarnings: nil, 93 expectedBidResponseFile: "test/complete-stage-outcomes/expected-verbose-response.json", 94 stageOutcomesFile: "test/complete-stage-outcomes/stage-outcomes.json", 95 bidResponse: &openrtb2.BidResponse{Ext: []byte(`{"prebid": {"foo": "bar"}}`)}, 96 bidRequest: &openrtb2.BidRequest{Test: 1, Ext: []byte(`{"prebid": {"debug": true, "trace": "verbose"}}`)}, 97 account: &config.Account{DebugAllow: false}, 98 }, 99 { 100 description: "BidResponse enriched with debug info if bidResponse.Ext is nil", 101 expectedWarnings: nil, 102 expectedBidResponseFile: "test/complete-stage-outcomes/expected-pure-debug-response.json", 103 stageOutcomesFile: "test/complete-stage-outcomes/stage-outcomes.json", 104 bidResponse: &openrtb2.BidResponse{}, 105 bidRequest: &openrtb2.BidRequest{Test: 1}, 106 account: &config.Account{DebugAllow: true}, 107 }, 108 { 109 description: "BidResponse not enriched with modules if stage outcome groups empty", 110 expectedWarnings: nil, 111 expectedBidResponseFile: "test/empty-stage-outcomes/empty.json", 112 stageOutcomesFile: "test/empty-stage-outcomes/empty-stage-outcomes-v1.json", 113 bidResponse: &openrtb2.BidResponse{}, 114 bidRequest: &openrtb2.BidRequest{Test: 1}, 115 account: &config.Account{DebugAllow: true}, 116 }, 117 { 118 description: "BidResponse not enriched with modules if stage outcomes empty", 119 expectedWarnings: nil, 120 expectedBidResponseFile: "test/empty-stage-outcomes/empty.json", 121 stageOutcomesFile: "test/empty-stage-outcomes/empty-stage-outcomes-v2.json", 122 bidResponse: &openrtb2.BidResponse{}, 123 bidRequest: &openrtb2.BidRequest{Test: 1}, 124 account: &config.Account{DebugAllow: true}, 125 }, 126 } 127 128 for _, test := range testCases { 129 t.Run(test.description, func(t *testing.T) { 130 expectedResponse := readFile(t, test.expectedBidResponseFile) 131 stageOutcomes := getStageOutcomes(t, test.stageOutcomesFile) 132 133 ext, warns, err := EnrichExtBidResponse(test.bidResponse.Ext, stageOutcomes, test.bidRequest, test.account) 134 require.NoError(t, err, "Failed to enrich BidResponse with hook debug information: %s", err) 135 assert.Equal(t, test.expectedWarnings, warns, "Unexpected warnings") 136 137 test.bidResponse.Ext = ext 138 if test.bidResponse.Ext == nil { 139 assert.Empty(t, expectedResponse) 140 } else { 141 assert.JSONEq(t, string(expectedResponse), string(test.bidResponse.Ext)) 142 } 143 }) 144 } 145 } 146 147 func TestGetModulesJSON(t *testing.T) { 148 testCases := []struct { 149 description string 150 expectedWarnings []error 151 expectedBidResponseFile string 152 stageOutcomesFile string 153 bidRequest *openrtb2.BidRequest 154 account *config.Account 155 }{ 156 { 157 description: "Modules Outcome contains verbose trace and debug info when bidRequest.test=1 and trace=verbose", 158 expectedWarnings: nil, 159 expectedBidResponseFile: "test/complete-stage-outcomes/expected-verbose-debug-response.json", 160 stageOutcomesFile: "test/complete-stage-outcomes/stage-outcomes.json", 161 bidRequest: &openrtb2.BidRequest{Test: 1, Ext: []byte(`{"prebid": {"trace": "verbose"}}`)}, 162 account: &config.Account{DebugAllow: true}, 163 }, 164 { 165 description: "Modules Outcome contains verbose trace and debug info when bidRequest.test=1 and trace=verbose and account is not defined", 166 expectedWarnings: nil, 167 expectedBidResponseFile: "test/complete-stage-outcomes/expected-verbose-debug-response.json", 168 stageOutcomesFile: "test/complete-stage-outcomes/stage-outcomes.json", 169 bidRequest: &openrtb2.BidRequest{Test: 1, Ext: []byte(`{"prebid": {"trace": "verbose"}}`)}, 170 account: nil, 171 }, 172 { 173 description: "Modules Outcome contains basic trace and debug info when bidRequest.ext.prebid.debug=true and trace=basic", 174 expectedWarnings: nil, 175 expectedBidResponseFile: "test/complete-stage-outcomes/expected-basic-debug-response.json", 176 stageOutcomesFile: "test/complete-stage-outcomes/stage-outcomes.json", 177 bidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid": {"debug": true, "trace": "basic"}}`)}, 178 account: &config.Account{DebugAllow: true}, 179 }, 180 { 181 description: "Modules Outcome contains debug info when bidRequest.ext.prebid.debug=true", 182 expectedWarnings: nil, 183 expectedBidResponseFile: "test/complete-stage-outcomes/expected-debug-response.json", 184 stageOutcomesFile: "test/complete-stage-outcomes/stage-outcomes.json", 185 bidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid": {"debug": true, "trace": ""}}`)}, 186 account: &config.Account{DebugAllow: true}, 187 }, 188 { 189 description: "Modules Outcome empty when bidRequest.ext.prebid.debug=false", 190 expectedWarnings: nil, 191 expectedBidResponseFile: "test/empty-stage-outcomes/empty.json", 192 stageOutcomesFile: "test/complete-stage-outcomes/stage-outcomes.json", 193 bidRequest: &openrtb2.BidRequest{}, 194 account: &config.Account{DebugAllow: true}, 195 }, 196 { 197 description: "Modules Outcome empty when bidRequest is nil", 198 expectedWarnings: nil, 199 expectedBidResponseFile: "test/empty-stage-outcomes/empty.json", 200 stageOutcomesFile: "test/complete-stage-outcomes/stage-outcomes.json", 201 bidRequest: nil, 202 account: &config.Account{DebugAllow: true}, 203 }, 204 { 205 description: "Modules Outcome contains only verbose trace when bidRequest.ext.prebid.trace=verbose and account.DebugAllow=false", 206 expectedWarnings: nil, 207 expectedBidResponseFile: "test/complete-stage-outcomes/expected-verbose-response.json", 208 stageOutcomesFile: "test/complete-stage-outcomes/stage-outcomes.json", 209 bidRequest: &openrtb2.BidRequest{Test: 1, Ext: []byte(`{"prebid": {"debug": true, "trace": "verbose"}}`)}, 210 account: &config.Account{DebugAllow: false}, 211 }, 212 { 213 description: "Modules Outcome contains debug info if bidResponse.Ext is nil", 214 expectedWarnings: nil, 215 expectedBidResponseFile: "test/complete-stage-outcomes/expected-pure-debug-response.json", 216 stageOutcomesFile: "test/complete-stage-outcomes/stage-outcomes.json", 217 bidRequest: &openrtb2.BidRequest{Test: 1}, 218 account: &config.Account{DebugAllow: true}, 219 }, 220 { 221 description: "Modules Outcome empty if stage outcome groups empty", 222 expectedWarnings: nil, 223 expectedBidResponseFile: "test/empty-stage-outcomes/empty.json", 224 stageOutcomesFile: "test/empty-stage-outcomes/empty-stage-outcomes-v1.json", 225 bidRequest: &openrtb2.BidRequest{Test: 1}, 226 account: &config.Account{DebugAllow: true}, 227 }, 228 { 229 description: "Modules Outcome empty if stage outcomes empty", 230 expectedWarnings: nil, 231 expectedBidResponseFile: "test/empty-stage-outcomes/empty.json", 232 stageOutcomesFile: "test/empty-stage-outcomes/empty-stage-outcomes-v2.json", 233 bidRequest: &openrtb2.BidRequest{Test: 1}, 234 account: &config.Account{DebugAllow: true}, 235 }, 236 { 237 description: "Warnings returned if debug info invalid", 238 expectedWarnings: []error{ 239 errors.New("Value is not a string: 1"), 240 errors.New("Value is not a boolean: active"), 241 }, 242 expectedBidResponseFile: "test/complete-stage-outcomes/expected-pure-debug-response.json", 243 stageOutcomesFile: "test/complete-stage-outcomes/stage-outcomes.json", 244 bidRequest: &openrtb2.BidRequest{Test: 1, Ext: json.RawMessage(`{"prebid": {"debug": "active", "trace": 1}}`)}, 245 account: &config.Account{DebugAllow: true}, 246 }, 247 } 248 249 for _, test := range testCases { 250 t.Run(test.description, func(t *testing.T) { 251 expectedResponse := readFile(t, test.expectedBidResponseFile) 252 stageOutcomes := getStageOutcomes(t, test.stageOutcomesFile) 253 254 modules, warns, err := GetModulesJSON(stageOutcomes, test.bidRequest, test.account) 255 require.NoError(t, err, "Failed to get modules outcome as json: %s", err) 256 assert.Equal(t, test.expectedWarnings, warns, "Unexpected warnings") 257 258 if modules == nil { 259 assert.Empty(t, expectedResponse) 260 } else { 261 var expectedExtBidResponse openrtb_ext.ExtBidResponse 262 err := jsonutil.UnmarshalValid(expectedResponse, &expectedExtBidResponse) 263 assert.NoError(t, err, "Failed to unmarshal prebid response extension") 264 assert.JSONEq(t, string(expectedExtBidResponse.Prebid.Modules), string(modules)) 265 } 266 }) 267 } 268 } 269 270 func getStageOutcomes(t *testing.T, file string) []StageOutcome { 271 var stageOutcomes []StageOutcome 272 var stageOutcomesTest []StageOutcomeTest 273 274 data := readFile(t, file) 275 err := jsonutil.UnmarshalValid(data, &stageOutcomesTest) 276 require.NoError(t, err, "Failed to unmarshal stage outcomes: %s", err) 277 278 for _, stageT := range stageOutcomesTest { 279 stage := StageOutcome{ 280 ExecutionTime: stageT.ExecutionTime, 281 Entity: stageT.Entity, 282 Stage: stageT.Stage, 283 } 284 285 for _, groupT := range stageT.Groups { 286 group := GroupOutcome{ExecutionTime: groupT.ExecutionTime} 287 for _, hookT := range groupT.InvocationResults { 288 group.InvocationResults = append(group.InvocationResults, HookOutcome(hookT)) 289 } 290 291 stage.Groups = append(stage.Groups, group) 292 } 293 stageOutcomes = append(stageOutcomes, stage) 294 } 295 296 return stageOutcomes 297 } 298 299 func readFile(t *testing.T, filename string) []byte { 300 data, err := os.ReadFile(filename) 301 require.NoError(t, err, "Failed to read file %s: %v", filename, err) 302 return data 303 }