From e033a1a4a50cf96986ad13350685118ae3a3fae0 Mon Sep 17 00:00:00 2001 From: Ben Schumacher Date: Thu, 23 Apr 2015 15:56:26 -0600 Subject: [PATCH 1/3] Alternative pattern for API unit tests --- datastore/datastore.go | 5 +- mocks/Datastore.go | 304 +++++++++++++++++++++++++++++++++++++++++ server/badge_test.go | 115 ++++++++++++++++ server/mocks_test.go | 19 +++ 4 files changed, 441 insertions(+), 2 deletions(-) create mode 100644 mocks/Datastore.go create mode 100644 server/badge_test.go create mode 100644 server/mocks_test.go diff --git a/datastore/datastore.go b/datastore/datastore.go index 359884d5d..6bd4a55c1 100644 --- a/datastore/datastore.go +++ b/datastore/datastore.go @@ -1,14 +1,15 @@ package datastore import ( + "errors" "io" "github.com/drone/drone/common" ) var ( - ErrConflict = "Key not unique" - ErrNotFound = "Key not found" + ErrConflict = errors.New("Key not unique") + ErrKeyNotFound = errors.New("Key not found") ) type Datastore interface { diff --git a/mocks/Datastore.go b/mocks/Datastore.go new file mode 100644 index 000000000..4d05f93db --- /dev/null +++ b/mocks/Datastore.go @@ -0,0 +1,304 @@ +package mocks + +import "github.com/stretchr/testify/mock" + +import "io" +import "github.com/drone/drone/common" + +type Datastore struct { + mock.Mock +} + +func (m *Datastore) User(_a0 string) (*common.User, error) { + ret := m.Called(_a0) + + var r0 *common.User + if ret.Get(0) != nil { + r0 = ret.Get(0).(*common.User) + } + r1 := ret.Error(1) + + return r0, r1 +} +func (m *Datastore) UserCount() (int, error) { + ret := m.Called() + + r0 := ret.Get(0).(int) + r1 := ret.Error(1) + + return r0, r1 +} +func (m *Datastore) UserList() ([]*common.User, error) { + ret := m.Called() + + var r0 []*common.User + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*common.User) + } + r1 := ret.Error(1) + + return r0, r1 +} +func (m *Datastore) SetUser(_a0 *common.User) error { + ret := m.Called(_a0) + + r0 := ret.Error(0) + + return r0 +} +func (m *Datastore) SetUserNotExists(_a0 *common.User) error { + ret := m.Called(_a0) + + r0 := ret.Error(0) + + return r0 +} +func (m *Datastore) DelUser(_a0 *common.User) error { + ret := m.Called(_a0) + + r0 := ret.Error(0) + + return r0 +} +func (m *Datastore) Token(_a0 string, _a1 string) (*common.Token, error) { + ret := m.Called(_a0, _a1) + + var r0 *common.Token + if ret.Get(0) != nil { + r0 = ret.Get(0).(*common.Token) + } + r1 := ret.Error(1) + + return r0, r1 +} +func (m *Datastore) TokenList(_a0 string) ([]*common.Token, error) { + ret := m.Called(_a0) + + var r0 []*common.Token + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*common.Token) + } + r1 := ret.Error(1) + + return r0, r1 +} +func (m *Datastore) SetToken(_a0 *common.Token) error { + ret := m.Called(_a0) + + r0 := ret.Error(0) + + return r0 +} +func (m *Datastore) DelToken(_a0 *common.Token) error { + ret := m.Called(_a0) + + r0 := ret.Error(0) + + return r0 +} +func (m *Datastore) Subscribed(_a0 string, _a1 string) (bool, error) { + ret := m.Called(_a0, _a1) + + r0 := ret.Get(0).(bool) + r1 := ret.Error(1) + + return r0, r1 +} +func (m *Datastore) SetSubscriber(_a0 string, _a1 string) error { + ret := m.Called(_a0, _a1) + + r0 := ret.Error(0) + + return r0 +} +func (m *Datastore) DelSubscriber(_a0 string, _a1 string) error { + ret := m.Called(_a0, _a1) + + r0 := ret.Error(0) + + return r0 +} +func (m *Datastore) Repo(_a0 string) (*common.Repo, error) { + ret := m.Called(_a0) + + var r0 *common.Repo + if ret.Get(0) != nil { + r0 = ret.Get(0).(*common.Repo) + } + r1 := ret.Error(1) + + return r0, r1 +} +func (m *Datastore) RepoList(_a0 string) ([]*common.Repo, error) { + ret := m.Called(_a0) + + var r0 []*common.Repo + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*common.Repo) + } + r1 := ret.Error(1) + + return r0, r1 +} +func (m *Datastore) RepoParams(_a0 string) (map[string]string, error) { + ret := m.Called(_a0) + + var r0 map[string]string + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]string) + } + r1 := ret.Error(1) + + return r0, r1 +} +func (m *Datastore) RepoKeypair(_a0 string) (*common.Keypair, error) { + ret := m.Called(_a0) + + var r0 *common.Keypair + if ret.Get(0) != nil { + r0 = ret.Get(0).(*common.Keypair) + } + r1 := ret.Error(1) + + return r0, r1 +} +func (m *Datastore) SetRepo(_a0 *common.Repo) error { + ret := m.Called(_a0) + + r0 := ret.Error(0) + + return r0 +} +func (m *Datastore) SetRepoNotExists(_a0 *common.User, _a1 *common.Repo) error { + ret := m.Called(_a0, _a1) + + r0 := ret.Error(0) + + return r0 +} +func (m *Datastore) SetRepoParams(_a0 string, _a1 map[string]string) error { + ret := m.Called(_a0, _a1) + + r0 := ret.Error(0) + + return r0 +} +func (m *Datastore) SetRepoKeypair(_a0 string, _a1 *common.Keypair) error { + ret := m.Called(_a0, _a1) + + r0 := ret.Error(0) + + return r0 +} +func (m *Datastore) DelRepo(_a0 *common.Repo) error { + ret := m.Called(_a0) + + r0 := ret.Error(0) + + return r0 +} +func (m *Datastore) Build(_a0 string, _a1 int) (*common.Build, error) { + ret := m.Called(_a0, _a1) + + var r0 *common.Build + if ret.Get(0) != nil { + r0 = ret.Get(0).(*common.Build) + } + r1 := ret.Error(1) + + return r0, r1 +} +func (m *Datastore) BuildList(_a0 string) ([]*common.Build, error) { + ret := m.Called(_a0) + + var r0 []*common.Build + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*common.Build) + } + r1 := ret.Error(1) + + return r0, r1 +} +func (m *Datastore) BuildLast(_a0 string) (*common.Build, error) { + ret := m.Called(_a0) + + var r0 *common.Build + if ret.Get(0) != nil { + r0 = ret.Get(0).(*common.Build) + } + r1 := ret.Error(1) + + return r0, r1 +} +func (m *Datastore) SetBuild(_a0 string, _a1 *common.Build) error { + ret := m.Called(_a0, _a1) + + r0 := ret.Error(0) + + return r0 +} +func (m *Datastore) Status(_a0 string, _a1 int, _a2 string) (*common.Status, error) { + ret := m.Called(_a0, _a1, _a2) + + var r0 *common.Status + if ret.Get(0) != nil { + r0 = ret.Get(0).(*common.Status) + } + r1 := ret.Error(1) + + return r0, r1 +} +func (m *Datastore) StatusList(_a0 string, _a1 int) ([]*common.Status, error) { + ret := m.Called(_a0, _a1) + + var r0 []*common.Status + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*common.Status) + } + r1 := ret.Error(1) + + return r0, r1 +} +func (m *Datastore) SetStatus(_a0 string, _a1 int, _a2 *common.Status) error { + ret := m.Called(_a0, _a1, _a2) + + r0 := ret.Error(0) + + return r0 +} +func (m *Datastore) LogReader(_a0 string, _a1 int, _a2 int) (io.Reader, error) { + ret := m.Called(_a0, _a1, _a2) + + r0 := ret.Get(0).(io.Reader) + r1 := ret.Error(1) + + return r0, r1 +} +func (m *Datastore) SetLogs(_a0 string, _a1 int, _a2 int, _a3 []byte) error { + ret := m.Called(_a0, _a1, _a2, _a3) + + r0 := ret.Error(0) + + return r0 +} +func (m *Datastore) SetBuildState(_a0 string, _a1 *common.Build) error { + ret := m.Called(_a0, _a1) + + r0 := ret.Error(0) + + return r0 +} +func (m *Datastore) SetBuildStatus(_a0 string, _a1 int, _a2 *common.Status) error { + ret := m.Called(_a0, _a1, _a2) + + r0 := ret.Error(0) + + return r0 +} +func (m *Datastore) SetBuildTask(_a0 string, _a1 int, _a2 *common.Task) error { + ret := m.Called(_a0, _a1, _a2) + + r0 := ret.Error(0) + + return r0 +} diff --git a/server/badge_test.go b/server/badge_test.go new file mode 100644 index 000000000..de8e448b0 --- /dev/null +++ b/server/badge_test.go @@ -0,0 +1,115 @@ +package server + +import ( + "bufio" + "net" + "net/http" + "net/http/httptest" + "testing" + + "github.com/drone/drone/common" + "github.com/drone/drone/mocks" + . "github.com/franela/goblin" + "github.com/gin-gonic/gin" +) + +type RecorderImpl struct { + recorder *httptest.ResponseRecorder +} + +func (ri RecorderImpl) Header() http.Header { + return ri.recorder.Header() +} + +func (ri RecorderImpl) Write(buf []byte) (int, error) { + return ri.recorder.Write(buf) +} + +func (ri RecorderImpl) WriteHeader(code int) { + ri.recorder.WriteHeader(code) +} + +func (ri RecorderImpl) CloseNotify() <-chan bool { + return http.ResponseWriter(ri.recorder).(http.CloseNotifier).CloseNotify() +} + +func (ri RecorderImpl) Flush() { + ri.recorder.Flush() +} + +func (ri RecorderImpl) Hijack() (net.Conn, *bufio.ReadWriter, error) { + return http.ResponseWriter(ri.recorder).(http.Hijacker).Hijack() +} + +func (ri RecorderImpl) Size() int { + return ri.recorder.Body.Len() +} + +func (ri RecorderImpl) Status() int { + return ri.recorder.Code +} + +func (ri RecorderImpl) WriteHeaderNow() { + // no-op? +} + +func (ri RecorderImpl) Written() bool { + return false +} + +func TestBadage(t *testing.T) { + g := Goblin(t) + g.Describe("Badge", func() { + // token := common.Token{ + // Kind: "github", + // Login: "Freya", + // Label: "github", + // Repos: []string{"Freya/Hello-World"}, + // Scopes: []string{}, + // Expiry: 0, + // Issued: 0, + // } + + var ds *mocks.Datastore + var ctx gin.Context + g.BeforeEach(func() { + router := gin.New() + ds = new(mocks.Datastore) + ctx = gin.Context{Engine: router} + ctx.Set("datastore", ds) + }) + + g.AfterEach(func() { + }) + + g.It("should provide SVG response", func() { + w := new(RecorderImpl) + w.recorder = httptest.NewRecorder() + ctx.Writer = w + + repo := &common.Repo{Owner: "Freya", Name: "Hello-World"} + ctx.Set("repo", repo) + + // TODO(benschumacher) expand this a lot. + GetBadge(&ctx) + g.Assert(w.Status()).Equal(200) + + // Check the variety of states + for _, state := range []string{common.StateError, common.StateFailure, common.StateKilled, common.StatePending, common.StateRunning, common.StateSuccess} { + repo.Last = &common.Build{State: state} + ctx.Set("repo", repo) + + GetBadge(&ctx) + g.Assert(w.Status()).Equal(200) + } + }) + g.It("should provide CCTray response") /*, func() { + w := httptest.NewRecorder() + ctx.Writer = w + + repo := &common.Repo{Owner: "Freya", Name: "Hello-World"} + ctx.Set("repo", repo) + + }*/ + }) +} diff --git a/server/mocks_test.go b/server/mocks_test.go new file mode 100644 index 000000000..91477b855 --- /dev/null +++ b/server/mocks_test.go @@ -0,0 +1,19 @@ +package server + +import ( + "net/http" + + "github.com/drone/drone/common" +) + +type MockSession struct { + Token *common.Token +} + +func (s MockSession) GenerateToken(*common.Token) (string, error) { + return "session", nil +} + +func (s MockSession) GetLogin(*http.Request) *common.Token { + return s.Token +} From 0e53778d22fd9c2e121cea258d67fe4b2884cbff Mon Sep 17 00:00:00 2001 From: Ben Schumacher Date: Tue, 28 Apr 2015 15:42:09 -0600 Subject: [PATCH 2/3] Badge API tests --- server/badge_test.go | 197 ++++++++++++++++++-------------- server/mocks_test.go | 19 --- server/responserecorder_test.go | 33 ++++++ 3 files changed, 146 insertions(+), 103 deletions(-) delete mode 100644 server/mocks_test.go create mode 100644 server/responserecorder_test.go diff --git a/server/badge_test.go b/server/badge_test.go index de8e448b0..22e6b07f6 100644 --- a/server/badge_test.go +++ b/server/badge_test.go @@ -1,115 +1,144 @@ package server import ( - "bufio" - "net" + "encoding/xml" "net/http" - "net/http/httptest" + "net/url" + "strings" "testing" + "time" "github.com/drone/drone/common" + "github.com/drone/drone/common/ccmenu" + "github.com/drone/drone/datastore" "github.com/drone/drone/mocks" . "github.com/franela/goblin" "github.com/gin-gonic/gin" ) -type RecorderImpl struct { - recorder *httptest.ResponseRecorder -} - -func (ri RecorderImpl) Header() http.Header { - return ri.recorder.Header() -} - -func (ri RecorderImpl) Write(buf []byte) (int, error) { - return ri.recorder.Write(buf) -} - -func (ri RecorderImpl) WriteHeader(code int) { - ri.recorder.WriteHeader(code) -} - -func (ri RecorderImpl) CloseNotify() <-chan bool { - return http.ResponseWriter(ri.recorder).(http.CloseNotifier).CloseNotify() -} - -func (ri RecorderImpl) Flush() { - ri.recorder.Flush() -} - -func (ri RecorderImpl) Hijack() (net.Conn, *bufio.ReadWriter, error) { - return http.ResponseWriter(ri.recorder).(http.Hijacker).Hijack() -} - -func (ri RecorderImpl) Size() int { - return ri.recorder.Body.Len() -} - -func (ri RecorderImpl) Status() int { - return ri.recorder.Code -} - -func (ri RecorderImpl) WriteHeaderNow() { - // no-op? -} - -func (ri RecorderImpl) Written() bool { - return false -} - func TestBadage(t *testing.T) { g := Goblin(t) g.Describe("Badge", func() { - // token := common.Token{ - // Kind: "github", - // Login: "Freya", - // Label: "github", - // Repos: []string{"Freya/Hello-World"}, - // Scopes: []string{}, - // Expiry: 0, - // Issued: 0, - // } - - var ds *mocks.Datastore var ctx gin.Context + owner := "Freya" + name := "Hello-World" + fullName := owner + "/" + name + repo := &common.Repo{Owner: owner, Name: name, FullName: fullName} g.BeforeEach(func() { - router := gin.New() - ds = new(mocks.Datastore) - ctx = gin.Context{Engine: router} - ctx.Set("datastore", ds) + ctx = gin.Context{Engine: gin.Default()} + url, _ := url.Parse("http://drone.local/badges/" + fullName) + ctx.Request = &http.Request{URL: url} + ctx.Set("repo", repo) }) g.AfterEach(func() { }) - g.It("should provide SVG response", func() { - w := new(RecorderImpl) - w.recorder = httptest.NewRecorder() - ctx.Writer = w + cycleStateTester := func(expector gin.HandlerFunc, handle gin.HandlerFunc, validator func(state string, w *ResponseRecorder)) { + for idx, state := range []string{"", common.StateError, common.StateFailure, common.StateKilled, common.StatePending, common.StateRunning, common.StateSuccess} { + w := NewResponseRecorder() + ctx.Writer = w - repo := &common.Repo{Owner: "Freya", Name: "Hello-World"} - ctx.Set("repo", repo) - - // TODO(benschumacher) expand this a lot. - GetBadge(&ctx) - g.Assert(w.Status()).Equal(200) - - // Check the variety of states - for _, state := range []string{common.StateError, common.StateFailure, common.StateKilled, common.StatePending, common.StateRunning, common.StateSuccess} { - repo.Last = &common.Build{State: state} + repo.Last = &common.Build{ + Started: time.Now().UTC().Unix(), + Finished: time.Now().UTC().Unix(), + Number: idx, + State: state, + } ctx.Set("repo", repo) - GetBadge(&ctx) - g.Assert(w.Status()).Equal(200) + if expector != nil { + expector(&ctx) + } + + handle(&ctx) + + validator(state, w) } + } + + g.It("should provide SVG response", func() { + { + // 1. verify no "last" build + w := NewResponseRecorder() + ctx.Writer = w + ctx.Request.URL.Path += "/status.svg" + + GetBadge(&ctx) + + g.Assert(w.Status()).Equal(200) + g.Assert(w.HeaderMap.Get("content-type")).Equal("image/svg+xml") + g.Assert(strings.Contains(w.Body.String(), ">none")).IsTrue() + } + + // 2. verify a variety of "last" build states + cycleStateTester(nil, GetBadge, func(state string, w *ResponseRecorder) { + g.Assert(w.Status()).Equal(200) + g.Assert(w.HeaderMap.Get("content-type")).Equal("image/svg+xml") + + // this may be excessive, but does effectively verify behavior + switch state { + case common.StateSuccess: + g.Assert(strings.Contains(w.Body.String(), ">success")).IsTrue() + case common.StatePending, common.StateRunning: + g.Assert(strings.Contains(w.Body.String(), ">started")).IsTrue() + case common.StateError, common.StateKilled: + g.Assert(strings.Contains(w.Body.String(), ">error")).IsTrue() + case common.StateFailure: + g.Assert(strings.Contains(w.Body.String(), ">failure")).IsTrue() + default: + g.Assert(strings.Contains(w.Body.String(), ">none")).IsTrue() + } + }) }) - g.It("should provide CCTray response") /*, func() { - w := httptest.NewRecorder() - ctx.Writer = w - repo := &common.Repo{Owner: "Freya", Name: "Hello-World"} - ctx.Set("repo", repo) + g.It("should provide CCTray response", func() { + { + // 1. verify no "last" build + w := NewResponseRecorder() + ctx.Writer = w + ctx.Request.URL.Path += "/cc.xml" - }*/ + ds := new(mocks.Datastore) + ctx.Set("datastore", ds) + + ds.On("BuildLast", fullName).Return(nil, datastore.ErrKeyNotFound).Once() + GetCC(&ctx) + + g.Assert(w.Status()).Equal(404) + } + + // 2. verify a variety of "last" build states + cycleStateTester(func(c *gin.Context) { + repo := ToRepo(c) + ds := new(mocks.Datastore) + ctx.Set("datastore", ds) + ds.On("BuildLast", fullName).Return(repo.Last, nil).Once() + }, + GetCC, + func(state string, w *ResponseRecorder) { + g.Assert(w.Status()).Equal(200) + + v := ccmenu.CCProjects{} + xml.Unmarshal(w.Body.Bytes(), &v) + switch state { + case common.StateSuccess: + g.Assert(v.Project.Activity).Equal("Sleeping") + g.Assert(v.Project.LastBuildStatus).Equal("Success") + case common.StatePending, common.StateRunning: + g.Assert(v.Project.Activity).Equal("Building") + g.Assert(v.Project.LastBuildStatus).Equal("Unknown") + case common.StateError, common.StateKilled: + g.Assert(v.Project.Activity).Equal("Sleeping") + g.Assert(v.Project.LastBuildStatus).Equal("Exception") + case common.StateFailure: + g.Assert(v.Project.Activity).Equal("Sleeping") + g.Assert(v.Project.LastBuildStatus).Equal("Failure") + default: + g.Assert(v.Project.Activity).Equal("Sleeping") + g.Assert(v.Project.LastBuildStatus).Equal("Unknown") + } + }) + }) }) } diff --git a/server/mocks_test.go b/server/mocks_test.go deleted file mode 100644 index 91477b855..000000000 --- a/server/mocks_test.go +++ /dev/null @@ -1,19 +0,0 @@ -package server - -import ( - "net/http" - - "github.com/drone/drone/common" -) - -type MockSession struct { - Token *common.Token -} - -func (s MockSession) GenerateToken(*common.Token) (string, error) { - return "session", nil -} - -func (s MockSession) GetLogin(*http.Request) *common.Token { - return s.Token -} diff --git a/server/responserecorder_test.go b/server/responserecorder_test.go new file mode 100644 index 000000000..1ab5e1a9b --- /dev/null +++ b/server/responserecorder_test.go @@ -0,0 +1,33 @@ +package server + +import ( + "bufio" + "net" + "net/http" + "net/http/httptest" +) + +type ResponseRecorder struct { + *httptest.ResponseRecorder +} + +func NewResponseRecorder() *ResponseRecorder { + return &ResponseRecorder{httptest.NewRecorder()} +} + +func (rr *ResponseRecorder) reset() { + rr.ResponseRecorder = httptest.NewRecorder() +} + +func (rr *ResponseRecorder) CloseNotify() <-chan bool { + return http.ResponseWriter(rr).(http.CloseNotifier).CloseNotify() +} + +func (rr *ResponseRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) { + return http.ResponseWriter(rr).(http.Hijacker).Hijack() +} + +func (rr *ResponseRecorder) Size() int { return rr.Body.Len() } +func (rr *ResponseRecorder) Status() int { return rr.Code } +func (rr *ResponseRecorder) WriteHeaderNow() {} +func (rr *ResponseRecorder) Written() bool { return rr.Code != 0 } From 52b441527c5eacc8cb9475c8a74d3fb06078e79b Mon Sep 17 00:00:00 2001 From: Ben Schumacher Date: Tue, 28 Apr 2015 15:57:56 -0600 Subject: [PATCH 3/3] Fix spelling mistake --- server/badge_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/badge_test.go b/server/badge_test.go index 22e6b07f6..b498218a2 100644 --- a/server/badge_test.go +++ b/server/badge_test.go @@ -16,7 +16,7 @@ import ( "github.com/gin-gonic/gin" ) -func TestBadage(t *testing.T) { +func TestBadge(t *testing.T) { g := Goblin(t) g.Describe("Badge", func() { var ctx gin.Context