From 9df7a6663a0c0c83a818b3ff0f9eee60d92c3363 Mon Sep 17 00:00:00 2001 From: Rick <1450685+LinuxSuRen@users.noreply.github.com> Date: Mon, 24 Apr 2023 12:20:49 +0800 Subject: [PATCH] fat: add template function randomKubernetesName (#48) --- README.md | 8 +++++ pkg/render/template.go | 9 ++++- pkg/render/template_test.go | 14 +++++++- pkg/runner/simple.go | 6 ++-- pkg/runner/simple_test.go | 68 ++++++++++++++++++----------------- pkg/util/rand.go | 70 +++++++++++++++++++++++++++++++++++++ pkg/util/rand_test.go | 54 ++++++++++++++++++++++++++++ sample/kubernetes.yaml | 12 +++---- 8 files changed, 196 insertions(+), 45 deletions(-) create mode 100644 pkg/util/rand.go create mode 100644 pkg/util/rand_test.go diff --git a/README.md b/README.md index 857177db..86544ca9 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,14 @@ The following fields are templated with [sprig](http://masterminds.github.io/spr * Request Body * Request Header +### Functions + +You could use all the common functions which comes from [sprig](http://masterminds.github.io/sprig/). Besides some specific functions are available: + +| Name | Usage | +|---|---| +| `randomKubernetesName` | `{{randomKubernetesName}}` to generate Kubernetes resource name randomly, the name will have 8 chars | + ## Verify against Kubernetes It could verify any kinds of Kubernetes resources. Please set the environment variables before using it: diff --git a/pkg/render/template.go b/pkg/render/template.go index 84927ec3..effca169 100644 --- a/pkg/render/template.go +++ b/pkg/render/template.go @@ -6,12 +6,19 @@ import ( "strings" "github.com/Masterminds/sprig/v3" + "github.com/linuxsuren/api-testing/pkg/util" ) // Render render then return the result func Render(name, text string, ctx interface{}) (result string, err error) { var tpl *template.Template - if tpl, err = template.New(name).Funcs(sprig.FuncMap()).Parse(text); err == nil { + if tpl, err = template.New(name). + Funcs(sprig.FuncMap()). + Funcs(template.FuncMap{ + "randomKubernetesName": func() string { + return util.String(8) + }, + }).Parse(text); err == nil { buf := new(bytes.Buffer) if err = tpl.Execute(buf, ctx); err == nil { result = strings.TrimSpace(buf.String()) diff --git a/pkg/render/template_test.go b/pkg/render/template_test.go index 182df133..812d2c20 100644 --- a/pkg/render/template_test.go +++ b/pkg/render/template_test.go @@ -12,6 +12,7 @@ func TestRender(t *testing.T) { text string ctx interface{} expect string + verify func(*testing.T, string) }{{ name: "default", text: `{{default "hello" .Bar}}`, @@ -22,12 +23,23 @@ func TestRender(t *testing.T) { text: `{{trim " hello "}}`, ctx: "", expect: "hello", + }, { + name: "randomKubernetesName", + text: `{{randomKubernetesName}}`, + verify: func(t *testing.T, s string) { + assert.Equal(t, 8, len(s)) + }, }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := Render(tt.name, tt.text, tt.ctx) assert.Nil(t, err) - assert.Equal(t, tt.expect, result) + if tt.expect != "" { + assert.Equal(t, tt.expect, result) + } + if tt.verify != nil { + tt.verify(t, result) + } }) } } diff --git a/pkg/runner/simple.go b/pkg/runner/simple.go index d9aa28d9..22025354 100644 --- a/pkg/runner/simple.go +++ b/pkg/runner/simple.go @@ -197,12 +197,12 @@ func (r *simpleTestCaseRunner) RunTestCase(testcase *testing.TestCase, dataConte }, } - var requestBody io.Reader - if requestBody, err = testcase.Request.GetBody(); err != nil { + if err = testcase.Request.Render(dataContext); err != nil { return } - if err = testcase.Request.Render(dataContext); err != nil { + var requestBody io.Reader + if requestBody, err = testcase.Request.GetBody(); err != nil { return } diff --git a/pkg/runner/simple_test.go b/pkg/runner/simple_test.go index b4dbc8af..049952cc 100644 --- a/pkg/runner/simple_test.go +++ b/pkg/runner/simple_test.go @@ -21,6 +21,17 @@ func TestTestCase(t *testing.T) { fooRequst := atest.Request{ API: urlFoo, } + defaultForm := map[string]string{ + "key": "value", + } + defaultPrepare := func() { + gock.New(urlLocalhost). + Get("/foo").Reply(http.StatusOK).BodyString(`{"items":[]}`) + } + defaultPostPrepare := func() { + gock.New(urlLocalhost). + Post("/foo").Reply(http.StatusOK).BodyString(`{"items":[]}`) + } tests := []struct { name string @@ -41,11 +52,9 @@ func TestTestCase(t *testing.T) { name: "normal, response is map", testCase: &atest.TestCase{ Request: atest.Request{ - API: urlFoo, - Header: map[string]string{ - "key": "value", - }, - Body: `{"foo":"bar"}`, + API: urlFoo, + Header: defaultForm, + Body: `{"foo":"bar"}`, }, Expect: atest.Response{ StatusCode: http.StatusOK, @@ -204,10 +213,7 @@ func TestTestCase(t *testing.T) { }, }, }, - prepare: func() { - gock.New(urlLocalhost). - Get("/foo").Reply(http.StatusOK).BodyString(`{"items":[]}`) - }, + prepare: defaultPrepare, verify: func(t *testing.T, output interface{}, err error) { assert.NotNil(t, err) assert.Contains(t, err.Error(), "failed to get field") @@ -225,10 +231,7 @@ func TestTestCase(t *testing.T) { // }, // }, // }, - // prepare: func() { - // gock.New(urlLocalhost). - // Get("/foo").Reply(http.StatusOK).BodyString(`{"items":[]}`) - // }, + // prepare: defaultPrepare, // verify: func(t *testing.T, output interface{}, err error) { // if assert.NotNil(t, err) { // assert.Contains(t, err.Error(), "failed to verify") @@ -245,10 +248,7 @@ func TestTestCase(t *testing.T) { }, }, }, - prepare: func() { - gock.New(urlLocalhost). - Get("/foo").Reply(http.StatusOK).BodyString(`{"items":[]}`) - }, + prepare: defaultPrepare, verify: func(t *testing.T, output interface{}, err error) { assert.NotNil(t, err) assert.Contains(t, err.Error(), "unknown name println") @@ -263,10 +263,7 @@ func TestTestCase(t *testing.T) { }, }, }, - prepare: func() { - gock.New(urlLocalhost). - Get("/foo").Reply(http.StatusOK).BodyString(`{"items":[]}`) - }, + prepare: defaultPrepare, verify: func(t *testing.T, output interface{}, err error) { assert.NotNil(t, err) assert.Contains(t, err.Error(), "expected bool, but got int") @@ -303,16 +300,11 @@ func TestTestCase(t *testing.T) { Header: map[string]string{ util.ContentType: "multipart/form-data", }, - Form: map[string]string{ - "key": "value", - }, + Form: defaultForm, }, }, - prepare: func() { - gock.New(urlLocalhost). - Post("/foo").Reply(http.StatusOK).BodyString(`{"items":[]}`) - }, - verify: noError, + prepare: defaultPostPrepare, + verify: noError, }, { name: "normal form request", testCase: &atest.TestCase{ @@ -322,14 +314,24 @@ func TestTestCase(t *testing.T) { Header: map[string]string{ util.ContentType: "application/x-www-form-urlencoded", }, - Form: map[string]string{ - "key": "value", - }, + Form: defaultForm, + }, + }, + prepare: defaultPostPrepare, + verify: noError, + }, { + name: "body is a template", + testCase: &atest.TestCase{ + Request: atest.Request{ + API: urlFoo, + Method: http.MethodPost, + Body: `{"name":"{{lower "HELLO"}}"}`, }, }, prepare: func() { gock.New(urlLocalhost). - Post("/foo").Reply(http.StatusOK).BodyString(`{"items":[]}`) + Post("/foo").BodyString(`{"name":"hello"}`). + Reply(http.StatusOK).BodyString(`{}`) }, verify: noError, }} diff --git a/pkg/util/rand.go b/pkg/util/rand.go new file mode 100644 index 00000000..7b722b6f --- /dev/null +++ b/pkg/util/rand.go @@ -0,0 +1,70 @@ +/* +Copyright 2015 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package util provides utilities related to randomization. +package util + +import ( + "math/rand" + "sync" + "time" +) + +var rng = struct { + sync.Mutex + rand *rand.Rand +}{ + rand: rand.New(rand.NewSource(time.Now().UnixNano())), +} + +const ( + // We omit vowels from the set of available characters to reduce the chances + // of "bad words" being formed. + alphanums = "bcdfghjklmnpqrstvwxz2456789" + // No. of bits required to index into alphanums string. + alphanumsIdxBits = 5 + // Mask used to extract last alphanumsIdxBits of an int. + alphanumsIdxMask = 1<>= alphanumsIdxBits + remaining-- + } + return string(b) +} diff --git a/pkg/util/rand_test.go b/pkg/util/rand_test.go new file mode 100644 index 00000000..386e81e2 --- /dev/null +++ b/pkg/util/rand_test.go @@ -0,0 +1,54 @@ +/* +Copyright 2015 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "strings" + "testing" +) + +const ( + maxRangeTestCount = 500 + testStringLength = 32 +) + +func TestString(t *testing.T) { + valid := "bcdfghjklmnpqrstvwxz2456789" + for _, l := range []int{0, 1, 2, 10, 123} { + s := String(l) + if len(s) != l { + t.Errorf("expected string of size %d, got %q", l, s) + } + for _, c := range s { + if !strings.ContainsRune(valid, c) { + t.Errorf("expected valid characters, got %v", c) + } + } + } +} + +func BenchmarkRandomStringGeneration(b *testing.B) { + b.ResetTimer() + var s string + for i := 0; i < b.N; i++ { + s = String(testStringLength) + } + b.StopTimer() + if len(s) == 0 { + b.Fatal(s) + } +} diff --git a/sample/kubernetes.yaml b/sample/kubernetes.yaml index 9972daf0..9bce18f6 100644 --- a/sample/kubernetes.yaml +++ b/sample/kubernetes.yaml @@ -22,7 +22,7 @@ items: request: api: /api/v1/namespaces/default/configmaps header: - Authorization: Bearer {{env "K8S_TOKEN"}} + Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkRINXBRRi0zSURrbkRDWGhfVHpEaGFuOVdpcEVLSmFwYUI4Y1V5YjFpcUEifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJjbHVzdGVyLWFkbWluLXRva2VuLWtobnI0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImNsdXN0ZXItYWRtaW4iLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiJmZmNlODg0Ny0yZGY4LTQyMTktOGRjYS1mNGRlMWYzNWNmYzkiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6a3ViZS1zeXN0ZW06Y2x1c3Rlci1hZG1pbiJ9.YapUNL7aSlAzlZwDqcMF1-eNpaEs0ZPwybV1uM289fDk8RwjHpLQzVZV0IewaOCAjifwyTyqs1Vgd4nF9I7CYPv64cjMcVTQHCj_-pAxXjiYEM9LkR_b__WGsd-3Z0aRrdyO4WS7moRxZ4kz7ULd_OtlHpq-cFIQtytOaQSZNSbxpa5uP7g7y-uv0nwXBSwqZL9j5XimGlYyy999Q8Vc2GLDrDdVp69wuvToODQzJV44nfuA_dhUFQOzC4sE7Dkq7JarrvZspstqLo1ULzt_Z-cZ-qAu_pUaLHkoLZH5o97g4UF8AXeFYLj8YP_IBP9uhDrm829pNHU82N6Hn-80NQ method: POST body: | { @@ -30,7 +30,10 @@ items: "kind": "ConfigMap", "metadata": { "name": "config", - "namespace": "default" + "namespace": "default", + "labels": { + "key": "{{randomKubernetesName}}" + } }, "data": { "key": "value" @@ -56,8 +59,6 @@ items: "key": "new value" } } - expect: - statusCode: 200 - name: get-configmap request: api: /api/v1/namespaces/default/configmaps/config @@ -77,7 +78,6 @@ items: } } expect: - statusCode: 200 bodyFieldsExpect: "data/key": "new value" - name: delete-configmap @@ -86,5 +86,3 @@ items: header: Authorization: Bearer {{env "K8S_TOKEN"}} method: DELETE - expect: - statusCode: 200