github.com/web-platform-tests/wpt.fyi@v0.0.0-20240530210107-70cf978996f1/shared/web_features_manifest_github_download_test.go (about)

     1  // Copyright 2024 The WPT Dashboard Project. All rights reserved.
     2  // Use of this source code is governed by a BSD-style license that can be
     3  // found in the LICENSE file.
     4  
     5  //go:build small
     6  
     7  package shared
     8  
     9  import (
    10  	"bytes"
    11  	"compress/gzip"
    12  	"context"
    13  	"encoding/json"
    14  	"errors"
    15  	"io"
    16  	"net/http"
    17  	"os"
    18  	"path/filepath"
    19  	"testing"
    20  
    21  	"github.com/google/go-github/v47/github"
    22  	"github.com/stretchr/testify/assert"
    23  	"github.com/stretchr/testify/require"
    24  )
    25  
    26  var compressedWebFeaturesManifestFilePath = filepath.Join("web_features_manifest_testdata", "WEB_FEATURES_MANIFEST.json.gz")
    27  
    28  func createWebFeaturesTestdata() {
    29  	v1Manifest := struct {
    30  		Version int                 `json:"version,omitempty"`
    31  		Data    map[string][]string `json:"data,omitempty"`
    32  	}{
    33  		Version: 1,
    34  		Data: map[string][]string{
    35  			"grid":    {"test1.js", "test2.js"},
    36  			"subgrid": {"test3.js", "test4.js"},
    37  		},
    38  	}
    39  	jsonData, err := json.Marshal(v1Manifest)
    40  	if err != nil {
    41  		panic(err)
    42  	}
    43  
    44  	// Create a buffer for compressing the JSON.
    45  	var buf bytes.Buffer
    46  
    47  	// Create a gzip writer and write the JSON to it.
    48  	gz := gzip.NewWriter(&buf)
    49  	if _, err := gz.Write(jsonData); err != nil {
    50  		panic(err)
    51  	}
    52  	if err := gz.Close(); err != nil {
    53  		panic(err)
    54  	}
    55  
    56  	// Write the compressed data to a file.
    57  	if err := os.WriteFile(compressedWebFeaturesManifestFilePath, buf.Bytes(), 0644); err != nil {
    58  		panic(err)
    59  	}
    60  }
    61  
    62  func TestResponseBodyTransformer_Success(t *testing.T) {
    63  	updateGolden := false // Switch this when we want to update the golden file.
    64  	if updateGolden {
    65  		createWebFeaturesTestdata()
    66  	}
    67  	f, err := os.Open(compressedWebFeaturesManifestFilePath)
    68  	defer f.Close()
    69  	require.NoError(t, err)
    70  
    71  	transformer := gzipBodyTransformer{}
    72  	reader, err := transformer.Transform(f)
    73  	defer reader.Close()
    74  	require.NoError(t, err)
    75  
    76  	rawBytes, err := io.ReadAll(reader)
    77  	require.NoError(t, err)
    78  
    79  	assert.Equal(t, `{"version":1,"data":{"grid":["test1.js","test2.js"],"subgrid":["test3.js","test4.js"]}}`, string(rawBytes))
    80  }
    81  
    82  type RoundTripFunc struct {
    83  	function func(req *http.Request) *http.Response
    84  	err      error
    85  }
    86  
    87  func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
    88  	return f.function(req), f.err
    89  }
    90  
    91  type mockBodyTransformerInput struct {
    92  	expectedBody string
    93  	output       io.ReadCloser
    94  	err          error
    95  }
    96  
    97  type mockBodyTransformer struct {
    98  	t *testing.T
    99  	mockBodyTransformerInput
   100  }
   101  
   102  func (tr mockBodyTransformer) Transform(body io.Reader) (io.ReadCloser, error) {
   103  	bodyBytes, err := io.ReadAll(body)
   104  	require.NoError(tr.t, err)
   105  	assert.Equal(tr.t, tr.expectedBody, string(bodyBytes))
   106  	return tr.output, tr.err
   107  }
   108  
   109  type mockRepositoryReleaseGetter struct {
   110  	t *testing.T
   111  	mockRepositoryReleaseGetterInput
   112  }
   113  
   114  type mockRepositoryReleaseGetterInput struct {
   115  	expectedOwner string
   116  	expectedRepo  string
   117  	repoRelease   *github.RepositoryRelease
   118  	resp          *github.Response
   119  	err           error
   120  }
   121  
   122  func (g mockRepositoryReleaseGetter) GetLatestRelease(
   123  	ctx context.Context,
   124  	owner, repo string) (*github.RepositoryRelease, *github.Response, error) {
   125  	require.Equal(g.t, g.expectedOwner, owner)
   126  	require.Equal(g.t, g.expectedRepo, repo)
   127  	return g.repoRelease, g.resp, g.err
   128  }
   129  
   130  /*
   131  Truncated example output from GitHub API.
   132  Useful for building the returned responses in TestGitHubWebFeaturesManifestDownloader_Download.
   133  
   134  gh release view --repo web-platform-tests/wpt --json assets
   135  {
   136    "assets": [
   137      {
   138        "apiUrl": "https://api.github.com/repos/web-platform-tests/wpt/releases/assets/147533430",
   139        "contentType": "application/octet-stream",
   140        "createdAt": "2024-01-24T14:40:18Z",
   141        "downloadCount": 0,
   142        "id": "RA_kwDOADc1Vc4Iyy52",
   143        "label": "WEB_FEATURES_MANIFEST.json.gz",
   144        "name": "WEB_FEATURES_MANIFEST-f8871bc568c2cf86b38cb70f28a9d5f707e19259.json.gz",
   145        "size": 38815,
   146        "state": "uploaded",
   147        "updatedAt": "2024-01-24T14:40:18Z",
   148        "url": "https://github.com/web-platform-tests/wpt/releases/download/merge_pr_41522/WEB_FEATURES_MANIFEST-f8871bc568c2cf86b38cb70f28a9d5f707e19259.json.gz"
   149      }
   150    ]
   151  }
   152  */
   153  
   154  func TestGitHubWebFeaturesManifestDownloader_Download(t *testing.T) {
   155  	// Test cases for Download
   156  	tests := []struct {
   157  		name               string
   158  		releaseGetterInput mockRepositoryReleaseGetterInput
   159  		roundTrip          RoundTripFunc
   160  		transformer        mockBodyTransformerInput
   161  		expectedBody       []byte
   162  		expectedError      error
   163  	}{
   164  		{
   165  			name: "successful download",
   166  			releaseGetterInput: mockRepositoryReleaseGetterInput{
   167  				expectedOwner: "web-platform-tests",
   168  				expectedRepo:  "wpt",
   169  				repoRelease: &github.RepositoryRelease{
   170  					Assets: []*github.ReleaseAsset{
   171  						{
   172  							Label:              github.String("WEB_FEATURES_MANIFEST.json.gz"),
   173  							BrowserDownloadURL: github.String("https://example.com/WEB_FEATURES_MANIFEST.json.gz"),
   174  						},
   175  					},
   176  				},
   177  				resp: &github.Response{},
   178  				err:  nil,
   179  			},
   180  			roundTrip: RoundTripFunc{function: func(req *http.Request) *http.Response {
   181  				assert.Equal(t, "https://example.com/WEB_FEATURES_MANIFEST.json.gz", req.URL.String())
   182  				return &http.Response{
   183  					StatusCode:    http.StatusOK,
   184  					ContentLength: int64(len("raw data")),
   185  					Body:          io.NopCloser(bytes.NewBufferString("raw data")),
   186  				}
   187  			}, err: nil},
   188  			transformer: mockBodyTransformerInput{
   189  				expectedBody: "raw data",
   190  				output:       io.NopCloser(bytes.NewBufferString("transformed data")),
   191  				err:          nil,
   192  			},
   193  			expectedBody:  []byte("transformed data"),
   194  			expectedError: nil,
   195  		},
   196  		{
   197  			name: "error getting latest release",
   198  			releaseGetterInput: mockRepositoryReleaseGetterInput{
   199  				expectedOwner: "web-platform-tests",
   200  				expectedRepo:  "wpt",
   201  				repoRelease:   nil,
   202  				resp:          nil,
   203  				err:           errors.New("fake GitHub client error"),
   204  			},
   205  			expectedBody:  nil,
   206  			expectedError: errUnableToRetrieveGitHubRelease,
   207  		},
   208  		{
   209  			name: "manifest file not found",
   210  			releaseGetterInput: mockRepositoryReleaseGetterInput{
   211  				expectedOwner: "web-platform-tests",
   212  				expectedRepo:  "wpt",
   213  				repoRelease: &github.RepositoryRelease{
   214  					Assets: []*github.ReleaseAsset{},
   215  				},
   216  				resp: &github.Response{},
   217  				err:  nil,
   218  			},
   219  			expectedBody:  nil,
   220  			expectedError: errNoWebFeaturesManifestFileFound,
   221  		},
   222  		{
   223  			name: "error downloading asset",
   224  			releaseGetterInput: mockRepositoryReleaseGetterInput{
   225  				expectedOwner: "web-platform-tests",
   226  				expectedRepo:  "wpt",
   227  				repoRelease: &github.RepositoryRelease{
   228  					Assets: []*github.ReleaseAsset{
   229  						{
   230  							Label:              github.String("WEB_FEATURES_MANIFEST.json.gz"),
   231  							BrowserDownloadURL: github.String("https://example.com/WEB_FEATURES_MANIFEST.json.gz"),
   232  						},
   233  					},
   234  				},
   235  				resp: &github.Response{},
   236  				err:  nil,
   237  			},
   238  			roundTrip: RoundTripFunc{function: func(req *http.Request) *http.Response {
   239  				assert.Equal(t, "https://example.com/WEB_FEATURES_MANIFEST.json.gz", req.URL.String())
   240  				return &http.Response{
   241  					StatusCode: http.StatusInternalServerError,
   242  				}
   243  			}, err: errors.New("simulated network error")},
   244  			expectedBody:  nil,
   245  			expectedError: errGitHubAssetDownloadFailedToComplete,
   246  		},
   247  		{
   248  			name: "empty response body",
   249  			releaseGetterInput: mockRepositoryReleaseGetterInput{
   250  				expectedOwner: "web-platform-tests",
   251  				expectedRepo:  "wpt",
   252  				repoRelease: &github.RepositoryRelease{
   253  					Assets: []*github.ReleaseAsset{
   254  						{
   255  							Label:              github.String("WEB_FEATURES_MANIFEST.json.gz"),
   256  							BrowserDownloadURL: github.String("https://example.com/WEB_FEATURES_MANIFEST.json.gz"),
   257  						},
   258  					},
   259  				},
   260  				resp: &github.Response{},
   261  				err:  nil,
   262  			},
   263  			roundTrip: RoundTripFunc{function: func(req *http.Request) *http.Response {
   264  				assert.Equal(t, "https://example.com/WEB_FEATURES_MANIFEST.json.gz", req.URL.String())
   265  				return &http.Response{
   266  					StatusCode: http.StatusNoContent,
   267  					Body:       nil,
   268  				}
   269  			}, err: nil},
   270  			expectedBody:  nil,
   271  			expectedError: errMissingBodyDuringWebFeaturesManifestDownload,
   272  		},
   273  	}
   274  	for _, tc := range tests {
   275  		t.Run(tc.name, func(t *testing.T) {
   276  			getter := mockRepositoryReleaseGetter{t, tc.releaseGetterInput}
   277  			httpClient := &http.Client{
   278  				Transport: tc.roundTrip,
   279  			}
   280  			downloader := newGitHubWebFeaturesManifestDownloader(httpClient, getter)
   281  			downloader.bodyTransformer = mockBodyTransformer{t, tc.transformer}
   282  			body, err := downloader.Download(context.Background())
   283  			if !errors.Is(err, tc.expectedError) {
   284  				t.Errorf("Download() returned unexpected error: (%v). expected error: (%v).", err, tc.expectedError)
   285  			}
   286  
   287  			// No need to compare the body if there's an error.
   288  			if err != nil {
   289  				return
   290  			}
   291  
   292  			bodyBytes, err := io.ReadAll(body)
   293  			require.NoError(t, err)
   294  			assert.Equal(t, tc.expectedBody, bodyBytes)
   295  		})
   296  	}
   297  }