Complete the basic test feature

This commit is contained in:
rick 2022-06-08 17:32:17 +08:00
parent 3d55ec106f
commit 316a4276de
13 changed files with 482 additions and 0 deletions

42
.github/release-drafter.yml vendored Normal file
View File

@ -0,0 +1,42 @@
# Configuration for Release Drafter: https://github.com/toolmantim/release-drafter
name-template: 'v$NEXT_PATCH_VERSION 🌈'
tag-template: 'v$NEXT_PATCH_VERSION'
version-template: $MAJOR.$MINOR.$PATCH
# Emoji reference: https://gitmoji.carloscuesta.me/
categories:
- title: '🚀 Features'
labels:
- 'feature'
- 'enhancement'
- 'kind/feature'
- title: '🐛 Bug Fixes'
labels:
- 'fix'
- 'bugfix'
- 'bug'
- 'regression'
- 'kind/bug'
- title: 📝 Documentation updates
labels:
- documentation
- 'kind/doc'
- title: 👻 Maintenance
labels:
- chore
- dependencies
- 'kind/chore'
- 'kind/dep'
- title: 🚦 Tests
labels:
- test
- tests
exclude-labels:
- reverted
- no-changelog
- skip-changelog
- invalid
change-template: '* $TITLE (#$NUMBER) @$AUTHOR'
template: |
## Whats Changed
$CHANGES

14
.github/workflows/release-drafter.yml vendored Normal file
View File

@ -0,0 +1,14 @@
name: Release Drafter
on:
push:
branches:
- master
jobs:
UpdateReleaseDraft:
runs-on: ubuntu-20.04
steps:
- uses: release-drafter/release-drafter@v5
env:
GITHUB_TOKEN: ${{ secrets.GH_PUBLISH_SECRETS }}

37
Dockerfile Normal file
View File

