...

Source file src/github.com/bradleyfalzon/ghinstallation/v2/transport_test.go

Documentation: github.com/bradleyfalzon/ghinstallation/v2

     1  package ghinstallation
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"net/http"
     9  	"net/http/httptest"
    10  	"os"
    11  	"strings"
    12  	"sync"
    13  	"testing"
    14  	"time"
    15  
    16  	"github.com/google/go-cmp/cmp"
    17  	"github.com/google/go-github/v45/github"
    18  )
    19  
    20  const (
    21  	installationID = 1
    22  	appID          = 2
    23  	token          = "abc123"
    24  )
    25  
    26  var key = []byte(`-----BEGIN RSA PRIVATE KEY-----
    27  MIIEpQIBAAKCAQEA0BUezcR7uycgZsfVLlAf4jXP7uFpVh4geSTY39RvYrAll0yh
    28  q7uiQypP2hjQJ1eQXZvkAZx0v9lBYJmX7e0HiJckBr8+/O2kARL+GTCJDJZECpjy
    29  97yylbzGBNl3s76fZ4CJ+4f11fCh7GJ3BJkMf9NFhe8g1TYS0BtSd/sauUQEuG/A
    30  3fOJxKTNmICZr76xavOQ8agA4yW9V5hKcrbHzkfecg/sQsPMmrXixPNxMsqyOMmg
    31  jdJ1aKr7ckEhd48ft4bPMO4DtVL/XFdK2wJZZ0gXJxWiT1Ny41LVql97Odm+OQyx
    32  tcayMkGtMb1nwTcVVl+RG2U5E1lzOYpcQpyYFQIDAQABAoIBAAfUY55WgFlgdYWo
    33  i0r81NZMNBDHBpGo/IvSaR6y/aX2/tMcnRC7NLXWR77rJBn234XGMeQloPb/E8iw
    34  vtjDDH+FQGPImnQl9P/dWRZVjzKcDN9hNfNAdG/R9JmGHUz0JUddvNNsIEH2lgEx
    35  C01u/Ntqdbk+cDvVlwuhm47MMgs6hJmZtS1KDPgYJu4IaB9oaZFN+pUyy8a1w0j9
    36  RAhHpZrsulT5ThgCra4kKGDNnk2yfI91N9lkP5cnhgUmdZESDgrAJURLS8PgInM4
    37  YPV9L68tJCO4g6k+hFiui4h/4cNXYkXnaZSBUoz28ICA6e7I3eJ6Y1ko4ou+Xf0V
    38  csM8VFkCgYEA7y21JfECCfEsTHwwDg0fq2nld4o6FkIWAVQoIh6I6o6tYREmuZ/1
    39  s81FPz/lvQpAvQUXGZlOPB9eW6bZZFytcuKYVNE/EVkuGQtpRXRT630CQiqvUYDZ
    40  4FpqdBQUISt8KWpIofndrPSx6JzI80NSygShQsScWFw2wBIQAnV3TpsCgYEA3reL
    41  L7AwlxCacsPvkazyYwyFfponblBX/OvrYUPPaEwGvSZmE5A/E4bdYTAixDdn4XvE
    42  ChwpmRAWT/9C6jVJ/o1IK25dwnwg68gFDHlaOE+B5/9yNuDvVmg34PWngmpucFb/
    43  6R/kIrF38lEfY0pRb05koW93uj1fj7Uiv+GWRw8CgYEAn1d3IIDQl+kJVydBKItL
    44  tvoEur/m9N8wI9B6MEjhdEp7bXhssSvFF/VAFeQu3OMQwBy9B/vfaCSJy0t79uXb
    45  U/dr/s2sU5VzJZI5nuDh67fLomMni4fpHxN9ajnaM0LyI/E/1FFPgqM+Rzb0lUQb
    46  yqSM/ptXgXJls04VRl4VjtMCgYEAprO/bLx2QjxdPpXGFcXbz6OpsC92YC2nDlsP
    47  3cfB0RFG4gGB2hbX/6eswHglLbVC/hWDkQWvZTATY2FvFps4fV4GrOt5Jn9+rL0U
    48  elfC3e81Dw+2z7jhrE1ptepprUY4z8Fu33HNcuJfI3LxCYKxHZ0R2Xvzo+UYSBqO
    49  ng0eTKUCgYEAxW9G4FjXQH0bjajntjoVQGLRVGWnteoOaQr/cy6oVii954yNMKSP
    50  rezRkSNbJ8cqt9XQS+NNJ6Xwzl3EbuAt6r8f8VO1TIdRgFOgiUXRVNZ3ZyW8Hegd
    51  kGTL0A6/0yAu9qQZlFbaD5bWhQo7eyx63u4hZGppBhkTSPikOYUPCH8=
    52  -----END RSA PRIVATE KEY-----`)
    53  
    54  func TestNew(t *testing.T) {
    55  	var authed bool
    56  	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    57  		if r.Header.Get("Accept") != acceptHeader {
    58  			t.Fatalf("Request URI %q accept header got %q want: %q", r.RequestURI, r.Header.Get("Accept"), acceptHeader)
    59  		}
    60  		switch r.RequestURI {
    61  		case fmt.Sprintf("/app/installations/%d/access_tokens", installationID):
    62  			// respond with any token to installation transport
    63  			js, _ := json.Marshal(accessToken{
    64  				Token:     token,
    65  				ExpiresAt: time.Now().Add(5 * time.Minute),
    66  			})
    67  			fmt.Fprintln(w, string(js))
    68  			authed = true
    69  		case "/auth/with/installation/token/endpoint":
    70  			if want := "token " + token; r.Header.Get("Authorization") != want {
    71  				t.Fatalf("Installation token got: %q want: %q", r.Header.Get("Authorization"), want)
    72  			}
    73  		default:
    74  			t.Fatalf("unexpected URI: %q", r.RequestURI)
    75  		}
    76  	}))
    77  	defer ts.Close()
    78  
    79  	tr, err := New(&http.Transport{}, appID, installationID, key)
    80  	if err != nil {
    81  		t.Fatal("unexpected error:", err)
    82  	}
    83  	tr.BaseURL = ts.URL
    84  
    85  	client := http.Client{Transport: tr}
    86  	_, err = client.Get(ts.URL + "/auth/with/installation/token/endpoint")
    87  	if err != nil {
    88  		t.Fatal("unexpected error from client:", err)
    89  	}
    90  
    91  	if !authed {
    92  		t.Fatal("Expected fetch of access_token but none occurred")
    93  	}
    94  
    95  	// Check the token is reused by setting expires into far future
    96  	tr.token.ExpiresAt = time.Now().Add(time.Hour)
    97  	authed = false
    98  
    99  	_, err = client.Get(ts.URL + "/auth/with/installation/token/endpoint")
   100  	if err != nil {
   101  		t.Fatal("unexpected error from client:", err)
   102  	}
   103  
   104  	if authed {
   105  		t.Fatal("Unexpected fetch of access_token")
   106  	}
   107  
   108  	// Check the token is refreshed by setting expires into far past
   109  	tr.token.ExpiresAt = time.Unix(0, 0)
   110  
   111  	_, err = client.Get(ts.URL + "/auth/with/installation/token/endpoint")
   112  	if err != nil {
   113  		t.Fatal("unexpected error from client:", err)
   114  	}
   115  
   116  	if !authed {
   117  		t.Fatal("Expected fetch of access_token but none occurred")
   118  	}
   119  }
   120  
   121  func TestNewKeyFromFile(t *testing.T) {
   122  	tmpfile, err := ioutil.TempFile("", "example")
   123  	if err != nil {
   124  		t.Fatal(err)
   125  	}
   126  	defer os.Remove(tmpfile.Name()) // clean up
   127  
   128  	if _, err := tmpfile.Write(key); err != nil {
   129  		t.Fatal(err)
   130  	}
   131  	if err := tmpfile.Close(); err != nil {
   132  		t.Fatal(err)
   133  	}
   134  
   135  	_, err = NewKeyFromFile(&http.Transport{}, appID, installationID, tmpfile.Name())
   136  	if err != nil {
   137  		t.Fatal("unexpected error:", err)
   138  	}
   139  }
   140  
   141  func TestNew_appendHeader(t *testing.T) {
   142  	var headers http.Header
   143  	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   144  		headers = r.Header
   145  		fmt.Fprintln(w, `{}`) // dummy response that looks like json
   146  	}))
   147  	defer ts.Close()
   148  
   149  	// Create a new request adding our own Accept header
   150  	myheader := "my-header"
   151  	req, err := http.NewRequest("GET", ts.URL+"/auth/with/installation/token/endpoint", nil)
   152  	if err != nil {
   153  		t.Fatal("unexpected error from http.NewRequest:", err)
   154  	}
   155  	req.Header.Add("Accept", myheader)
   156  
   157  	tr, err := New(&http.Transport{}, appID, installationID, key)
   158  	if err != nil {
   159  		t.Fatal("unexpected error:", err)
   160  	}
   161  	tr.BaseURL = ts.URL
   162  
   163  	client := http.Client{Transport: tr}
   164  	_, err = client.Do(req)
   165  	if err != nil {
   166  		t.Fatal("unexpected error from client:", err)
   167  	}
   168  
   169  	found := false
   170  	for _, v := range headers["Accept"] {
   171  		if v == myheader {
   172  			found = true
   173  			break
   174  		}
   175  	}
   176  
   177  	if !found {
   178  		t.Errorf("could not find %v in request's accept headers: %v", myheader, headers["Accept"])
   179  	}
   180  }
   181  
   182  func TestRefreshTokenWithParameters(t *testing.T) {
   183  	installationTokenOptions := &github.InstallationTokenOptions{
   184  		RepositoryIDs: []int64{1234},
   185  		Permissions: &github.InstallationPermissions{
   186  			Contents: github.String("write"),
   187  			Issues:   github.String("read"),
   188  		},
   189  	}
   190  
   191  	// Convert InstallationTokenOptions into a ReadWriter to pass as an argument to http.NewRequest.
   192  	body, err := GetReadWriter(installationTokenOptions)
   193  	if err != nil {
   194  		t.Fatalf("error calling GetReadWriter: %v", err)
   195  	}
   196  
   197  	// Convert io.ReadWriter to String without deleting body data.
   198  	wantBody, _ := GetReadWriter(installationTokenOptions)
   199  	wantBodyBytes := new(bytes.Buffer)
   200  	wantBodyBytes.ReadFrom(wantBody)
   201  	wantBodyString := wantBodyBytes.String()
   202  
   203  	roundTripper := RoundTrip{
   204  		rt: func(req *http.Request) (*http.Response, error) {
   205  			// Convert io.ReadCloser to String without deleting body data.
   206  			var gotBodyBytes []byte
   207  			gotBodyBytes, _ = ioutil.ReadAll(req.Body)
   208  			req.Body = ioutil.NopCloser(bytes.NewBuffer(gotBodyBytes))
   209  			gotBodyString := string(gotBodyBytes)
   210  
   211  			// Compare request sent with request received.
   212  			if diff := cmp.Diff(wantBodyString, gotBodyString); diff != "" {
   213  				t.Errorf("HTTP body want->got: %s", diff)
   214  			}
   215  
   216  			// Return acceptable access token.
   217  			accessToken := accessToken{
   218  				Token:     "token_string",
   219  				ExpiresAt: time.Now(),
   220  				Repositories: []github.Repository{{
   221  					ID: github.Int64(1234),
   222  				}},
   223  				Permissions: github.InstallationPermissions{
   224  					Contents: github.String("write"),
   225  					Issues:   github.String("read"),
   226  				},
   227  			}
   228  			tokenReadWriter, err := GetReadWriter(accessToken)
   229  			if err != nil {
   230  				return nil, fmt.Errorf("error converting token into io.ReadWriter: %+v", err)
   231  			}
   232  			tokenBody := ioutil.NopCloser(tokenReadWriter)
   233  			return &http.Response{
   234  				Body:       tokenBody,
   235  				StatusCode: 200,
   236  			}, nil
   237  		},
   238  	}
   239  
   240  	tr, err := New(roundTripper, appID, installationID, key)
   241  	if err != nil {
   242  		t.Fatal("unexpected error:", err)
   243  	}
   244  	tr.InstallationTokenOptions = installationTokenOptions
   245  
   246  	req, err := http.NewRequest("POST", fmt.Sprintf("%s/app/installations/%v/access_tokens", tr.BaseURL, tr.installationID), body)
   247  	if err != nil {
   248  		t.Fatal("unexpected error:", err)
   249  	}
   250  	if _, err := tr.RoundTrip(req); err != nil {
   251  		t.Fatalf("error calling RoundTrip: %v", err)
   252  	}
   253  }
   254  
   255  func TestRefreshTokenWithTrailingSlashBaseURL(t *testing.T) {
   256  	installationTokenOptions := &github.InstallationTokenOptions{
   257  		RepositoryIDs: []int64{1234},
   258  		Permissions: &github.InstallationPermissions{
   259  			Contents: github.String("write"),
   260  			Issues:   github.String("read"),
   261  		},
   262  	}
   263  
   264  	tokenToBe := "token_string"
   265  
   266  	// Convert io.ReadWriter to String without deleting body data.
   267  	wantBody, _ := GetReadWriter(installationTokenOptions)
   268  	wantBodyBytes := new(bytes.Buffer)
   269  	wantBodyBytes.ReadFrom(wantBody)
   270  	wantBodyString := wantBodyBytes.String()
   271  
   272  	roundTripper := RoundTrip{
   273  		rt: func(req *http.Request) (*http.Response, error) {
   274  			if strings.Contains(req.URL.Path, "//") {
   275  				return &http.Response{
   276  					Body:       ioutil.NopCloser(strings.NewReader("Forbidden\n")),
   277  					StatusCode: 401,
   278  				}, fmt.Errorf("Got simulated 401 Github Forbidden response")
   279  			}
   280  
   281  			if req.URL.Path == "test_endpoint/" && req.Header.Get("Authorization") == fmt.Sprintf("token %s", tokenToBe) {
   282  				return &http.Response{
   283  					Body:       ioutil.NopCloser(strings.NewReader("Beautiful\n")),
   284  					StatusCode: 200,
   285  				}, nil
   286  			}
   287  
   288  			// Convert io.ReadCloser to String without deleting body data.
   289  			var gotBodyBytes []byte
   290  			gotBodyBytes, _ = ioutil.ReadAll(req.Body)
   291  			req.Body = ioutil.NopCloser(bytes.NewBuffer(gotBodyBytes))
   292  			gotBodyString := string(gotBodyBytes)
   293  
   294  			// Compare request sent with request received.
   295  			if diff := cmp.Diff(wantBodyString, gotBodyString); diff != "" {
   296  				t.Errorf("HTTP body want->got: %s", diff)
   297  			}
   298  
   299  			// Return acceptable access token.
   300  			accessToken := accessToken{
   301  				Token:     tokenToBe,
   302  				ExpiresAt: time.Now(),
   303  				Repositories: []github.Repository{{
   304  					ID: github.Int64(1234),
   305  				}},
   306  				Permissions: github.InstallationPermissions{
   307  					Contents: github.String("write"),
   308  					Issues:   github.String("read"),
   309  				},
   310  			}
   311  			tokenReadWriter, err := GetReadWriter(accessToken)
   312  			if err != nil {
   313  				return nil, fmt.Errorf("error converting token into io.ReadWriter: %+v", err)
   314  			}
   315  			tokenBody := ioutil.NopCloser(tokenReadWriter)
   316  			return &http.Response{
   317  				Body:       tokenBody,
   318  				StatusCode: 200,
   319  			}, nil
   320  		},
   321  	}
   322  
   323  	tr, err := New(roundTripper, appID, installationID, key)
   324  	if err != nil {
   325  		t.Fatal("unexpected error:", err)
   326  	}
   327  	tr.InstallationTokenOptions = installationTokenOptions
   328  	tr.BaseURL = "http://localhost/github/api/v3/"
   329  
   330  	// Convert InstallationTokenOptions into a ReadWriter to pass as an argument to http.NewRequest.
   331  	body, err := GetReadWriter(installationTokenOptions)
   332  	if err != nil {
   333  		t.Fatalf("error calling GetReadWriter: %v", err)
   334  	}
   335  
   336  	req, err := http.NewRequest("POST", "http://localhost/test_endpoint/", body)
   337  	if err != nil {
   338  		t.Fatal("unexpected error:", err)
   339  	}
   340  	res, err := tr.RoundTrip(req)
   341  	if err != nil {
   342  		t.Fatalf("error calling RoundTrip: %v", err)
   343  	}
   344  	if res.StatusCode != 200 {
   345  		t.Fatalf("Unexpected RoundTrip response code: %d", res.StatusCode)
   346  	}
   347  }
   348  
   349  func TestRoundTripperContract(t *testing.T) {
   350  	tr := &Transport{
   351  		token: &accessToken{
   352  			ExpiresAt: time.Now().Add(1 * time.Hour),
   353  			Token:     "42",
   354  		},
   355  		mu: &sync.Mutex{},
   356  		tr: roundTripperFunc(func(req *http.Request) (*http.Response, error) {
   357  			if auth := req.Header.Get("Authorization"); auth != "token 42" {
   358  				t.Errorf("got unexpected Authorization request header in parent RoundTripper: %q", auth)
   359  			}
   360  			return nil, nil
   361  		}),
   362  	}
   363  	req, err := http.NewRequest("GET", "http://localhost", nil)
   364  	if err != nil {
   365  		t.Fatal(err)
   366  	}
   367  	req.Header.Set("Authorization", "xxx")
   368  	_, err = tr.RoundTrip(req)
   369  	if err != nil {
   370  		t.Fatal(err)
   371  	}
   372  	if accept := req.Header.Get("Authorization"); accept != "xxx" {
   373  		t.Errorf("got unexpected Authorization request header in caller: %q", accept)
   374  	}
   375  }
   376  
   377  type roundTripperFunc func(*http.Request) (*http.Response, error)
   378  
   379  func (fn roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {
   380  	return fn(req)
   381  }
   382  

View as plain text