fat: add template function randomKubernetesName (#48)

This commit is contained in:
Rick 2023-04-24 12:20:49 +08:00 committed by GitHub
parent 607818fd4b
commit 9df7a6663a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 196 additions and 45 deletions

View File

@ -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:

View File

@ -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())

View File

@ -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)
}
})
}
}

View File

@ -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
}

View File

@ -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,
}}

70
pkg/util/rand.go Normal file
View File

@ -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 - 1
// No. of random letters we can extract from a single int63.
maxAlphanumsPerInt = 63 / alphanumsIdxBits
)
// String generates a random alphanumeric string, without vowels, which is n
// characters long. This will panic if n is less than zero.
// How the random string is created:
// - we generate random int63's
// - from each int63, we are extracting multiple random letters by bit-shifting and masking
// - if some index is out of range of alphanums we neglect it (unlikely to happen multiple times in a row)
func String(n int) string {
b := make([]byte, n)
rng.Lock()
defer rng.Unlock()
randomInt63 := rng.rand.Int63()
remaining := maxAlphanumsPerInt
for i := 0; i < n; {
if remaining == 0 {
randomInt63, remaining = rng.rand.Int63(), maxAlphanumsPerInt
}
if idx := int(randomInt63 & alphanumsIdxMask); idx < len(alphanums) {
b[i] = alphanums[idx]
i++
}
randomInt63 >>= alphanumsIdxBits
remaining--
}
return string(b)
}

54
pkg/util/rand_test.go Normal file
View File

@ -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)
}
}

View File

@ -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