@ -0,0 +1,37 @@
FROM golang:1.17 as builder
WORKDIR /workspace
COPY . .
RUN go mod download
RUN CGO_ENABLE=0 go build -ldflags "-w -s" -o atest cmd/*.go
FROM ghcr.io/linuxsuren/hd:v0.0.67 as hd
FROM alpine:3.10
LABEL "com.github.actions.name"="API testing"
LABEL "com.github.actions.description"="API testing"
LABEL "com.github.actions.icon"="home"
LABEL "com.github.actions.color"="red"
LABEL "repository"="https://github.com/linuxsuren/api-testing"
LABEL "homepage"="https://github.com/linuxsuren/api-testing"
LABEL "maintainer"="Rick <linuxsuren@gmail.com>"
LABEL "Name"="API testing"
ENV LC_ALL C.UTF-8
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US.UTF-8
RUN apk add --no-cache \
git \
openssh-client \
libc6-compat \
libstdc++
COPY entrypoint.sh /entrypoint.sh
COPY --from=builder /workspace/atest /usr/bin/atest
COPY --from=hd /usr/local/bin/hd /usr/local/bin/hd
ENTRYPOINT ["/entrypoint.sh"]

1
README.md Normal file
View File

@ -0,0 +1 @@
This is a API testing tool.

12
action.yml Normal file
View File

@ -0,0 +1,12 @@
name: 'API testing'
description: 'API testing'
inputs:
pattern:
description: 'The pattern of the items'
required: true
default: 'testcase-*.yaml'
runs:
using: 'docker'
image: 'Dockerfile'
args:
- ${{ inputs.pattern }}

51
cmd/root.go Normal file
View File

@ -0,0 +1,51 @@
package main
import (
"github.com/linuxsuren/api-testing/pkg/runner"
"github.com/linuxsuren/api-testing/pkg/testing"
"github.com/spf13/cobra"
"os"
"path/filepath"
)
type option struct {
pattern string
}
func main() {
opt := &option{}
cmd := &cobra.Command{
Use: "atest",
RunE: opt.runE,
}
// set flags
flags := cmd.Flags()
flags.StringVarP(&opt.pattern, "pattern", "p", "testcase-*.yaml",
"The file pattern which try to execute the test cases")
// run command
if err := cmd.Execute(); err != nil {
os.Exit(1)
}
}
func (o *option) runE(cmd *cobra.Command, args []string) (err error) {
var files []string
if files, err = filepath.Glob(o.pattern); err == nil {
for i := range files {
item := files[i]
var testcase *testing.TestCase
if testcase, err = testing.Parse(item); err != nil {
return
}
if err = runner.RunTestCase(testcase); err != nil {
return
}
}
}
return
}

3
entrypoint.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
atest -p "$1"

15
go.mod Normal file
View File

@ -0,0 +1,15 @@
module github.com/linuxsuren/api-testing
go 1.17
require (
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883
github.com/spf13/cobra v1.4.0
gopkg.in/yaml.v2 v2.4.0
)
require (
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/sergi/go-diff v1.2.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
)

32
go.sum Normal file
View File

@ -0,0 +1,32 @@
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q=
github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

110
pkg/exec/command.go Normal file
View File

@ -0,0 +1,110 @@
package exec
import (
"bytes"
"io"
"os"
"os/exec"
"sync"
)
// LookPath is the wrapper of os/exec.LookPath
func LookPath(file string) (string, error) {
return exec.LookPath(file)
}
// RunCommandAndReturn runs a command, then returns the output
func RunCommandAndReturn(name, dir string, args ...string) (result string, err error) {
stdout := &bytes.Buffer{}
if err = RunCommandWithBuffer(name, dir, stdout, nil, args...); err == nil {
result = stdout.String()
}
return
}
// RunCommandWithBuffer runs a command with buffer
// stdout and stderr could be nil
func RunCommandWithBuffer(name, dir string, stdout, stderr *bytes.Buffer, args ...string) error {
if stdout == nil {
stdout = &bytes.Buffer{}
}
if stderr != nil {
stderr = &bytes.Buffer{}
}
return RunCommandWithIO(name, dir, stdout, stderr, args...)
}
// RunCommandWithIO runs a command with given IO
func RunCommandWithIO(name, dir string, stdout, stderr io.Writer, args ...string) (err error) {
command := exec.Command(name, args...)
if dir != "" {
command.Dir = dir
}
//var stdout []byte
//var errStdout error
stdoutIn, _ := command.StdoutPipe()
stderrIn, _ := command.StderrPipe()
err = command.Start()
if err != nil {
return
}
// cmd.Wait() should be called only after we finish reading
// from stdoutIn and stderrIn.
// wg ensures that we finish
var wg sync.WaitGroup
wg.Add(1)
go func() {
_, _ = copyAndCapture(stdout, stdoutIn)
wg.Done()
}()
_, _ = copyAndCapture(stderr, stderrIn)
wg.Wait()
err = command.Wait()
return
}
// RunCommandInDir runs a command
func RunCommandInDir(name, dir string, args ...string) error {
return RunCommandWithIO(name, dir, os.Stdout, os.Stderr, args...)
}
// RunCommand runs a command
func RunCommand(name string, arg ...string) (err error) {
return RunCommandInDir(name, "", arg...)
}
// RunCommandWithSudo runs a command with sudo
func RunCommandWithSudo(name string, args ...string) (err error) {
newArgs := make([]string, 0)
newArgs = append(newArgs, name)
newArgs = append(newArgs, args...)
return RunCommand("sudo", newArgs...)
}
func copyAndCapture(w io.Writer, r io.Reader) ([]byte, error) {
var out []byte
buf := make([]byte, 1024, 1024)
for {
n, err := r.Read(buf[:])
if n > 0 {
d := buf[:n]
out = append(out, d...)
_, err := w.Write(d)
if err != nil {
return out, err
}
}
if err != nil {
// Read returns io.EOF at the end of file, which is not an error for us
if err == io.EOF {
err = nil
}
return out, err
}
}
}

114
pkg/runner/simple.go Normal file
View File

@ -0,0 +1,114 @@
package runner
import (
"bytes"
"fmt"
"github.com/andreyvit/diff"
"github.com/linuxsuren/api-testing/pkg/exec"
"github.com/linuxsuren/api-testing/pkg/testing"
"io"
"io/ioutil"
"net/http"
"strings"
)
func RunTestCase(testcase *testing.TestCase) (err error) {
fmt.Printf("start to run: '%s'\n", testcase.Name)
if err = doPrepare(testcase); err != nil {
err = fmt.Errorf("failed to prepare, error: %v", err)
return
}
defer func() {
if testcase.Clean.CleanPrepare {
if err = doCleanPrepare(testcase); err != nil {
return
}
}
}()
client := http.Client{}
var requestBody io.Reader
if testcase.Request.Body != "" {
requestBody = bytes.NewBufferString(testcase.Request.Body)
}
var request *http.Request
if request, err = http.NewRequest(testcase.Request.Method, testcase.Request.API, requestBody); err != nil {
return
}
// set headers
for key, val := range testcase.Request.Header {
request.Header.Add(key, val)
}
// send the HTTP request
var resp *http.Response
if resp, err = client.Do(request); err != nil {
return
}
if testcase.Expect.StatusCode != 0 {
if err = expectInt(testcase.Name, testcase.Expect.StatusCode, resp.StatusCode); err != nil {
return
}
}
for key, val := range testcase.Expect.Header {
actualVal := resp.Header.Get(key)
if err = expectString(testcase.Name, val, actualVal); err != nil {
return
}
}
if testcase.Expect.Body != "" {
var data []byte
if data, err = ioutil.ReadAll(resp.Body); err != nil {
return
}
if string(data) != strings.TrimSpace(testcase.Expect.Body) {
err = fmt.Errorf("case: %s, got different response body, diff: \n%s", testcase.Name,
diff.LineDiff(testcase.Expect.Body, string(data)))
return
}
}
return
}
func doPrepare(testcase *testing.TestCase) (err error) {
for i := range testcase.Prepare.Kubernetes {
item := testcase.Prepare.Kubernetes[i]
if err = exec.RunCommand("kubectl", "apply", "-f", item); err != nil {
return
}
}
return
}
func doCleanPrepare(testcase *testing.TestCase) (err error) {
for i := range testcase.Prepare.Kubernetes {
item := testcase.Prepare.Kubernetes[i]
if err = exec.RunCommand("kubectl", "delete", "-f", item); err != nil {
return
}
}
return
}
func expectInt(name string, expect, actual int) (err error) {
if expect != actual {
err = fmt.Errorf("case: %s, expect %d, actual %d", name, expect, actual)
}
return
}
func expectString(name, expect, actual string) (err error) {
if expect != actual {
err = fmt.Errorf("case: %s, expect %s, actual %s", name, expect, actual)
}
return
}

32
pkg/testing/case.go Normal file
View File

@ -0,0 +1,32 @@
package testing
type TestCase struct {
Name string
Group string
Prepare Prepare `yaml:"prepare"`
Request Request `yaml:"request"`
Expect Response `yaml:"expect"`
Clean Clean `yaml:"clean"`
}
type Prepare struct {
Kubernetes []string `yaml:"kubernetes"`
}
type Request struct {
API string `yaml:"api"`
Method string `yaml:"method"`
Query map[string]string `yaml:"query"`
Header map[string]string `yaml:"header"`
Body string `yaml:"body"`
}
type Response struct {
StatusCode int `yaml:"statusCode"`
Body string `yaml:"body"`
Header map[string]string `yaml:"header"`
}
type Clean struct {
CleanPrepare bool `yaml:"cleanPrepare"`
}

19
pkg/testing/parser.go Normal file
View File

@ -0,0 +1,19 @@
package testing
import (
"gopkg.in/yaml.v2"
"io/ioutil"
)
func Parse(configFile string) (testCase *TestCase, err error) {
var data []byte
if data, err = ioutil.ReadFile(configFile); err != nil {
return
}
testCase = &TestCase{}
if err = yaml.Unmarshal(data, testCase); err != nil {
return
}
return
}