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  }