sigs.k8s.io/cluster-api-provider-azure@v1.14.3/azure/converters/futures_test.go (about) 1 /* 2 Copyright 2021 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 converters 18 19 import ( 20 "encoding/base64" 21 "io" 22 "net/http" 23 "net/url" 24 "strings" 25 "testing" 26 27 "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" 28 . "github.com/onsi/gomega" 29 infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" 30 ) 31 32 var ( 33 validFuture = infrav1.Future{ 34 Type: infrav1.DeleteFuture, 35 ServiceName: "test-service", 36 Name: "test-group", 37 ResourceGroup: "test-group", 38 Data: "eyJtZXRob2QiOiJERUxFVEUiLCJwb2xsaW5nTWV0aG9kIjoiTG9jYXRpb24iLCJscm9TdGF0ZSI6IkluUHJvZ3Jlc3MifQ==", 39 } 40 41 emptyDataFuture = infrav1.Future{ 42 Type: infrav1.DeleteFuture, 43 ServiceName: "test-service", 44 Name: "test-group", 45 ResourceGroup: "test-group", 46 Data: "", 47 } 48 49 decodedDataFuture = infrav1.Future{ 50 Type: infrav1.DeleteFuture, 51 ServiceName: "test-service", 52 Name: "test-group", 53 ResourceGroup: "test-group", 54 Data: "this is not b64 encoded", 55 } 56 57 invalidFuture = infrav1.Future{ 58 Type: infrav1.DeleteFuture, 59 ServiceName: "test-service", 60 Name: "test-group", 61 ResourceGroup: "test-group", 62 Data: "ZmFrZSBiNjQgZnV0dXJlIGRhdGEK", 63 } 64 ) 65 66 func TestPollerToFuture(t *testing.T) { 67 cases := []struct { 68 name string 69 futureType string 70 statusCode int 71 expectedErr string 72 }{ 73 { 74 name: "valid DELETE poller", 75 futureType: infrav1.DeleteFuture, 76 statusCode: http.StatusAccepted, 77 }, 78 { 79 name: "invalid DELETE poller", 80 futureType: infrav1.DeleteFuture, 81 statusCode: http.StatusNoContent, 82 expectedErr: "failed to get resume token", 83 }, 84 { 85 name: "valid PUT poller", 86 futureType: infrav1.PutFuture, 87 statusCode: http.StatusAccepted, 88 }, 89 { 90 name: "invalid PUT poller", 91 futureType: infrav1.PutFuture, 92 statusCode: http.StatusNoContent, 93 expectedErr: "failed to get resume token", 94 }, 95 } 96 97 for _, c := range cases { 98 c := c 99 t.Run(c.name, func(t *testing.T) { 100 t.Parallel() 101 g := NewGomegaWithT(t) 102 poller := fakePoller[MockPolled](g, c.statusCode) 103 future, err := PollerToFuture(poller, c.futureType, "test-service", "test-resource", "test-group") 104 if c.expectedErr != "" { 105 g.Expect(err).To(HaveOccurred()) 106 g.Expect(err.Error()).To(ContainSubstring(c.expectedErr)) 107 } else { 108 g.Expect(err).NotTo(HaveOccurred()) 109 g.Expect(future).NotTo(BeNil()) 110 g.Expect(future.Data).NotTo(BeNil()) 111 token, err := base64.URLEncoding.DecodeString(future.Data) 112 g.Expect(err).NotTo(HaveOccurred()) 113 g.Expect(string(token)).NotTo(BeEmpty()) 114 // The following assertion should pass, but the token's format could change. 115 // g.Expect(string(token)).To(MatchJSON(`{"type":"MockPolled","token":{"type":"body","pollURL":"/","state":"InProgress"}}`)) 116 } 117 }) 118 } 119 } 120 121 func TestFutureToResumeToken(t *testing.T) { 122 cases := []struct { 123 name string 124 future infrav1.Future 125 expect func(*GomegaWithT, string, error) 126 }{ 127 { 128 name: "data is empty", 129 future: emptyDataFuture, 130 expect: func(g *GomegaWithT, token string, err error) { 131 g.Expect(err).To(HaveOccurred()) 132 g.Expect(err.Error()).Should(ContainSubstring("failed to unmarshal future data")) 133 }, 134 }, 135 { 136 name: "data is not base64-encoded", 137 future: decodedDataFuture, 138 expect: func(g *GomegaWithT, token string, err error) { 139 g.Expect(err).To(HaveOccurred()) 140 g.Expect(err.Error()).Should(ContainSubstring("failed to decode future data")) 141 }, 142 }, 143 { 144 name: "data does not contain a valid resume token", 145 future: invalidFuture, 146 expect: func(g *GomegaWithT, token string, err error) { 147 // "The token's format should be considered opaque and is subject to change." 148 // This validates decoding the unit test data, but actual SDKv2 tokens won't look like this. 149 g.Expect(err).NotTo(HaveOccurred()) 150 g.Expect(token).To(ContainSubstring("fake b64 future data")) 151 }, 152 }, 153 { 154 name: "data contains a valid resume token", 155 future: validFuture, 156 expect: func(g *GomegaWithT, token string, err error) { 157 // "The token's format should be considered opaque and is subject to change." 158 // This validates decoding the unit test data, but actual SDKv2 tokens won't look like this. 159 g.Expect(err).NotTo(HaveOccurred()) 160 g.Expect(token).To(MatchJSON(`{"method":"DELETE","pollingMethod":"Location","lroState":"InProgress"}`)) 161 }, 162 }, 163 } 164 165 for _, c := range cases { 166 c := c 167 t.Run(c.name, func(t *testing.T) { 168 t.Parallel() 169 g := NewGomegaWithT(t) 170 token, err := FutureToResumeToken(c.future) 171 c.expect(g, token, err) 172 }) 173 } 174 } 175 176 type MockPolled struct{} 177 178 func (m *MockPolled) Done() bool { return true } 179 180 func fakePoller[T any](g *GomegaWithT, statusCode int) *runtime.Poller[T] { 181 response := &http.Response{ 182 Body: io.NopCloser(strings.NewReader("")), 183 Request: &http.Request{ 184 Method: http.MethodPut, 185 URL: &url.URL{Path: "/"}, 186 }, 187 StatusCode: statusCode, 188 } 189 pipeline := runtime.NewPipeline("testmodule", "v0.1.0", runtime.PipelineOptions{}, nil) 190 poller, err := runtime.NewPoller[T](response, pipeline, nil) 191 g.Expect(err).NotTo(HaveOccurred()) 192 return poller 193 }