sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/apptokenequalizer/app_token_equalizer_test.go (about) 1 /* 2 Copyright 2020 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 apptokenequalizer 18 19 import ( 20 "bytes" 21 "encoding/json" 22 "fmt" 23 "io" 24 "net/http" 25 "testing" 26 "time" 27 28 "github.com/google/go-cmp/cmp" 29 30 "sigs.k8s.io/prow/pkg/github" 31 ) 32 33 func TestRoundTrip(t *testing.T) { 34 const appID = "appid" 35 oneHourInFuture := time.Now().Add(time.Hour) 36 testCases := []struct { 37 name string 38 tokenCache map[string]github.AppInstallationToken 39 requestPath string 40 delegateResponse *http.Response 41 delegateError error 42 expectedResponse *http.Response 43 expectedError error 44 }{ 45 { 46 name: "Response is served from cache", 47 tokenCache: map[string]github.AppInstallationToken{appID: {Token: "token", ExpiresAt: oneHourInFuture}}, 48 requestPath: fmt.Sprintf("/app/installations/%s/tokens", appID), 49 delegateResponse: &http.Response{ 50 StatusCode: 201, 51 Header: http.Header{}, 52 Body: serializeOrDie(github.AppInstallationToken{Token: "other-token", ExpiresAt: time.Now().Add(time.Hour)}), 53 }, 54 expectedResponse: &http.Response{ 55 StatusCode: 201, 56 Body: serializeOrDie(github.AppInstallationToken{Token: "token", ExpiresAt: oneHourInFuture}), 57 }, 58 }, 59 { 60 name: "Delegate error is passed on and response is not from cache", 61 tokenCache: map[string]github.AppInstallationToken{appID: {Token: "token", ExpiresAt: oneHourInFuture}}, 62 requestPath: fmt.Sprintf("/app/installations/%s/tokens", appID), 63 delegateError: ComparableError("some-error"), 64 expectedError: ComparableError("some-error"), 65 }, 66 { 67 name: "Status is not 201, response is not from cache", 68 tokenCache: map[string]github.AppInstallationToken{appID: {Token: "token", ExpiresAt: oneHourInFuture}}, 69 requestPath: fmt.Sprintf("/app/installations/%s/tokens", appID), 70 delegateResponse: &http.Response{ 71 StatusCode: 200, 72 Header: http.Header{}, 73 Body: serializeOrDie(github.AppInstallationToken{Token: "other-token", ExpiresAt: oneHourInFuture}), 74 }, 75 expectedResponse: &http.Response{ 76 StatusCode: 200, 77 Body: serializeOrDie(github.AppInstallationToken{Token: "other-token", ExpiresAt: oneHourInFuture}), 78 }, 79 }, 80 } 81 82 for _, tc := range testCases { 83 t.Run(tc.name, func(t *testing.T) { 84 transport := &appTokenEqualizerTransport{ 85 tokenCache: tc.tokenCache, 86 delegate: &fakeRoundTripper{ 87 response: tc.delegateResponse, 88 err: tc.delegateError, 89 }, 90 } 91 92 r, err := http.NewRequest(http.MethodPost, tc.requestPath, nil) 93 if err != nil { 94 t.Fatalf("failed to construct request: %v", err) 95 } 96 97 response, err := transport.RoundTrip(r) 98 if diff := cmp.Diff(err, tc.expectedError); diff != "" { 99 t.Fatalf("actual error differs from expected: %s", diff) 100 } 101 if err != nil { 102 return 103 } 104 105 if diff := cmp.Diff(response.StatusCode, tc.expectedResponse.StatusCode); diff != "" { 106 t.Errorf("actual status code differs from expected: %s", diff) 107 } 108 109 actualBody, err := io.ReadAll(response.Body) 110 if err != nil { 111 t.Fatalf("failed to read actual response body: %v", err) 112 } 113 expectedBody, err := io.ReadAll(tc.expectedResponse.Body) 114 if err != nil { 115 t.Fatalf("failed to read expected response body: %v", err) 116 } 117 if diff := cmp.Diff(string(actualBody), string(expectedBody)); diff != "" { 118 t.Errorf("actual response differs from expectedResponse: %s", diff) 119 } 120 }) 121 } 122 } 123 124 func serializeOrDie(in interface{}) io.ReadCloser { 125 rawData, err := json.Marshal(in) 126 if err != nil { 127 panic(fmt.Sprintf("Serialization failed: %v", err)) 128 } 129 return io.NopCloser(bytes.NewBuffer(rawData)) 130 } 131 132 type fakeRoundTripper struct { 133 response *http.Response 134 err error 135 } 136 137 func (frt *fakeRoundTripper) RoundTrip(_ *http.Request) (*http.Response, error) { 138 return frt.response, frt.err 139 } 140 141 type ComparableError string 142 143 func (c ComparableError) Error() string { 144 return string(c) 145 }