Skip to content

Commit 9df7a66

Browse files
authoredApr 24, 2023
fat: add template function randomKubernetesName (#48)
1 parent 607818f commit 9df7a66

File tree

8 files changed

+196
-45
lines changed

8 files changed

+196
-45
lines changed
 

‎README.md

+8
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@ The following fields are templated with [sprig](http://masterminds.github.io/spr
6363
* Request Body
6464
* Request Header
6565
66+
### Functions
67+
68+
You could use all the common functions which comes from [sprig](http://masterminds.github.io/sprig/). Besides some specific functions are available:
69+
70+
| Name | Usage |
71+
|---|---|
72+
| `randomKubernetesName` | `{{randomKubernetesName}}` to generate Kubernetes resource name randomly, the name will have 8 chars |
73+
6674
## Verify against Kubernetes
6775
6876
It could verify any kinds of Kubernetes resources. Please set the environment variables before using it:

‎pkg/render/template.go

+8-1
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,19 @@ import (
66
"strings"
77

88
"github.com/Masterminds/sprig/v3"
9+
"github.com/linuxsuren/api-testing/pkg/util"
910
)
1011

1112
// Render render then return the result
1213
func Render(name, text string, ctx interface{}) (result string, err error) {
1314
var tpl *template.Template
14-
if tpl, err = template.New(name).Funcs(sprig.FuncMap()).Parse(text); err == nil {
15+
if tpl, err = template.New(name).
16+
Funcs(sprig.FuncMap()).
17+
Funcs(template.FuncMap{
18+
"randomKubernetesName": func() string {
19+
return util.String(8)
20+
},
21+
}).Parse(text); err == nil {
1522
buf := new(bytes.Buffer)
1623
if err = tpl.Execute(buf, ctx); err == nil {
1724
result = strings.TrimSpace(buf.String())

‎pkg/render/template_test.go

+13-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ func TestRender(t *testing.T) {
1212
text string
1313
ctx interface{}
1414
expect string
15+
verify func(*testing.T, string)
1516
}{{
1617
name: "default",
1718
text: `{{default "hello" .Bar}}`,
@@ -22,12 +23,23 @@ func TestRender(t *testing.T) {
2223
text: `{{trim " hello "}}`,
2324
ctx: "",
2425
expect: "hello",
26+
}, {
27+
name: "randomKubernetesName",
28+
text: `{{randomKubernetesName}}`,
29+
verify: func(t *testing.T, s string) {
30+
assert.Equal(t, 8, len(s))
31+
},
2532
}}
2633
for _, tt := range tests {
2734
t.Run(tt.name, func(t *testing.T) {
2835
result, err := Render(tt.name, tt.text, tt.ctx)
2936
assert.Nil(t, err)
30-
assert.Equal(t, tt.expect, result)
37+
if tt.expect != "" {
38+
assert.Equal(t, tt.expect, result)
39+
}
40+
if tt.verify != nil {
41+
tt.verify(t, result)
42+
}
3143
})
3244
}
3345
}

‎pkg/runner/simple.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -197,12 +197,12 @@ func (r *simpleTestCaseRunner) RunTestCase(testcase *testing.TestCase, dataConte
197197
},
198198
}
199199

200-
var requestBody io.Reader
201-
if requestBody, err = testcase.Request.GetBody(); err != nil {
200+
if err = testcase.Request.Render(dataContext); err != nil {
202201
return
203202
}
204203

205-
if err = testcase.Request.Render(dataContext); err != nil {
204+
var requestBody io.Reader
205+
if requestBody, err = testcase.Request.GetBody(); err != nil {
206206
return
207207
}
208208

‎pkg/runner/simple_test.go

+35-33
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,17 @@ func TestTestCase(t *testing.T) {
2121
fooRequst := atest.Request{
2222
API: urlFoo,
2323
}
24+
defaultForm := map[string]string{
25+
"key": "value",
26+
}
27+
defaultPrepare := func() {
28+
gock.New(urlLocalhost).
29+
Get("/foo").Reply(http.StatusOK).BodyString(`{"items":[]}`)
30+
}
31+
defaultPostPrepare := func() {
32+
gock.New(urlLocalhost).
33+
Post("/foo").Reply(http.StatusOK).BodyString(`{"items":[]}`)
34+
}
2435

2536
tests := []struct {
2637
name string
@@ -41,11 +52,9 @@ func TestTestCase(t *testing.T) {
4152
name: "normal, response is map",
4253
testCase: &atest.TestCase{
4354
Request: atest.Request{
44-
API: urlFoo,
45-
Header: map[string]string{
46-
"key": "value",
47-
},
48-
Body: `{"foo":"bar"}`,
55+
API: urlFoo,
56+
Header: defaultForm,
57+
Body: `{"foo":"bar"}`,
4958
},
5059
Expect: atest.Response{
5160
StatusCode: http.StatusOK,
@@ -204,10 +213,7 @@ func TestTestCase(t *testing.T) {
204213
},
205214
},
206215
},
207-
prepare: func() {
208-
gock.New(urlLocalhost).
209-
Get("/foo").Reply(http.StatusOK).BodyString(`{"items":[]}`)
210-
},
216+
prepare: defaultPrepare,
211217
verify: func(t *testing.T, output interface{}, err error) {
212218
assert.NotNil(t, err)
213219
assert.Contains(t, err.Error(), "failed to get field")
@@ -225,10 +231,7 @@ func TestTestCase(t *testing.T) {
225231
// },
226232
// },
227233
// },
228-
// prepare: func() {
229-
// gock.New(urlLocalhost).
230-
// Get("/foo").Reply(http.StatusOK).BodyString(`{"items":[]}`)
231-
// },
234+
// prepare: defaultPrepare,
232235
// verify: func(t *testing.T, output interface{}, err error) {
233236
// if assert.NotNil(t, err) {
234237
// assert.Contains(t, err.Error(), "failed to verify")
@@ -245,10 +248,7 @@ func TestTestCase(t *testing.T) {
245248
},
246249
},
247250
},
248-
prepare: func() {
249-
gock.New(urlLocalhost).
250-
Get("/foo").Reply(http.StatusOK).BodyString(`{"items":[]}`)
251-
},
251+
prepare: defaultPrepare,
252252
verify: func(t *testing.T, output interface{}, err error) {
253253
assert.NotNil(t, err)
254254
assert.Contains(t, err.Error(), "unknown name println")
@@ -263,10 +263,7 @@ func TestTestCase(t *testing.T) {
263263
},
264264
},
265265
},
266-
prepare: func() {
267-
gock.New(urlLocalhost).
268-
Get("/foo").Reply(http.StatusOK).BodyString(`{"items":[]}`)
269-
},
266+
prepare: defaultPrepare,
270267
verify: func(t *testing.T, output interface{}, err error) {
271268
assert.NotNil(t, err)
272269
assert.Contains(t, err.Error(), "expected bool, but got int")
@@ -303,16 +300,11 @@ func TestTestCase(t *testing.T) {
303300
Header: map[string]string{
304301
util.ContentType: "multipart/form-data",
305302
},
306-
Form: map[string]string{
307-
"key": "value",
308-
},
303+
Form: defaultForm,
309304
},
310305
},
311-
prepare: func() {
312-
gock.New(urlLocalhost).
313-
Post("/foo").Reply(http.StatusOK).BodyString(`{"items":[]}`)
314-
},
315-
verify: noError,
306+
prepare: defaultPostPrepare,
307+
verify: noError,
316308
}, {
317309
name: "normal form request",
318310
testCase: &atest.TestCase{
@@ -322,14 +314,24 @@ func TestTestCase(t *testing.T) {
322314
Header: map[string]string{
323315
util.ContentType: "application/x-www-form-urlencoded",
324316
},
325-
Form: map[string]string{
326-
"key": "value",
327-
},
317+
Form: defaultForm,
318+
},
319+
},
320+
prepare: defaultPostPrepare,
321+
verify: noError,
322+
}, {
323+
name: "body is a template",
324+
testCase: &atest.TestCase{
325+
Request: atest.Request{
326+
API: urlFoo,
327+
Method: http.MethodPost,
328+
Body: `{"name":"{{lower "HELLO"}}"}`,
328329
},
329330
},
330331
prepare: func() {
331332
gock.New(urlLocalhost).
332-
Post("/foo").Reply(http.StatusOK).BodyString(`{"items":[]}`)
333+
Post("/foo").BodyString(`{"name":"hello"}`).
334+
Reply(http.StatusOK).BodyString(`{}`)
333335
},
334336
verify: noError,
335337
}}

‎pkg/util/rand.go

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
Copyright 2015 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 util provides utilities related to randomization.
18+
package util
19+
20+
import (
21+
"math/rand"
22+
"sync"
23+
"time"
24+
)
25+
26+
var rng = struct {
27+
sync.Mutex
28+
rand *rand.Rand
29+
}{
30+
rand: rand.New(rand.NewSource(time.Now().UnixNano())),
31+
}
32+
33+
const (
34+
// We omit vowels from the set of available characters to reduce the chances
35+
// of "bad words" being formed.
36+
alphanums = "bcdfghjklmnpqrstvwxz2456789"
37+
// No. of bits required to index into alphanums string.
38+
alphanumsIdxBits = 5
39+
// Mask used to extract last alphanumsIdxBits of an int.
40+
alphanumsIdxMask = 1<<alphanumsIdxBits - 1
41+
// No. of random letters we can extract from a single int63.
42+
maxAlphanumsPerInt = 63 / alphanumsIdxBits
43+
)
44+
45+
// String generates a random alphanumeric string, without vowels, which is n
46+
// characters long. This will panic if n is less than zero.
47+
// How the random string is created:
48+
// - we generate random int63's
49+
// - from each int63, we are extracting multiple random letters by bit-shifting and masking
50+
// - if some index is out of range of alphanums we neglect it (unlikely to happen multiple times in a row)
51+
func String(n int) string {
52+
b := make([]byte, n)
53+
rng.Lock()
54+
defer rng.Unlock()
55+
56+
randomInt63 := rng.rand.Int63()
57+
remaining := maxAlphanumsPerInt
58+
for i := 0; i < n; {
59+
if remaining == 0 {
60+
randomInt63, remaining = rng.rand.Int63(), maxAlphanumsPerInt
61+
}
62+
if idx := int(randomInt63 & alphanumsIdxMask); idx < len(alphanums) {
63+
b[i] = alphanums[idx]
64+
i++
65+
}
66+
randomInt63 >>= alphanumsIdxBits
67+
remaining--
68+
}
69+
return string(b)
70+
}

‎pkg/util/rand_test.go

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
Copyright 2015 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 util
18+
19+
import (
20+
"strings"
21+
"testing"
22+
)
23+
24+
const (
25+
maxRangeTestCount = 500
26+
testStringLength = 32
27+
)
28+
29+
func TestString(t *testing.T) {
30+
valid := "bcdfghjklmnpqrstvwxz2456789"
31+
for _, l := range []int{0, 1, 2, 10, 123} {
32+
s := String(l)
33+
if len(s) != l {
34+
t.Errorf("expected string of size %d, got %q", l, s)
35+
}
36+
for _, c := range s {
37+
if !strings.ContainsRune(valid, c) {
38+
t.Errorf("expected valid characters, got %v", c)
39+
}
40+
}
41+
}
42+
}
43+
44+
func BenchmarkRandomStringGeneration(b *testing.B) {
45+
b.ResetTimer()
46+
var s string
47+
for i := 0; i < b.N; i++ {
48+
s = String(testStringLength)
49+
}
50+
b.StopTimer()
51+
if len(s) == 0 {
52+
b.Fatal(s)
53+
}
54+
}

‎sample/kubernetes.yaml

+5-7
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,18 @@ items:
2222
request:
2323
api: /api/v1/namespaces/default/configmaps
2424
header:
25-
Authorization: Bearer {{env "K8S_TOKEN"}}
25+
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkRINXBRRi0zSURrbkRDWGhfVHpEaGFuOVdpcEVLSmFwYUI4Y1V5YjFpcUEifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJjbHVzdGVyLWFkbWluLXRva2VuLWtobnI0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImNsdXN0ZXItYWRtaW4iLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiJmZmNlODg0Ny0yZGY4LTQyMTktOGRjYS1mNGRlMWYzNWNmYzkiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6a3ViZS1zeXN0ZW06Y2x1c3Rlci1hZG1pbiJ9.YapUNL7aSlAzlZwDqcMF1-eNpaEs0ZPwybV1uM289fDk8RwjHpLQzVZV0IewaOCAjifwyTyqs1Vgd4nF9I7CYPv64cjMcVTQHCj_-pAxXjiYEM9LkR_b__WGsd-3Z0aRrdyO4WS7moRxZ4kz7ULd_OtlHpq-cFIQtytOaQSZNSbxpa5uP7g7y-uv0nwXBSwqZL9j5XimGlYyy999Q8Vc2GLDrDdVp69wuvToODQzJV44nfuA_dhUFQOzC4sE7Dkq7JarrvZspstqLo1ULzt_Z-cZ-qAu_pUaLHkoLZH5o97g4UF8AXeFYLj8YP_IBP9uhDrm829pNHU82N6Hn-80NQ
2626
method: POST
2727
body: |
2828
{
2929
"apiVersion": "v1",
3030
"kind": "ConfigMap",
3131
"metadata": {
3232
"name": "config",
33-
"namespace": "default"
33+
"namespace": "default",
34+
"labels": {
35+
"key": "{{randomKubernetesName}}"
36+
}
3437
},
3538
"data": {
3639
"key": "value"
@@ -56,8 +59,6 @@ items:
5659
"key": "new value"
5760
}
5861
}
59-
expect:
60-
statusCode: 200
6162
- name: get-configmap
6263
request:
6364
api: /api/v1/namespaces/default/configmaps/config
@@ -77,7 +78,6 @@ items:
7778
}
7879
}
7980
expect:
80-
statusCode: 200
8181
bodyFieldsExpect:
8282
"data/key": "new value"
8383
- name: delete-configmap
@@ -86,5 +86,3 @@ items:
8686
header:
8787
Authorization: Bearer {{env "K8S_TOKEN"}}
8888
method: DELETE
89-
expect:
90-
statusCode: 200

0 commit comments

Comments
 (0)
Please sign in to comment.