Complete the basic test feature
This commit is contained in:
parent
3d55ec106f
commit
316a4276de
|
@ -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: |
|
||||
## What’s Changed
|
||||
|
||||
$CHANGES
|
|
@ -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 }}
|
|
@ -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"]
|
|
@ -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 }}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
|
||||
atest -p "$1"
|
|
@ -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
|
||||
)
|
|
@ -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=
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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"`
|
||||
}
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue