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  }