diff --git a/cmd/root_test.go b/cmd/root_test.go index 3047559e..5546fc1d 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -5,22 +5,28 @@ import ( "github.com/stretchr/testify/assert" - exec "github.com/linuxsuren/go-fake-runtime" + fakeruntime "github.com/linuxsuren/go-fake-runtime" ) func TestCreateRunCommand(t *testing.T) { cmd := createRunCommand() assert.Equal(t, "run", cmd.Use) - init := createInitCommand(exec.FakeExecer{}) + init := createInitCommand(fakeruntime.FakeExecer{}) assert.Equal(t, "init", init.Use) server := createServerCmd(&fakeGRPCServer{}) assert.NotNil(t, server) assert.Equal(t, "server", server.Use) - root := NewRootCmd(exec.FakeExecer{}, NewFakeGRPCServer()) + root := NewRootCmd(fakeruntime.FakeExecer{}, NewFakeGRPCServer()) root.SetArgs([]string{"init", "-k=demo.yaml", "--wait-namespace", "demo", "--wait-resource", "demo"}) err := root.Execute() assert.Nil(t, err) } + +func TestRootCmd(t *testing.T) { + c := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}, NewFakeGRPCServer()) + assert.NotNil(t, c) + assert.Equal(t, "atest", c.Use) +} diff --git a/cmd/run.go b/cmd/run.go index 37c76d59..17d58337 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -5,8 +5,6 @@ import ( "fmt" "io" "os" - "path" - "path/filepath" "strings" "sync" "time" @@ -16,7 +14,6 @@ import ( "github.com/linuxsuren/api-testing/pkg/render" "github.com/linuxsuren/api-testing/pkg/runner" "github.com/linuxsuren/api-testing/pkg/testing" - "github.com/linuxsuren/api-testing/pkg/util" "github.com/spf13/cobra" "golang.org/x/sync/semaphore" ) @@ -40,12 +37,16 @@ type runOption struct { swaggerURL string level string caseItems []string + + // for internal use + loader testing.Loader } func newDefaultRunOption() *runOption { return &runOption{ reporter: runner.NewMemoryTestReporter(), reportWriter: runner.NewResultWriter(os.Stdout), + loader: testing.NewFileLoader(), } } @@ -134,19 +135,13 @@ func (o *runOption) runE(cmd *cobra.Command, args []string) (err error) { o.limiter.Stop() }() - var suites []string - for _, pattern := range util.Expand(o.pattern) { - var files []string - if files, err = filepath.Glob(pattern); err == nil { - suites = append(suites, files...) - } + if err = o.loader.Put(o.pattern); err != nil { + return } - cmd.Println("found suites:", len(suites)) - for i := range suites { - item := suites[i] - cmd.Println("run suite:", item) - if err = o.runSuiteWithDuration(item); err != nil { + cmd.Println("found suites:", o.loader.GetCount()) + for o.loader.HasMore() { + if err = o.runSuiteWithDuration(o.loader); err != nil { break } } @@ -166,7 +161,7 @@ func (o *runOption) runE(cmd *cobra.Command, args []string) (err error) { return } -func (o *runOption) runSuiteWithDuration(suite string) (err error) { +func (o *runOption) runSuiteWithDuration(loader testing.Loader) (err error) { sem := semaphore.NewWeighted(o.thread) stop := false var timeout *time.Ticker @@ -204,7 +199,7 @@ func (o *runOption) runSuiteWithDuration(suite string) (err error) { }() dataContext := getDefaultContext() - ch <- o.runSuite(suite, dataContext, o.context, stopSingal) + ch <- o.runSuite(loader, dataContext, o.context, stopSingal) }(errChannel, sem) if o.duration <= 0 { stop = true @@ -221,9 +216,14 @@ func (o *runOption) runSuiteWithDuration(suite string) (err error) { return } -func (o *runOption) runSuite(suite string, dataContext map[string]interface{}, ctx context.Context, stopSingal chan struct{}) (err error) { +func (o *runOption) runSuite(loader testing.Loader, dataContext map[string]interface{}, ctx context.Context, stopSingal chan struct{}) (err error) { + var data []byte + if data, err = loader.Load(); err != nil { + return + } + var testSuite *testing.TestSuite - if testSuite, err = testing.Parse(suite); err != nil { + if testSuite, err = testing.Parse(data); err != nil { return } @@ -253,7 +253,7 @@ func (o *runOption) runSuite(suite string, dataContext map[string]interface{}, c o.limiter.Accept() ctxWithTimeout, _ := context.WithTimeout(ctx, o.requestTimeout) - ctxWithTimeout = context.WithValue(ctxWithTimeout, runner.ContextKey("").ParentDir(), path.Dir(suite)) + ctxWithTimeout = context.WithValue(ctxWithTimeout, runner.ContextKey("").ParentDir(), loader.GetContext()) simpleRunner := runner.NewSimpleTestCaseRunner() simpleRunner.WithTestReporter(o.reporter) diff --git a/cmd/run_test.go b/cmd/run_test.go index 4e0e0a64..e8ac5bdc 100644 --- a/cmd/run_test.go +++ b/cmd/run_test.go @@ -12,8 +12,8 @@ import ( "github.com/h2non/gock" "github.com/linuxsuren/api-testing/pkg/limit" + atest "github.com/linuxsuren/api-testing/pkg/testing" "github.com/linuxsuren/api-testing/pkg/util" - fakeruntime "github.com/linuxsuren/go-fake-runtime" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" ) @@ -58,8 +58,13 @@ func TestRunSuite(t *testing.T) { opt.limiter = limit.NewDefaultRateLimiter(0, 0) stopSingal := make(chan struct{}, 1) - err := opt.runSuite(tt.suiteFile, ctx, context.TODO(), stopSingal) - assert.Equal(t, tt.hasError, err != nil, err) + loader := atest.NewFileLoader() + err := loader.Put(tt.suiteFile) + assert.NoError(t, err) + if loader.HasMore() { + err = opt.runSuite(loader, ctx, context.TODO(), stopSingal) + assert.Equal(t, tt.hasError, err != nil, err) + } }) } } @@ -128,6 +133,11 @@ func TestRunCommand(t *testing.T) { prepare: fooPrepare, args: []string{"-p", simpleSuite, "--report", "md", "--report-file", path.Join(tmpFile.Name(), "fake")}, hasErr: true, + }, { + name: "malformed report file path", + prepare: fooPrepare, + args: []string{"-p", "[]]$#%*^&()"}, + hasErr: true, }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -146,12 +156,6 @@ func TestRunCommand(t *testing.T) { } } -func TestRootCmd(t *testing.T) { - c := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}, NewFakeGRPCServer()) - assert.NotNil(t, c) - assert.Equal(t, "atest", c.Use) -} - func TestPreRunE(t *testing.T) { tests := []struct { name string diff --git a/pkg/testing/loader.go b/pkg/testing/loader.go new file mode 100644 index 00000000..a6140da8 --- /dev/null +++ b/pkg/testing/loader.go @@ -0,0 +1,10 @@ +package testing + +// Loader is an interface for test cases loader +type Loader interface { + HasMore() bool + Load() ([]byte, error) + Put(string) (err error) + GetContext() string + GetCount() int +} diff --git a/pkg/testing/loader_file.go b/pkg/testing/loader_file.go new file mode 100644 index 00000000..9fd77ba4 --- /dev/null +++ b/pkg/testing/loader_file.go @@ -0,0 +1,52 @@ +package testing + +import ( + "os" + "path" + "path/filepath" + + "github.com/linuxsuren/api-testing/pkg/util" +) + +type fileLoader struct { + paths []string + index int +} + +// NewFileLoader creates the instance of file loader +func NewFileLoader() Loader { + return &fileLoader{index: -1} +} + +// HasMore returns if there are more test cases +func (l *fileLoader) HasMore() bool { + l.index++ + return l.index < len(l.paths) +} + +// Load returns the test case content +func (l *fileLoader) Load() (data []byte, err error) { + data, err = os.ReadFile(l.paths[l.index]) + return +} + +// Put adds the test case path +func (l *fileLoader) Put(item string) (err error) { + for _, pattern := range util.Expand(item) { + var files []string + if files, err = filepath.Glob(pattern); err == nil { + l.paths = append(l.paths, files...) + } + } + return +} + +// GetContext returns the context of current test case +func (l *fileLoader) GetContext() string { + return path.Dir(l.paths[l.index]) +} + +// GetCount returns the count of test cases +func (l *fileLoader) GetCount() int { + return len(l.paths) +} diff --git a/pkg/testing/loader_file_test.go b/pkg/testing/loader_file_test.go new file mode 100644 index 00000000..d3ac7f10 --- /dev/null +++ b/pkg/testing/loader_file_test.go @@ -0,0 +1,56 @@ +package testing_test + +import ( + "testing" + + atest "github.com/linuxsuren/api-testing/pkg/testing" + "github.com/stretchr/testify/assert" +) + +func TestFileLoader(t *testing.T) { + tests := []struct { + name string + items []string + verify func(t *testing.T, loader atest.Loader) + }{{ + name: "empty", + items: []string{}, + verify: func(t *testing.T, loader atest.Loader) { + assert.False(t, loader.HasMore()) + assert.Empty(t, loader.GetCount()) + }, + }, { + name: "brace expansion path", + items: []string{"testdata/{invalid-,}testcase.yaml"}, + verify: defaultVerify, + }, { + name: "glob path", + items: []string{"testdata/*testcase.yaml"}, + verify: defaultVerify, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + loader := atest.NewFileLoader() + for _, item := range tt.items { + loader.Put(item) + } + tt.verify(t, loader) + }) + } +} + +func defaultVerify(t *testing.T, loader atest.Loader) { + assert.True(t, loader.HasMore()) + data, err := loader.Load() + assert.Nil(t, err) + assert.Equal(t, invalidTestCaseContent, string(data)) + assert.Equal(t, "testdata", loader.GetContext()) + + assert.True(t, loader.HasMore()) + data, err = loader.Load() + assert.Nil(t, err) + assert.Equal(t, testCaseContent, string(data)) + assert.Equal(t, "testdata", loader.GetContext()) + + assert.False(t, loader.HasMore()) +} diff --git a/pkg/testing/parser.go b/pkg/testing/parser.go index ffc9936f..14f13259 100644 --- a/pkg/testing/parser.go +++ b/pkg/testing/parser.go @@ -19,11 +19,8 @@ import ( ) // Parse parses a file and returns the test suite -func Parse(configFile string) (testSuite *TestSuite, err error) { - var data []byte - if data, err = os.ReadFile(configFile); err == nil { - testSuite, err = ParseFromData(data) - } +func Parse(data []byte) (testSuite *TestSuite, err error) { + testSuite, err = ParseFromData(data) // schema validation if err == nil { @@ -116,7 +113,7 @@ func (r *Request) Render(ctx interface{}, dataDir string) (err error) { } // setting default values - r.Method = emptyThenDefault(r.Method, http.MethodGet) + r.Method = EmptyThenDefault(r.Method, http.MethodGet) return } @@ -153,18 +150,20 @@ func (r *Request) GetBody() (reader io.Reader, err error) { // Render renders the response func (r *Response) Render(ctx interface{}) (err error) { - r.StatusCode = zeroThenDefault(r.StatusCode, http.StatusOK) + r.StatusCode = ZeroThenDefault(r.StatusCode, http.StatusOK) return } -func zeroThenDefault(val, defVal int) int { +// ZeroThenDefault return the default value if the val is zero +func ZeroThenDefault(val, defVal int) int { if val == 0 { val = defVal } return val } -func emptyThenDefault(val, defVal string) string { +// EmptyThenDefault return the default value if the val is empty +func EmptyThenDefault(val, defVal string) string { if strings.TrimSpace(val) == "" { val = defVal } diff --git a/pkg/testing/parser_test.go b/pkg/testing/parser_test.go index b2d56635..84b4d3e9 100644 --- a/pkg/testing/parser_test.go +++ b/pkg/testing/parser_test.go @@ -1,150 +1,162 @@ -package testing +package testing_test import ( "io" "net/http" + "os" "testing" _ "embed" + atest "github.com/linuxsuren/api-testing/pkg/testing" "github.com/linuxsuren/api-testing/pkg/util" "github.com/stretchr/testify/assert" ) func TestParse(t *testing.T) { - suite, err := Parse("../../sample/testsuite-gitlab.yaml") + data, err := os.ReadFile("../../sample/testsuite-gitlab.yaml") + if !assert.NoError(t, err) { + return + } + + suite, err := atest.Parse(data) if assert.Nil(t, err) && assert.NotNil(t, suite) { assert.Equal(t, "Gitlab", suite.Name) assert.Equal(t, 2, len(suite.Items)) - assert.Equal(t, TestCase{ + assert.Equal(t, atest.TestCase{ Name: "projects", - Request: Request{ + Request: atest.Request{ API: "https://gitlab.com/api/v4/projects", }, - Expect: Response{ + Expect: atest.Response{ StatusCode: http.StatusOK, Schema: `{ "type": "array" } `, }, - Before: Job{ + Before: atest.Job{ Items: []string{"sleep(1)"}, }, - After: Job{ + After: atest.Job{ Items: []string{"sleep(1)"}, }, }, suite.Items[0]) } - _, err = Parse("testdata/invalid-testcase.yaml") + _, err = atest.Parse([]byte(invalidTestCaseContent)) assert.NotNil(t, err) } func TestDuplicatedNames(t *testing.T) { - _, err := Parse("testdata/duplicated-names.yaml") + data, err := os.ReadFile("testdata/duplicated-names.yaml") + if !assert.NoError(t, err) { + return + } + + _, err = atest.Parse(data) assert.NotNil(t, err) - _, err = ParseFromData([]byte("fake")) + _, err = atest.ParseFromData([]byte("fake")) assert.NotNil(t, err) } func TestRequestRender(t *testing.T) { tests := []struct { name string - request *Request - verify func(t *testing.T, req *Request) + request *atest.Request + verify func(t *testing.T, req *atest.Request) ctx interface{} hasErr bool }{{ name: "slice as context", - request: &Request{ + request: &atest.Request{ API: "http://localhost/{{index . 0}}", Body: "{{index . 1}}", }, ctx: []string{"foo", "bar"}, hasErr: false, - verify: func(t *testing.T, req *Request) { + verify: func(t *testing.T, req *atest.Request) { assert.Equal(t, "http://localhost/foo", req.API) assert.Equal(t, "bar", req.Body) }, }, { name: "default values", - request: &Request{}, - verify: func(t *testing.T, req *Request) { + request: &atest.Request{}, + verify: func(t *testing.T, req *atest.Request) { assert.Equal(t, http.MethodGet, req.Method) }, hasErr: false, }, { name: "context is nil", - request: &Request{}, + request: &atest.Request{}, ctx: nil, hasErr: false, }, { name: "body from file", - request: &Request{ + request: &atest.Request{ BodyFromFile: "testdata/generic_body.json", }, - ctx: TestCase{ + ctx: atest.TestCase{ Name: "linuxsuren", }, hasErr: false, - verify: func(t *testing.T, req *Request) { + verify: func(t *testing.T, req *atest.Request) { assert.Equal(t, `{"name": "linuxsuren"}`, req.Body) }, }, { name: "body file not found", - request: &Request{ + request: &atest.Request{ BodyFromFile: "testdata/fake", }, hasErr: true, }, { name: "invalid API as template", - request: &Request{ + request: &atest.Request{ API: "{{.name}", }, hasErr: true, }, { name: "failed with API render", - request: &Request{ + request: &atest.Request{ API: "{{.name}}", }, - ctx: TestCase{}, + ctx: atest.TestCase{}, hasErr: true, }, { name: "invalid body as template", - request: &Request{ + request: &atest.Request{ Body: "{{.name}", }, hasErr: true, }, { name: "failed with body render", - request: &Request{ + request: &atest.Request{ Body: "{{.name}}", }, - ctx: TestCase{}, + ctx: atest.TestCase{}, hasErr: true, }, { name: "form render", - request: &Request{ + request: &atest.Request{ Form: map[string]string{ "key": "{{.Name}}", }, }, - ctx: TestCase{Name: "linuxsuren"}, - verify: func(t *testing.T, req *Request) { + ctx: atest.TestCase{Name: "linuxsuren"}, + verify: func(t *testing.T, req *atest.Request) { assert.Equal(t, "linuxsuren", req.Form["key"]) }, hasErr: false, }, { name: "header render", - request: &Request{ + request: &atest.Request{ Header: map[string]string{ "key": "{{.Name}}", }, }, - ctx: TestCase{Name: "linuxsuren"}, - verify: func(t *testing.T, req *Request) { + ctx: atest.TestCase{Name: "linuxsuren"}, + verify: func(t *testing.T, req *atest.Request) { assert.Equal(t, "linuxsuren", req.Header["key"]) }, hasErr: false, @@ -162,14 +174,14 @@ func TestRequestRender(t *testing.T) { func TestResponseRender(t *testing.T) { tests := []struct { name string - response *Response - verify func(t *testing.T, req *Response) + response *atest.Response + verify func(t *testing.T, req *atest.Response) ctx interface{} hasErr bool }{{ name: "blank response", - response: &Response{}, - verify: func(t *testing.T, req *Response) { + response: &atest.Response{}, + verify: func(t *testing.T, req *atest.Response) { assert.Equal(t, http.StatusOK, req.StatusCode) }, hasErr: false, @@ -208,24 +220,24 @@ func TestEmptyThenDefault(t *testing.T) { }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := emptyThenDefault(tt.val, tt.defVal) + result := atest.EmptyThenDefault(tt.val, tt.defVal) assert.Equal(t, tt.expect, result, result) }) } - assert.Equal(t, 1, zeroThenDefault(0, 1)) - assert.Equal(t, 1, zeroThenDefault(1, 2)) + assert.Equal(t, 1, atest.ZeroThenDefault(0, 1)) + assert.Equal(t, 1, atest.ZeroThenDefault(1, 2)) } func TestTestCase(t *testing.T) { - testCase, err := ParseTestCaseFromData([]byte(testCaseContent)) + testCase, err := atest.ParseTestCaseFromData([]byte(testCaseContent)) assert.Nil(t, err) - assert.Equal(t, &TestCase{ + assert.Equal(t, &atest.TestCase{ Name: "projects", - Request: Request{ + Request: atest.Request{ API: "https://foo", }, - Expect: Response{ + Expect: atest.Response{ StatusCode: http.StatusOK, }, }, testCase) @@ -236,21 +248,21 @@ func TestGetBody(t *testing.T) { tests := []struct { name string - req *Request + req *atest.Request expectBody string containBody string expectErr bool }{{ name: "normal body", - req: &Request{Body: defaultBody}, + req: &atest.Request{Body: defaultBody}, expectBody: defaultBody, }, { name: "body from file", - req: &Request{BodyFromFile: "testdata/testcase.yaml"}, + req: &atest.Request{BodyFromFile: "testdata/testcase.yaml"}, expectBody: testCaseContent, }, { name: "multipart form data", - req: &Request{ + req: &atest.Request{ Header: map[string]string{ util.ContentType: util.MultiPartFormData, }, @@ -261,7 +273,7 @@ func TestGetBody(t *testing.T) { containBody: "name=\"key\"\r\n\r\nvalue\r\n", }, { name: "normal form", - req: &Request{ + req: &atest.Request{ Header: map[string]string{ util.ContentType: util.Form, }, @@ -292,3 +304,6 @@ func TestGetBody(t *testing.T) { //go:embed testdata/testcase.yaml var testCaseContent string + +//go:embed testdata/invalid-testcase.yaml +var invalidTestCaseContent string