sigs.k8s.io/cluster-api@v1.6.3/cmd/clusterctl/client/repository/repository_github_test.go (about)

     1  /*
     2  Copyright 2019 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package repository
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"net/http"
    23  	"net/http/httptest"
    24  	"net/url"
    25  	"testing"
    26  	"time"
    27  
    28  	"github.com/google/go-github/v53/github"
    29  	. "github.com/onsi/gomega"
    30  	"k8s.io/utils/pointer"
    31  
    32  	clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
    33  	"sigs.k8s.io/cluster-api/cmd/clusterctl/client/config"
    34  	"sigs.k8s.io/cluster-api/cmd/clusterctl/internal/test"
    35  	"sigs.k8s.io/cluster-api/internal/goproxy"
    36  )
    37  
    38  func Test_gitHubRepository_GetVersions(t *testing.T) {
    39  	retryableOperationInterval = 200 * time.Millisecond
    40  	retryableOperationTimeout = 1 * time.Second
    41  
    42  	client, mux, teardown := test.NewFakeGitHub()
    43  	defer teardown()
    44  
    45  	// Setup an handler for returning 5 fake releases.
    46  	mux.HandleFunc("/repos/o/r1/releases", func(w http.ResponseWriter, r *http.Request) {
    47  		testMethod(t, r, "GET")
    48  		fmt.Fprint(w, `[`)
    49  		fmt.Fprint(w, `{"id":1, "tag_name": "v0.4.0"},`)
    50  		fmt.Fprint(w, `{"id":2, "tag_name": "v0.4.1"},`)
    51  		fmt.Fprint(w, `{"id":3, "tag_name": "v0.4.2"},`)
    52  		fmt.Fprint(w, `{"id":4, "tag_name": "v0.4.3-alpha"}`) // Pre-release
    53  		fmt.Fprint(w, `]`)
    54  	})
    55  
    56  	clientGoproxy, muxGoproxy, teardownGoproxy := newFakeGoproxy()
    57  	defer teardownGoproxy()
    58  
    59  	// Setup a handler for returning 4 fake releases.
    60  	muxGoproxy.HandleFunc("/github.com/o/r2/@v/list", func(w http.ResponseWriter, r *http.Request) {
    61  		testMethod(t, r, "GET")
    62  		fmt.Fprint(w, "v0.5.0\n")
    63  		fmt.Fprint(w, "v0.4.0\n")
    64  		fmt.Fprint(w, "v0.3.2\n")
    65  		fmt.Fprint(w, "v0.3.1\n")
    66  	})
    67  
    68  	// Setup a handler for returning 3 different major fake releases.
    69  	muxGoproxy.HandleFunc("/github.com/o/r3/@v/list", func(w http.ResponseWriter, r *http.Request) {
    70  		testMethod(t, r, "GET")
    71  		fmt.Fprint(w, "v1.0.0\n")
    72  		fmt.Fprint(w, "v0.1.0\n")
    73  	})
    74  	muxGoproxy.HandleFunc("/github.com/o/r3/v2/@v/list", func(w http.ResponseWriter, r *http.Request) {
    75  		testMethod(t, r, "GET")
    76  		fmt.Fprint(w, "v2.0.0\n")
    77  	})
    78  	muxGoproxy.HandleFunc("/github.com/o/r3/v3/@v/list", func(w http.ResponseWriter, r *http.Request) {
    79  		testMethod(t, r, "GET")
    80  		fmt.Fprint(w, "v3.0.0\n")
    81  	})
    82  
    83  	configVariablesClient := test.NewFakeVariableClient()
    84  
    85  	tests := []struct {
    86  		name           string
    87  		providerConfig config.Provider
    88  		want           []string
    89  		wantErr        bool
    90  	}{
    91  		{
    92  			name:           "fallback to github",
    93  			providerConfig: config.NewProvider("test", "https://github.com/o/r1/releases/v0.4.0/path", clusterctlv1.CoreProviderType),
    94  			want:           []string{"v0.4.0", "v0.4.1", "v0.4.2", "v0.4.3-alpha"},
    95  			wantErr:        false,
    96  		},
    97  		{
    98  			name:           "use goproxy",
    99  			providerConfig: config.NewProvider("test", "https://github.com/o/r2/releases/v0.4.0/path", clusterctlv1.CoreProviderType),
   100  			want:           []string{"v0.3.1", "v0.3.2", "v0.4.0", "v0.5.0"},
   101  			wantErr:        false,
   102  		},
   103  		{
   104  			name:           "use goproxy having multiple majors",
   105  			providerConfig: config.NewProvider("test", "https://github.com/o/r3/releases/v3.0.0/path", clusterctlv1.CoreProviderType),
   106  			want:           []string{"v0.1.0", "v1.0.0", "v2.0.0", "v3.0.0"},
   107  			wantErr:        false,
   108  		},
   109  		{
   110  			name:           "failure",
   111  			providerConfig: config.NewProvider("test", "https://github.com/o/unknown/releases/v0.4.0/path", clusterctlv1.CoreProviderType),
   112  			wantErr:        true,
   113  		},
   114  	}
   115  	for _, tt := range tests {
   116  		t.Run(tt.name, func(t *testing.T) {
   117  			g := NewWithT(t)
   118  
   119  			ctx := context.Background()
   120  
   121  			resetCaches()
   122  
   123  			gRepo, err := NewGitHubRepository(ctx, tt.providerConfig, configVariablesClient, injectGithubClient(client), injectGoproxyClient(clientGoproxy))
   124  			g.Expect(err).ToNot(HaveOccurred())
   125  
   126  			got, err := gRepo.GetVersions(ctx)
   127  			if tt.wantErr {
   128  				g.Expect(err).To(HaveOccurred())
   129  				return
   130  			}
   131  			g.Expect(err).ToNot(HaveOccurred())
   132  			g.Expect(got).To(Equal(tt.want))
   133  		})
   134  	}
   135  }
   136  
   137  func Test_githubRepository_newGitHubRepository(t *testing.T) {
   138  	retryableOperationInterval = 200 * time.Millisecond
   139  	retryableOperationTimeout = 1 * time.Second
   140  	type field struct {
   141  		providerConfig config.Provider
   142  		variableClient config.VariablesClient
   143  	}
   144  	tests := []struct {
   145  		name    string
   146  		field   field
   147  		want    *gitHubRepository
   148  		wantErr bool
   149  	}{
   150  		{
   151  			name: "can create a new GitHub repo",
   152  			field: field{
   153  				providerConfig: config.NewProvider("test", "https://github.com/o/r1/releases/v0.4.1/path", clusterctlv1.CoreProviderType),
   154  				variableClient: test.NewFakeVariableClient(),
   155  			},
   156  			want: &gitHubRepository{
   157  				providerConfig:           config.NewProvider("test", "https://github.com/o/r1/releases/v0.4.1/path", clusterctlv1.CoreProviderType),
   158  				configVariablesClient:    test.NewFakeVariableClient(),
   159  				authenticatingHTTPClient: nil,
   160  				owner:                    "o",
   161  				repository:               "r1",
   162  				defaultVersion:           "v0.4.1",
   163  				rootPath:                 ".",
   164  				componentsPath:           "path",
   165  				injectClient:             nil,
   166  			},
   167  			wantErr: false,
   168  		},
   169  		{
   170  			name: "missing variableClient",
   171  			field: field{
   172  				providerConfig: config.NewProvider("test", "https://github.com/o/r1/releases/v0.4.1/path", clusterctlv1.CoreProviderType),
   173  				variableClient: nil,
   174  			},
   175  			want:    nil,
   176  			wantErr: true,
   177  		},
   178  		{
   179  			name: "provider url is not valid",
   180  			field: field{
   181  				providerConfig: config.NewProvider("test", "%gh&%ij", clusterctlv1.CoreProviderType),
   182  				variableClient: test.NewFakeVariableClient(),
   183  			},
   184  			want:    nil,
   185  			wantErr: true,
   186  		},
   187  		{
   188  			name: "provider url should be in https",
   189  			field: field{
   190  				providerConfig: config.NewProvider("test", "http://github.com/blabla", clusterctlv1.CoreProviderType),
   191  				variableClient: test.NewFakeVariableClient(),
   192  			},
   193  			want:    nil,
   194  			wantErr: true,
   195  		},
   196  		{
   197  			name: "provider url should be in github",
   198  			field: field{
   199  				providerConfig: config.NewProvider("test", "http://gitlab.com/blabla", clusterctlv1.CoreProviderType),
   200  				variableClient: test.NewFakeVariableClient(),
   201  			},
   202  			want:    &gitHubRepository{},
   203  			wantErr: true,
   204  		},
   205  		{
   206  			name: "provider url should be in https://github.com/{owner}/{Repository}/%s/{latest|version-tag}/{componentsClient.yaml} format",
   207  			field: field{
   208  				providerConfig: config.NewProvider("test", "https://github.com/dd/", clusterctlv1.CoreProviderType),
   209  				variableClient: test.NewFakeVariableClient(),
   210  			},
   211  			want:    nil,
   212  			wantErr: true,
   213  		},
   214  	}
   215  
   216  	for _, tt := range tests {
   217  		t.Run(tt.name, func(t *testing.T) {
   218  			g := NewWithT(t)
   219  			resetCaches()
   220  
   221  			gitHub, err := NewGitHubRepository(context.Background(), tt.field.providerConfig, tt.field.variableClient)
   222  			if tt.wantErr {
   223  				g.Expect(err).To(HaveOccurred())
   224  				return
   225  			}
   226  
   227  			g.Expect(err).ToNot(HaveOccurred())
   228  			g.Expect(gitHub).To(Equal(tt.want))
   229  		})
   230  	}
   231  }
   232  
   233  func Test_githubRepository_getComponentsPath(t *testing.T) {
   234  	tests := []struct {
   235  		name     string
   236  		path     string
   237  		rootPath string
   238  		want     string
   239  	}{
   240  		{
   241  			name:     "get the file name",
   242  			path:     "github.com/o/r/releases/v0.4.1/file.yaml",
   243  			rootPath: "github.com/o/r/releases/v0.4.1/",
   244  			want:     "file.yaml",
   245  		},
   246  		{
   247  			name:     "trim github.com",
   248  			path:     "github.com/o/r/releases/v0.4.1/file.yaml",
   249  			rootPath: "github.com",
   250  			want:     "o/r/releases/v0.4.1/file.yaml",
   251  		},
   252  	}
   253  
   254  	for _, tt := range tests {
   255  		t.Run(tt.name, func(t *testing.T) {
   256  			g := NewWithT(t)
   257  			resetCaches()
   258  
   259  			g.Expect(getComponentsPath(tt.path, tt.rootPath)).To(Equal(tt.want))
   260  		})
   261  	}
   262  }
   263  
   264  func Test_githubRepository_getFile(t *testing.T) {
   265  	retryableOperationInterval = 200 * time.Millisecond
   266  	retryableOperationTimeout = 1 * time.Second
   267  	client, mux, teardown := test.NewFakeGitHub()
   268  	defer teardown()
   269  
   270  	providerConfig := config.NewProvider("test", "https://github.com/o/r/releases/v0.4.1/file.yaml", clusterctlv1.CoreProviderType)
   271  
   272  	// Setup a handler for returning a fake release.
   273  	mux.HandleFunc("/repos/o/r/releases/tags/v0.4.1", func(w http.ResponseWriter, r *http.Request) {
   274  		testMethod(t, r, "GET")
   275  		fmt.Fprint(w, `{"id":13, "tag_name": "v0.4.1", "assets": [{"id": 1, "name": "file.yaml"}] }`)
   276  	})
   277  
   278  	// Setup a handler for returning a fake release asset.
   279  	mux.HandleFunc("/repos/o/r/releases/assets/1", func(w http.ResponseWriter, r *http.Request) {
   280  		testMethod(t, r, "GET")
   281  		w.Header().Set("Content-Type", "application/octet-stream")
   282  		w.Header().Set("Content-Disposition", "attachment; filename=file.yaml")
   283  		fmt.Fprint(w, "content")
   284  	})
   285  
   286  	configVariablesClient := test.NewFakeVariableClient()
   287  
   288  	tests := []struct {
   289  		name     string
   290  		release  string
   291  		fileName string
   292  		want     []byte
   293  		wantErr  bool
   294  	}{
   295  		{
   296  			name:     "Release and file exist",
   297  			release:  "v0.4.1",
   298  			fileName: "file.yaml",
   299  			want:     []byte("content"),
   300  			wantErr:  false,
   301  		},
   302  		{
   303  			name:     "Release does not exist",
   304  			release:  "not-a-release",
   305  			fileName: "file.yaml",
   306  			want:     nil,
   307  			wantErr:  true,
   308  		},
   309  		{
   310  			name:     "File does not exist",
   311  			release:  "v0.4.1",
   312  			fileName: "404.file",
   313  			want:     nil,
   314  			wantErr:  true,
   315  		},
   316  	}
   317  
   318  	for _, tt := range tests {
   319  		t.Run(tt.name, func(t *testing.T) {
   320  			g := NewWithT(t)
   321  			resetCaches()
   322  
   323  			gitHub, err := NewGitHubRepository(context.Background(), providerConfig, configVariablesClient, injectGithubClient(client))
   324  			g.Expect(err).ToNot(HaveOccurred())
   325  
   326  			got, err := gitHub.GetFile(context.Background(), tt.release, tt.fileName)
   327  			if tt.wantErr {
   328  				g.Expect(err).To(HaveOccurred())
   329  				return
   330  			}
   331  
   332  			g.Expect(err).ToNot(HaveOccurred())
   333  			g.Expect(got).To(Equal(tt.want))
   334  		})
   335  	}
   336  }
   337  
   338  func Test_gitHubRepository_getVersions(t *testing.T) {
   339  	retryableOperationInterval = 200 * time.Millisecond
   340  	retryableOperationTimeout = 1 * time.Second
   341  	client, mux, teardown := test.NewFakeGitHub()
   342  	defer teardown()
   343  
   344  	// Setup a handler for returning fake releases in a paginated manner
   345  	// Each response contains a link to the next page (if available) which
   346  	// is parsed by the handler to navigate through all pages
   347  	mux.HandleFunc("/repos/o/r1/releases", func(w http.ResponseWriter, r *http.Request) {
   348  		testMethod(t, r, "GET")
   349  		page := r.URL.Query().Get("page")
   350  		switch page {
   351  		case "", "1":
   352  			// Page 1
   353  			w.Header().Set("Link", `<https://api.github.com/repositories/12345/releases?page=2>; rel="next"`) // Link to page 2
   354  			fmt.Fprint(w, `[`)
   355  			fmt.Fprint(w, `{"id":1, "tag_name": "v0.4.0"},`)
   356  			fmt.Fprint(w, `{"id":2, "tag_name": "v0.4.1"}`)
   357  			fmt.Fprint(w, `]`)
   358  		case "2":
   359  			// Page 2
   360  			w.Header().Set("Link", `<https://api.github.com/repositories/12345/releases?page=3>; rel="next"`) // Link to page 3
   361  			fmt.Fprint(w, `[`)
   362  			fmt.Fprint(w, `{"id":3, "tag_name": "v0.4.2"},`)
   363  			fmt.Fprint(w, `{"id":4, "tag_name": "v0.4.3-alpha"}`) // Pre-release
   364  			fmt.Fprint(w, `]`)
   365  		case "3":
   366  			// Page 3 (last page)
   367  			fmt.Fprint(w, `[`)
   368  			fmt.Fprint(w, `{"id":4, "tag_name": "v0.4.4-beta"},`) // Pre-release
   369  			fmt.Fprint(w, `{"id":5, "tag_name": "foo"}`)          // No semantic version tag
   370  			fmt.Fprint(w, `]`)
   371  		default:
   372  			t.Fatalf("unexpected page requested")
   373  		}
   374  	})
   375  
   376  	configVariablesClient := test.NewFakeVariableClient()
   377  
   378  	type field struct {
   379  		providerConfig config.Provider
   380  	}
   381  	tests := []struct {
   382  		name    string
   383  		field   field
   384  		want    []string
   385  		wantErr bool
   386  	}{
   387  		{
   388  			name: "Get versions with all releases",
   389  			field: field{
   390  				providerConfig: config.NewProvider("test", "https://github.com/o/r1/releases/v0.4.1/path", clusterctlv1.CoreProviderType),
   391  			},
   392  			want:    []string{"v0.4.0", "v0.4.1", "v0.4.2", "v0.4.3-alpha", "v0.4.4-beta"},
   393  			wantErr: false,
   394  		},
   395  	}
   396  	for _, tt := range tests {
   397  		t.Run(tt.name, func(t *testing.T) {
   398  			g := NewWithT(t)
   399  
   400  			ctx := context.Background()
   401  
   402  			resetCaches()
   403  
   404  			gitHub, err := NewGitHubRepository(ctx, tt.field.providerConfig, configVariablesClient, injectGithubClient(client))
   405  			g.Expect(err).ToNot(HaveOccurred())
   406  
   407  			got, err := gitHub.(*gitHubRepository).getVersions(ctx)
   408  			if tt.wantErr {
   409  				g.Expect(err).To(HaveOccurred())
   410  				return
   411  			}
   412  			g.Expect(err).ToNot(HaveOccurred())
   413  
   414  			g.Expect(got).To(ConsistOf(tt.want))
   415  		})
   416  	}
   417  }
   418  
   419  func Test_gitHubRepository_getLatestContractRelease(t *testing.T) {
   420  	retryableOperationInterval = 200 * time.Millisecond
   421  	retryableOperationTimeout = 1 * time.Second
   422  	client, mux, teardown := test.NewFakeGitHub()
   423  	defer teardown()
   424  
   425  	// Setup a handler for returning a fake release.
   426  	mux.HandleFunc("/repos/o/r1/releases/tags/v0.5.0", func(w http.ResponseWriter, r *http.Request) {
   427  		testMethod(t, r, "GET")
   428  		fmt.Fprint(w, `{"id":13, "tag_name": "v0.5.0", "assets": [{"id": 1, "name": "metadata.yaml"}] }`)
   429  	})
   430  
   431  	// Setup a handler for returning a fake release metadata file.
   432  	mux.HandleFunc("/repos/o/r1/releases/assets/1", func(w http.ResponseWriter, r *http.Request) {
   433  		testMethod(t, r, "GET")
   434  		w.Header().Set("Content-Type", "application/octet-stream")
   435  		w.Header().Set("Content-Disposition", "attachment; filename=metadata.yaml")
   436  		fmt.Fprint(w, "apiVersion: clusterctl.cluster.x-k8s.io/v1alpha3\nreleaseSeries:\n  - major: 0\n    minor: 4\n    contract: v1alpha4\n  - major: 0\n    minor: 5\n    contract: v1alpha4\n  - major: 0\n    minor: 3\n    contract: v1alpha3\n")
   437  	})
   438  
   439  	clientGoproxy, muxGoproxy, teardownGoproxy := newFakeGoproxy()
   440  	defer teardownGoproxy()
   441  
   442  	// Setup a handler for returning 4 fake releases.
   443  	muxGoproxy.HandleFunc("/github.com/o/r1/@v/list", func(w http.ResponseWriter, r *http.Request) {
   444  		testMethod(t, r, "GET")
   445  		fmt.Fprint(w, "v0.5.0\n")
   446  		fmt.Fprint(w, "v0.4.0\n")
   447  		fmt.Fprint(w, "v0.3.2\n")
   448  		fmt.Fprint(w, "v0.3.1\n")
   449  	})
   450  
   451  	// setup an handler for returning 4 fake releases but no actual tagged release
   452  	muxGoproxy.HandleFunc("/github.com/o/r2/@v/list", func(w http.ResponseWriter, r *http.Request) {
   453  		testMethod(t, r, "GET")
   454  		fmt.Fprint(w, "v0.5.0\n")
   455  		fmt.Fprint(w, "v0.4.0\n")
   456  		fmt.Fprint(w, "v0.3.2\n")
   457  		fmt.Fprint(w, "v0.3.1\n")
   458  	})
   459  
   460  	configVariablesClient := test.NewFakeVariableClient()
   461  
   462  	type field struct {
   463  		providerConfig config.Provider
   464  	}
   465  	tests := []struct {
   466  		name     string
   467  		field    field
   468  		contract string
   469  		want     string
   470  		wantErr  bool
   471  	}{
   472  		{
   473  			name: "Get latest release if it matches the contract",
   474  			field: field{
   475  				providerConfig: config.NewProvider("test", "https://github.com/o/r1/releases/latest/path", clusterctlv1.CoreProviderType),
   476  			},
   477  			contract: "v1alpha4",
   478  			want:     "v0.5.0",
   479  			wantErr:  false,
   480  		},
   481  		{
   482  			name: "Get previous release if the latest doesn't match the contract",
   483  			field: field{
   484  				providerConfig: config.NewProvider("test", "https://github.com/o/r1/releases/latest/path", clusterctlv1.CoreProviderType),
   485  			},
   486  			contract: "v1alpha3",
   487  			want:     "v0.3.2",
   488  			wantErr:  false,
   489  		},
   490  		{
   491  			name: "Return the latest release if the contract doesn't exist",
   492  			field: field{
   493  				providerConfig: config.NewProvider("test", "https://github.com/o/r1/releases/latest/path", clusterctlv1.CoreProviderType),
   494  			},
   495  			want:     "v0.5.0",
   496  			contract: "foo",
   497  			wantErr:  false,
   498  		},
   499  		{
   500  			name: "Return 404 if there is no release for the tag",
   501  			field: field{
   502  				providerConfig: config.NewProvider("test", "https://github.com/o/r2/releases/v0.99.0/path", clusterctlv1.CoreProviderType),
   503  			},
   504  			want:     "0.99.0",
   505  			contract: "v1alpha4",
   506  			wantErr:  true,
   507  		},
   508  	}
   509  	for _, tt := range tests {
   510  		t.Run(tt.name, func(t *testing.T) {
   511  			g := NewWithT(t)
   512  			resetCaches()
   513  
   514  			gRepo, err := NewGitHubRepository(context.Background(), tt.field.providerConfig, configVariablesClient, injectGithubClient(client), injectGoproxyClient(clientGoproxy))
   515  			g.Expect(err).ToNot(HaveOccurred())
   516  
   517  			got, err := latestContractRelease(context.Background(), gRepo, tt.contract)
   518  			if tt.wantErr {
   519  				g.Expect(err).To(HaveOccurred())
   520  				return
   521  			}
   522  			g.Expect(err).ToNot(HaveOccurred())
   523  			g.Expect(got).To(Equal(tt.want))
   524  		})
   525  	}
   526  }
   527  
   528  func Test_gitHubRepository_getLatestRelease(t *testing.T) {
   529  	retryableOperationInterval = 200 * time.Millisecond
   530  	retryableOperationTimeout = 1 * time.Second
   531  	clientGoproxy, muxGoproxy, teardownGoproxy := newFakeGoproxy()
   532  	defer teardownGoproxy()
   533  
   534  	// Setup a handler for returning 4 fake releases.
   535  	muxGoproxy.HandleFunc("/github.com/o/r1/@v/list", func(w http.ResponseWriter, r *http.Request) {
   536  		testMethod(t, r, "GET")
   537  		fmt.Fprint(w, "v0.4.1\n")
   538  		fmt.Fprint(w, "v0.4.2\n")
   539  		fmt.Fprint(w, "v0.4.3-alpha\n") // prerelease
   540  		fmt.Fprint(w, "foo\n")          // no semantic version tag
   541  	})
   542  	// And also expose a release for them
   543  	muxGoproxy.HandleFunc("/api.github.com/repos/o/r1/releases/tags/v0.4.2", func(w http.ResponseWriter, r *http.Request) {
   544  		testMethod(t, r, "GET")
   545  		fmt.Fprint(w, "{}\n")
   546  	})
   547  
   548  	// Setup a handler for returning no releases.
   549  	muxGoproxy.HandleFunc("/github.com/o/r2/@v/list", func(w http.ResponseWriter, r *http.Request) {
   550  		testMethod(t, r, "GET")
   551  		// no releases
   552  	})
   553  
   554  	// Setup a handler for returning fake prereleases only.
   555  	muxGoproxy.HandleFunc("/github.com/o/r3/@v/list", func(w http.ResponseWriter, r *http.Request) {
   556  		testMethod(t, r, "GET")
   557  		fmt.Fprint(w, "v0.1.0-alpha.0\n")
   558  		fmt.Fprint(w, "v0.1.0-alpha.1\n")
   559  		fmt.Fprint(w, "v0.1.0-alpha.2\n")
   560  	})
   561  
   562  	configVariablesClient := test.NewFakeVariableClient()
   563  
   564  	type field struct {
   565  		providerConfig config.Provider
   566  	}
   567  	tests := []struct {
   568  		name    string
   569  		field   field
   570  		want    string
   571  		wantErr bool
   572  	}{
   573  		{
   574  			name: "Get latest release, ignores pre-release version",
   575  			field: field{
   576  				providerConfig: config.NewProvider("test", "https://github.com/o/r1/releases/v0.4.2/path", clusterctlv1.CoreProviderType),
   577  			},
   578  			want:    "v0.4.2",
   579  			wantErr: false,
   580  		},
   581  		{
   582  			name: "Fails, when no release found",
   583  			field: field{
   584  				providerConfig: config.NewProvider("test", "https://github.com/o/r2/releases/v0.4.1/path", clusterctlv1.CoreProviderType),
   585  			},
   586  			want:    "",
   587  			wantErr: true,
   588  		},
   589  		{
   590  			name: "Falls back to latest prerelease when no official release present",
   591  			field: field{
   592  				providerConfig: config.NewProvider("test", "https://github.com/o/r3/releases/v0.1.0-alpha.2/path", clusterctlv1.CoreProviderType),
   593  			},
   594  			want:    "v0.1.0-alpha.2",
   595  			wantErr: false,
   596  		},
   597  	}
   598  	for _, tt := range tests {
   599  		t.Run(tt.name, func(t *testing.T) {
   600  			g := NewWithT(t)
   601  
   602  			ctx := context.Background()
   603  
   604  			resetCaches()
   605  
   606  			gRepo, err := NewGitHubRepository(ctx, tt.field.providerConfig, configVariablesClient, injectGoproxyClient(clientGoproxy))
   607  			g.Expect(err).ToNot(HaveOccurred())
   608  
   609  			got, err := latestRelease(ctx, gRepo)
   610  			if tt.wantErr {
   611  				g.Expect(err).To(HaveOccurred())
   612  				return
   613  			}
   614  			g.Expect(err).ToNot(HaveOccurred())
   615  			g.Expect(got).To(Equal(tt.want))
   616  			g.Expect(gRepo.(*gitHubRepository).defaultVersion).To(Equal(tt.want))
   617  		})
   618  	}
   619  }
   620  
   621  func Test_gitHubRepository_getLatestPatchRelease(t *testing.T) {
   622  	retryableOperationInterval = 200 * time.Millisecond
   623  	retryableOperationTimeout = 1 * time.Second
   624  	clientGoproxy, muxGoproxy, teardownGoproxy := newFakeGoproxy()
   625  	defer teardownGoproxy()
   626  
   627  	// Setup a handler for returning 4 fake releases.
   628  	muxGoproxy.HandleFunc("/github.com/o/r1/@v/list", func(w http.ResponseWriter, r *http.Request) {
   629  		testMethod(t, r, "GET")
   630  		fmt.Fprint(w, "v0.4.0\n")
   631  		fmt.Fprint(w, "v0.3.2\n")
   632  		fmt.Fprint(w, "v1.3.2\n")
   633  	})
   634  
   635  	major0 := uint(0)
   636  	minor3 := uint(3)
   637  	minor4 := uint(4)
   638  
   639  	configVariablesClient := test.NewFakeVariableClient()
   640  
   641  	type field struct {
   642  		providerConfig config.Provider
   643  	}
   644  	tests := []struct {
   645  		name    string
   646  		field   field
   647  		major   *uint
   648  		minor   *uint
   649  		want    string
   650  		wantErr bool
   651  	}{
   652  		{
   653  			name: "Get latest patch release, no Major/Minor specified",
   654  			field: field{
   655  				providerConfig: config.NewProvider("test", "https://github.com/o/r1/releases/v1.3.2/path", clusterctlv1.CoreProviderType),
   656  			},
   657  			minor:   nil,
   658  			major:   nil,
   659  			want:    "v1.3.2",
   660  			wantErr: false,
   661  		},
   662  		{
   663  			name: "Get latest patch release, for Major 0 and Minor 3",
   664  			field: field{
   665  				providerConfig: config.NewProvider("test", "https://github.com/o/r1/releases/v0.3.2/path", clusterctlv1.CoreProviderType),
   666  			},
   667  			major:   &major0,
   668  			minor:   &minor3,
   669  			want:    "v0.3.2",
   670  			wantErr: false,
   671  		},
   672  		{
   673  			name: "Get latest patch release, for Major 0 and Minor 4",
   674  			field: field{
   675  				providerConfig: config.NewProvider("test", "https://github.com/o/r1/releases/v0.4.0/path", clusterctlv1.CoreProviderType),
   676  			},
   677  			major:   &major0,
   678  			minor:   &minor4,
   679  			want:    "v0.4.0",
   680  			wantErr: false,
   681  		},
   682  	}
   683  	for _, tt := range tests {
   684  		t.Run(tt.name, func(t *testing.T) {
   685  			g := NewWithT(t)
   686  
   687  			ctx := context.Background()
   688  
   689  			resetCaches()
   690  
   691  			gRepo, err := NewGitHubRepository(ctx, tt.field.providerConfig, configVariablesClient, injectGoproxyClient(clientGoproxy))
   692  			g.Expect(err).ToNot(HaveOccurred())
   693  
   694  			got, err := latestPatchRelease(ctx, gRepo, tt.major, tt.minor)
   695  			if tt.wantErr {
   696  				g.Expect(err).To(HaveOccurred())
   697  				return
   698  			}
   699  			g.Expect(err).ToNot(HaveOccurred())
   700  			g.Expect(got).To(Equal(tt.want))
   701  		})
   702  	}
   703  }
   704  
   705  func Test_gitHubRepository_getReleaseByTag(t *testing.T) {
   706  	retryableOperationInterval = 200 * time.Millisecond
   707  	retryableOperationTimeout = 1 * time.Second
   708  	client, mux, teardown := test.NewFakeGitHub()
   709  	defer teardown()
   710  
   711  	providerConfig := config.NewProvider("test", "https://github.com/o/r/releases/v0.4.1/path", clusterctlv1.CoreProviderType)
   712  
   713  	// Setup a handler for returning a fake release.
   714  	mux.HandleFunc("/repos/o/r/releases/tags/foo", func(w http.ResponseWriter, r *http.Request) {
   715  		testMethod(t, r, "GET")
   716  		fmt.Fprint(w, `{"id":13, "tag_name": "v0.4.1"}`)
   717  	})
   718  
   719  	configVariablesClient := test.NewFakeVariableClient()
   720  
   721  	type args struct {
   722  		tag string
   723  	}
   724  	tests := []struct {
   725  		name        string
   726  		args        args
   727  		wantTagName *string
   728  		wantErr     bool
   729  	}{
   730  		{
   731  			name: "Return existing version",
   732  			args: args{
   733  				tag: "foo",
   734  			},
   735  			wantTagName: pointer.String("v0.4.1"),
   736  			wantErr:     false,
   737  		},
   738  		{
   739  			name: "Fails if version does not exists",
   740  			args: args{
   741  				tag: "bar",
   742  			},
   743  			wantTagName: nil,
   744  			wantErr:     true,
   745  		},
   746  	}
   747  	for _, tt := range tests {
   748  		t.Run(tt.name, func(t *testing.T) {
   749  			g := NewWithT(t)
   750  
   751  			ctx := context.Background()
   752  
   753  			resetCaches()
   754  
   755  			gRepo, err := NewGitHubRepository(ctx, providerConfig, configVariablesClient, injectGithubClient(client))
   756  			g.Expect(err).ToNot(HaveOccurred())
   757  
   758  			got, err := gRepo.(*gitHubRepository).getReleaseByTag(ctx, tt.args.tag)
   759  			if tt.wantErr {
   760  				g.Expect(err).To(HaveOccurred())
   761  				return
   762  			}
   763  			g.Expect(err).ToNot(HaveOccurred())
   764  
   765  			if tt.wantTagName == nil {
   766  				g.Expect(got).To(BeNil())
   767  				return
   768  			}
   769  
   770  			g.Expect(got.TagName).To(Equal(tt.wantTagName))
   771  		})
   772  	}
   773  }
   774  
   775  func Test_gitHubRepository_downloadFilesFromRelease(t *testing.T) {
   776  	retryableOperationInterval = 200 * time.Millisecond
   777  	retryableOperationTimeout = 1 * time.Second
   778  	client, mux, teardown := test.NewFakeGitHub()
   779  	defer teardown()
   780  
   781  	providerConfig := config.NewProvider("test", "https://github.com/o/r/releases/v0.4.1/file.yaml", clusterctlv1.CoreProviderType)                           // tree/main/path not relevant for the test
   782  	providerConfigWithRedirect := config.NewProvider("test", "https://github.com/o/r-with-redirect/releases/v0.4.1/file.yaml", clusterctlv1.CoreProviderType) // tree/main/path not relevant for the test
   783  
   784  	// Setup a handler for returning a fake release asset.
   785  	mux.HandleFunc("/repos/o/r/releases/assets/1", func(w http.ResponseWriter, r *http.Request) {
   786  		testMethod(t, r, "GET")
   787  		w.Header().Set("Content-Type", "application/octet-stream")
   788  		w.Header().Set("Content-Disposition", "attachment; filename=file.yaml")
   789  		fmt.Fprint(w, "content")
   790  	})
   791  	// Setup a handler which redirects to a different location.
   792  	mux.HandleFunc("/repos/o/r-with-redirect/releases/assets/1", func(w http.ResponseWriter, r *http.Request) {
   793  		testMethod(t, r, "GET")
   794  		http.Redirect(w, r, "/api-v3/repos/o/r/releases/assets/1", http.StatusFound)
   795  	})
   796  
   797  	configVariablesClient := test.NewFakeVariableClient()
   798  
   799  	var id1 int64 = 1
   800  	var id2 int64 = 2
   801  	tagName := "vO.3.3"
   802  	file := "file.yaml"
   803  
   804  	type args struct {
   805  		release  *github.RepositoryRelease
   806  		fileName string
   807  	}
   808  	tests := []struct {
   809  		name           string
   810  		args           args
   811  		providerConfig config.Provider
   812  		want           []byte
   813  		wantErr        bool
   814  	}{
   815  		{
   816  			name: "Pass if file exists",
   817  			args: args{
   818  				release: &github.RepositoryRelease{
   819  					TagName: &tagName,
   820  					Assets: []*github.ReleaseAsset{
   821  						{
   822  							ID:   &id1,
   823  							Name: &file,
   824  						},
   825  					},
   826  				},
   827  				fileName: file,
   828  			},
   829  			providerConfig: providerConfig,
   830  			want:           []byte("content"),
   831  			wantErr:        false,
   832  		},
   833  		{
   834  			name: "Pass if file exists with redirect",
   835  			args: args{
   836  				release: &github.RepositoryRelease{
   837  					TagName: &tagName,
   838  					Assets: []*github.ReleaseAsset{
   839  						{
   840  							ID:   &id1,
   841  							Name: &file,
   842  						},
   843  					},
   844  				},
   845  				fileName: file,
   846  			},
   847  			providerConfig: providerConfigWithRedirect,
   848  			want:           []byte("content"),
   849  			wantErr:        false,
   850  		},
   851  		{
   852  			name: "Fails if file does not exists",
   853  			args: args{
   854  				release: &github.RepositoryRelease{
   855  					TagName: &tagName,
   856  					Assets: []*github.ReleaseAsset{
   857  						{
   858  							ID:   &id1,
   859  							Name: &file,
   860  						},
   861  					},
   862  				},
   863  				fileName: "another file",
   864  			},
   865  			providerConfig: providerConfig,
   866  			wantErr:        true,
   867  		},
   868  		{
   869  			name: "Fails if file does not exists",
   870  			args: args{
   871  				release: &github.RepositoryRelease{
   872  					TagName: &tagName,
   873  					Assets: []*github.ReleaseAsset{
   874  						{
   875  							ID:   &id2, // id does not match any file (this should not happen)
   876  							Name: &file,
   877  						},
   878  					},
   879  				},
   880  				fileName: "another file",
   881  			},
   882  			providerConfig: providerConfig,
   883  			wantErr:        true,
   884  		},
   885  	}
   886  	for _, tt := range tests {
   887  		t.Run(tt.name, func(t *testing.T) {
   888  			g := NewWithT(t)
   889  			resetCaches()
   890  
   891  			gRepo, err := NewGitHubRepository(context.Background(), tt.providerConfig, configVariablesClient, injectGithubClient(client))
   892  			g.Expect(err).ToNot(HaveOccurred())
   893  
   894  			got, err := gRepo.(*gitHubRepository).downloadFilesFromRelease(context.Background(), tt.args.release, tt.args.fileName)
   895  			if tt.wantErr {
   896  				g.Expect(err).To(HaveOccurred())
   897  				return
   898  			}
   899  
   900  			g.Expect(err).ToNot(HaveOccurred())
   901  			g.Expect(got).To(Equal(tt.want))
   902  		})
   903  	}
   904  }
   905  
   906  func testMethod(t *testing.T, r *http.Request, want string) {
   907  	t.Helper()
   908  
   909  	if got := r.Method; got != want {
   910  		t.Errorf("Request method: %v, want %v", got, want)
   911  	}
   912  }
   913  
   914  // resetCaches is called repeatedly throughout tests to help avoid cross-test pollution.
   915  func resetCaches() {
   916  	cacheVersions = map[string][]string{}
   917  	cacheReleases = map[string]*github.RepositoryRelease{}
   918  	cacheFiles = map[string][]byte{}
   919  }
   920  
   921  // newFakeGoproxy sets up a test HTTP server along with a github.Client that is
   922  // configured to talk to that test server. Tests should register handlers on
   923  // mux which provide mock responses for the API method being tested.
   924  func newFakeGoproxy() (client *goproxy.Client, mux *http.ServeMux, teardown func()) {
   925  	// mux is the HTTP request multiplexer used with the test server.
   926  	mux = http.NewServeMux()
   927  
   928  	apiHandler := http.NewServeMux()
   929  	apiHandler.Handle("/", mux)
   930  
   931  	// server is a test HTTP server used to provide mock API responses.
   932  	server := httptest.NewServer(apiHandler)
   933  
   934  	// client is the GitHub client being tested and is configured to use test server.
   935  	url, _ := url.Parse(server.URL + "/")
   936  	return goproxy.NewClient(url.Scheme, url.Host), mux, server.Close
   937  }