github.com/web-platform-tests/wpt.fyi@v0.0.0-20240530210107-70cf978996f1/api/query/search_medium_test.go (about) 1 //go:build medium 2 // +build medium 3 4 // Copyright 2017 The WPT Dashboard Project. All rights reserved. 5 // Use of this source code is governed by a BSD-style license that can be 6 // found in the LICENSE file. 7 8 package query 9 10 import ( 11 "bytes" 12 "context" 13 "encoding/json" 14 "fmt" 15 "io" 16 "net/http" 17 "net/http/httptest" 18 "net/url" 19 "testing" 20 time "time" 21 22 "github.com/golang/mock/gomock" 23 "github.com/stretchr/testify/assert" 24 "github.com/web-platform-tests/wpt.fyi/shared" 25 "github.com/web-platform-tests/wpt.fyi/shared/sharedtest" 26 ) 27 28 type shouldCache struct { 29 Called bool 30 31 t *testing.T 32 expected bool 33 delegate func(context.Context, int, []byte) bool 34 } 35 36 func (sc *shouldCache) ShouldCache(ctx context.Context, statusCode int, payload []byte) bool { 37 sc.Called = true 38 ret := sc.delegate(ctx, statusCode, payload) 39 assert.Equal(sc.t, sc.expected, ret) 40 return ret 41 } 42 43 func NewShouldCache(t *testing.T, expected bool, delegate func(context.Context, int, []byte) bool) *shouldCache { 44 return &shouldCache{false, t, expected, delegate} 45 } 46 47 func TestUnstructuredSearchHandler(t *testing.T) { 48 urls := []string{ 49 "https://example.com/1-summary_v2.json.gz", 50 "https://example.com/2-summary_v2.json.gz", 51 } 52 testRuns := shared.TestRuns{ 53 shared.TestRun{ 54 ResultsURL: urls[0], 55 }, 56 shared.TestRun{ 57 ResultsURL: urls[1], 58 }, 59 } 60 summaryBytes := [][]byte{ 61 []byte(`{"/a/b/c": {"s":"T","c":[1,2]},"/b/c":{"s":"O","c":[9,9]}}`), 62 []byte(`{"/z/b/c": {"s":"F","c":[0,8]},"/b/c":{"s":"O","c":[5,9]}}`), 63 } 64 65 i, err := sharedtest.NewAEInstance(true) 66 assert.Nil(t, err) 67 defer i.Close() 68 69 // Scope setup context. 70 { 71 req, err := i.NewRequest("GET", "/", nil) 72 assert.Nil(t, err) 73 ctx := req.Context() 74 store := shared.NewAppEngineDatastore(ctx, false) 75 76 for idx := range testRuns { 77 key, err := store.Put(store.NewIncompleteKey("TestRun"), &testRuns[idx]) 78 assert.Nil(t, err) 79 id := key.IntID() 80 assert.NotEqual(t, 0, id) 81 testRuns[idx].ID = id 82 } 83 } 84 85 mockCtrl := gomock.NewController(t) 86 defer mockCtrl.Finish() 87 88 // TODO(markdittmer): Should this be hitting GCS instead? 89 cache := sharedtest.NewMockReadable(mockCtrl) 90 rs := []*sharedtest.MockReadCloser{ 91 sharedtest.NewMockReadCloser(t, summaryBytes[0]), 92 sharedtest.NewMockReadCloser(t, summaryBytes[1]), 93 } 94 95 cache.EXPECT().NewReadCloser(urls[0]).Return(rs[0], nil) 96 cache.EXPECT().NewReadCloser(urls[1]).Return(rs[1], nil) 97 98 // Same params as TestGetRunsAndFilters_specificRunIDs. 99 q := "/b/" 100 url := fmt.Sprintf( 101 "/api/search?run_ids=%s&q=%s", 102 url.QueryEscape(fmt.Sprintf("%d,%d", testRuns[0].ID, testRuns[1].ID)), 103 url.QueryEscape(q)) 104 r, err := i.NewRequest("GET", url, nil) 105 assert.Nil(t, err) 106 ctx := r.Context() 107 w := httptest.NewRecorder() 108 109 // TODO: This is parroting apiSearchHandler details. Perhaps they should be 110 // abstracted and tested directly. 111 mc := shared.NewGZReadWritable(shared.NewRedisReadWritable(ctx, 48*time.Hour)) 112 sh := unstructuredSearchHandler{queryHandler{ 113 store: shared.NewAppEngineDatastore(ctx, false), 114 dataSource: shared.NewByteCachedStore(ctx, mc, cache), 115 }} 116 sc := NewShouldCache(t, true, shouldCacheSearchResponse) 117 118 ch := shared.NewCachingHandler(ctx, sh, mc, isRequestCacheable, shared.URLAsCacheKey, sc.ShouldCache) 119 ch.ServeHTTP(w, r) 120 assert.Equal(t, http.StatusOK, w.Code) 121 bytes, err := io.ReadAll(w.Result().Body) 122 assert.Nil(t, err) 123 var data shared.SearchResponse 124 err = json.Unmarshal(bytes, &data) 125 assert.Nil(t, err) 126 127 // Same result as TestGetRunsAndFilters_specificRunIDs. 128 assert.Equal(t, shared.SearchResponse{ 129 Runs: testRuns, 130 Results: []shared.SearchResult{ 131 { 132 Test: "/a/b/c", 133 LegacyStatus: []shared.LegacySearchRunResult{ 134 { 135 Passes: 1, 136 Total: 2, 137 Status: "T", 138 NewAggProcess: true, 139 }, 140 { 141 Passes: 0, 142 Total: 0, 143 Status: "", 144 NewAggProcess: false, 145 }, 146 }, 147 }, 148 { 149 Test: "/b/c", 150 LegacyStatus: []shared.LegacySearchRunResult{ 151 { 152 Passes: 9, 153 Total: 9, 154 Status: "O", 155 NewAggProcess: true, 156 }, 157 { 158 Passes: 5, 159 Total: 9, 160 Status: "O", 161 NewAggProcess: true, 162 }, 163 }, 164 }, 165 { 166 Test: "/z/b/c", 167 LegacyStatus: []shared.LegacySearchRunResult{ 168 { 169 Passes: 0, 170 Total: 0, 171 Status: "", 172 NewAggProcess: false, 173 }, 174 { 175 Passes: 0, 176 Total: 8, 177 Status: "F", 178 NewAggProcess: true, 179 }, 180 }, 181 }, 182 }, 183 }, data) 184 185 assert.True(t, rs[0].IsClosed()) 186 assert.True(t, rs[1].IsClosed()) 187 assert.True(t, sc.Called) 188 } 189 190 func TestStructuredSearchHandler_equivalentToUnstructured(t *testing.T) { 191 urls := []string{ 192 "https://example.com/1-summary_v2.json.gz", 193 "https://example.com/2-summary_v2.json.gz", 194 } 195 testRuns := []shared.TestRun{ 196 { 197 ResultsURL: urls[0], 198 }, 199 { 200 ResultsURL: urls[1], 201 }, 202 } 203 summaryBytes := [][]byte{ 204 []byte(`{"/a/b/c": {"s":"T","c":[1,2]},"/b/c":{"s":"O","c":[9,9]}}`), 205 []byte(`{"/z/b/c": {"s":"F","c":[0,8]},"/b/c":{"s":"O","c":[5,9]}}`), 206 } 207 208 i, err := sharedtest.NewAEInstance(true) 209 assert.Nil(t, err) 210 defer i.Close() 211 212 // Scope setup context. 213 { 214 req, err := i.NewRequest("GET", "/", nil) 215 assert.Nil(t, err) 216 ctx := req.Context() 217 store := shared.NewAppEngineDatastore(ctx, false) 218 219 for idx, testRun := range testRuns { 220 key, err := store.Put(store.NewIncompleteKey("TestRun"), &testRun) 221 assert.Nil(t, err) 222 id := key.IntID() 223 assert.NotEqual(t, 0, id) 224 testRun.ID = id 225 // Copy back testRun after mutating ID. 226 testRuns[idx] = testRun 227 } 228 } 229 230 mockCtrl := gomock.NewController(t) 231 defer mockCtrl.Finish() 232 233 // TODO(markdittmer): Should this be hitting GCS instead? 234 store := sharedtest.NewMockReadable(mockCtrl) 235 rs := []*sharedtest.MockReadCloser{ 236 sharedtest.NewMockReadCloser(t, summaryBytes[0]), 237 sharedtest.NewMockReadCloser(t, summaryBytes[1]), 238 } 239 240 store.EXPECT().NewReadCloser(urls[0]).Return(rs[0], nil) 241 store.EXPECT().NewReadCloser(urls[1]).Return(rs[1], nil) 242 243 // Same params as TestGetRunsAndFilters_specificRunIDs. 244 q, err := json.Marshal("/b/") 245 assert.Nil(t, err) 246 url := "/api/search" 247 r, err := i.NewRequest("POST", url, bytes.NewBuffer([]byte(fmt.Sprintf(`{ 248 "run_ids": [%d, %d], 249 "query": {"exists": [{"pattern": %s}] } 250 }`, testRuns[0].ID, testRuns[1].ID, string(q))))) 251 assert.Nil(t, err) 252 ctx := r.Context() 253 w := httptest.NewRecorder() 254 255 // TODO: This is parroting apiSearchHandler details. Perhaps they should be 256 // abstracted and tested directly. 257 api := shared.NewAppEngineAPI(ctx) 258 mc := shared.NewGZReadWritable(shared.NewRedisReadWritable(ctx, 48*time.Hour)) 259 sh := structuredSearchHandler{ 260 queryHandler{ 261 store: shared.NewAppEngineDatastore(ctx, false), 262 dataSource: shared.NewByteCachedStore(ctx, mc, store), 263 }, 264 api, 265 } 266 sc := NewShouldCache(t, true, shouldCacheSearchResponse) 267 268 ch := shared.NewCachingHandler(ctx, sh, mc, isRequestCacheable, shared.URLAsCacheKey, sc.ShouldCache) 269 ch.ServeHTTP(w, r) 270 assert.Equal(t, http.StatusOK, w.Code) 271 bytes, err := io.ReadAll(w.Result().Body) 272 assert.Nil(t, err) 273 var data shared.SearchResponse 274 err = json.Unmarshal(bytes, &data) 275 assert.Nil(t, err, "Error unmarshalling \"%s\"", string(bytes)) 276 277 // Same result as TestGetRunsAndFilters_specificRunIDs. 278 assert.Equal(t, shared.SearchResponse{ 279 Runs: testRuns, 280 Results: []shared.SearchResult{ 281 { 282 Test: "/a/b/c", 283 LegacyStatus: []shared.LegacySearchRunResult{ 284 { 285 Passes: 1, 286 Total: 2, 287 Status: "T", 288 NewAggProcess: true, 289 }, 290 { 291 Passes: 0, 292 Total: 0, 293 Status: "", 294 NewAggProcess: false, 295 }, 296 }, 297 }, 298 { 299 Test: "/b/c", 300 LegacyStatus: []shared.LegacySearchRunResult{ 301 { 302 Passes: 9, 303 Total: 9, 304 Status: "O", 305 NewAggProcess: true, 306 }, 307 { 308 Passes: 5, 309 Total: 9, 310 Status: "O", 311 NewAggProcess: true, 312 }, 313 }, 314 }, 315 { 316 Test: "/z/b/c", 317 LegacyStatus: []shared.LegacySearchRunResult{ 318 { 319 Passes: 0, 320 Total: 0, 321 Status: "", 322 NewAggProcess: false, 323 }, 324 { 325 Passes: 0, 326 Total: 8, 327 Status: "F", 328 NewAggProcess: true, 329 }, 330 }, 331 }, 332 }, 333 }, data) 334 335 assert.True(t, rs[0].IsClosed()) 336 assert.True(t, rs[1].IsClosed()) 337 assert.True(t, sc.Called) 338 } 339 340 func TestUnstructuredSearchHandler_doNotCacheEmptyResult(t *testing.T) { 341 urls := []string{ 342 "https://example.com/1-summary_v2.json.gz", 343 "https://example.com/2-summary_v2.json.gz", 344 } 345 testRuns := shared.TestRuns{ 346 shared.TestRun{ 347 ResultsURL: urls[0], 348 }, 349 shared.TestRun{ 350 ResultsURL: urls[1], 351 }, 352 } 353 summaryBytes := [][]byte{ 354 []byte(`{}`), 355 []byte(`{}`), 356 } 357 358 i, err := sharedtest.NewAEInstance(true) 359 assert.Nil(t, err) 360 defer i.Close() 361 362 // Scope setup context. 363 { 364 req, err := i.NewRequest("GET", "/", nil) 365 assert.Nil(t, err) 366 ctx := req.Context() 367 store := shared.NewAppEngineDatastore(ctx, false) 368 369 for idx, testRun := range testRuns { 370 key, err := store.Put(store.NewIncompleteKey("TestRun"), &testRun) 371 assert.Nil(t, err) 372 id := key.IntID() 373 assert.NotEqual(t, 0, id) 374 testRun.ID = id 375 // Copy back testRun after mutating ID. 376 testRuns[idx] = testRun 377 } 378 } 379 380 mockCtrl := gomock.NewController(t) 381 defer mockCtrl.Finish() 382 383 // TODO(markdittmer): Should this be hitting GCS instead? 384 store := sharedtest.NewMockReadable(mockCtrl) 385 rs := []*sharedtest.MockReadCloser{ 386 sharedtest.NewMockReadCloser(t, summaryBytes[0]), 387 sharedtest.NewMockReadCloser(t, summaryBytes[1]), 388 } 389 390 store.EXPECT().NewReadCloser(urls[0]).Return(rs[0], nil) 391 store.EXPECT().NewReadCloser(urls[1]).Return(rs[1], nil) 392 393 // Same params as TestGetRunsAndFilters_specificRunIDs. 394 q := "/b/" 395 url := fmt.Sprintf( 396 "/api/search?run_ids=%s&q=%s", 397 url.QueryEscape(fmt.Sprintf("%d,%d", testRuns[0].ID, testRuns[1].ID)), 398 url.QueryEscape(q)) 399 r, err := i.NewRequest("GET", url, nil) 400 assert.Nil(t, err) 401 ctx := r.Context() 402 w := httptest.NewRecorder() 403 404 // TODO: This is parroting apiSearchHandler details. Perhaps they should be 405 // abstracted and tested directly. 406 mc := shared.NewGZReadWritable(shared.NewRedisReadWritable(ctx, 48*time.Hour)) 407 sh := unstructuredSearchHandler{ 408 queryHandler{ 409 store: shared.NewAppEngineDatastore(ctx, false), 410 dataSource: shared.NewByteCachedStore(ctx, mc, store), 411 }, 412 } 413 sc := NewShouldCache(t, false, shouldCacheSearchResponse) 414 415 ch := shared.NewCachingHandler(ctx, sh, mc, isRequestCacheable, shared.URLAsCacheKey, sc.ShouldCache) 416 ch.ServeHTTP(w, r) 417 assert.Equal(t, http.StatusOK, w.Code) 418 bytes, err := io.ReadAll(w.Result().Body) 419 assert.Nil(t, err) 420 var data shared.SearchResponse 421 err = json.Unmarshal(bytes, &data) 422 assert.Nil(t, err) 423 424 // Same result as TestGetRunsAndFilters_specificRunIDs. 425 assert.Equal(t, shared.SearchResponse{ 426 Runs: testRuns, 427 Results: []shared.SearchResult{}, 428 }, data) 429 430 assert.True(t, rs[0].IsClosed()) 431 assert.True(t, rs[1].IsClosed()) 432 assert.True(t, sc.Called) 433 } 434 435 func TestStructuredSearchHandler_doNotCacheEmptyResult(t *testing.T) { 436 urls := []string{ 437 "https://example.com/1-summary_v2.json.gz", 438 "https://example.com/2-summary_v2.json.gz", 439 } 440 testRuns := []shared.TestRun{ 441 { 442 ResultsURL: urls[0], 443 }, 444 { 445 ResultsURL: urls[1], 446 }, 447 } 448 summaryBytes := [][]byte{ 449 []byte(`{}`), 450 []byte(`{}`), 451 } 452 453 i, err := sharedtest.NewAEInstance(true) 454 assert.Nil(t, err) 455 defer i.Close() 456 457 // Scope setup context. 458 { 459 req, err := i.NewRequest("GET", "/", nil) 460 assert.Nil(t, err) 461 ctx := req.Context() 462 store := shared.NewAppEngineDatastore(ctx, false) 463 464 for idx, testRun := range testRuns { 465 key, err := store.Put(store.NewIncompleteKey("TestRun"), &testRun) 466 assert.Nil(t, err) 467 id := key.IntID() 468 assert.NotEqual(t, 0, id) 469 testRun.ID = id 470 // Copy back testRun after mutating ID. 471 testRuns[idx] = testRun 472 } 473 } 474 475 mockCtrl := gomock.NewController(t) 476 defer mockCtrl.Finish() 477 478 // TODO(markdittmer): Should this be hitting GCS instead? 479 store := sharedtest.NewMockReadable(mockCtrl) 480 rs := []*sharedtest.MockReadCloser{ 481 sharedtest.NewMockReadCloser(t, summaryBytes[0]), 482 sharedtest.NewMockReadCloser(t, summaryBytes[1]), 483 } 484 485 store.EXPECT().NewReadCloser(urls[0]).Return(rs[0], nil) 486 store.EXPECT().NewReadCloser(urls[1]).Return(rs[1], nil) 487 488 // Same params as TestGetRunsAndFilters_specificRunIDs. 489 q, err := json.Marshal("/b/") 490 assert.Nil(t, err) 491 url := "/api/search" 492 r, err := i.NewRequest("POST", url, bytes.NewBuffer([]byte(fmt.Sprintf(`{ 493 "run_ids": [%d, %d], 494 "query": {"exists": [{"pattern": %s}] } 495 }`, testRuns[0].ID, testRuns[1].ID, string(q))))) 496 assert.Nil(t, err) 497 ctx := r.Context() 498 w := httptest.NewRecorder() 499 500 // TODO: This is parroting apiSearchHandler details. Perhaps they should be 501 // abstracted and tested directly. 502 api := shared.NewAppEngineAPI(ctx) 503 mc := shared.NewGZReadWritable(shared.NewRedisReadWritable(ctx, 48*time.Hour)) 504 sh := structuredSearchHandler{ 505 queryHandler{ 506 store: shared.NewAppEngineDatastore(ctx, false), 507 dataSource: shared.NewByteCachedStore(ctx, mc, store), 508 }, 509 api, 510 } 511 sc := NewShouldCache(t, false, shouldCacheSearchResponse) 512 513 ch := shared.NewCachingHandler(ctx, sh, mc, isRequestCacheable, shared.URLAsCacheKey, sc.ShouldCache) 514 ch.ServeHTTP(w, r) 515 assert.Equal(t, http.StatusOK, w.Code) 516 bytes, err := io.ReadAll(w.Result().Body) 517 assert.Nil(t, err) 518 var data shared.SearchResponse 519 err = json.Unmarshal(bytes, &data) 520 assert.Nil(t, err) 521 522 // Same result as TestGetRunsAndFilters_specificRunIDs. 523 assert.Equal(t, shared.SearchResponse{ 524 Runs: testRuns, 525 Results: []shared.SearchResult{}, 526 }, data) 527 528 assert.True(t, rs[0].IsClosed()) 529 assert.True(t, rs[1].IsClosed()) 530 assert.True(t, sc.Called) 531 }