You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by GitBox <gi...@apache.org> on 2021/02/07 10:16:37 UTC

[GitHub] [apisix-dashboard] Jaycean opened a new pull request #1452: feat(be): refactor upstream unit test

Jaycean opened a new pull request #1452:
URL: https://github.com/apache/apisix-dashboard/pull/1452


   Please answer these questions before submitting a pull request
   
   - Why submit this pull request?
   - [ ] Bugfix
   - [x] New feature provided
   - [ ] Improve performance
   - [ ] Backport patches
   
   - Related issues
   
   ___
   ### Bugfix
   - Description
   Refactor upstream unit test.
   Remove etcd dependency of unit test.


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix-dashboard] Jaycean edited a comment on pull request #1452: feat(be): refactor upstream unit test

Posted by GitBox <gi...@apache.org>.
Jaycean edited a comment on pull request #1452:
URL: https://github.com/apache/apisix-dashboard/pull/1452#issuecomment-775716643


   >@starsz starsz 3 hours ago Contributor
   Yes.The same question I had meet.
   
   It's strange why I can't reply directly
   Yes, in fact, I learned from your writing here


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix-dashboard] Jaycean commented on pull request #1452: feat(be): refactor upstream unit test

Posted by GitBox <gi...@apache.org>.
Jaycean commented on pull request #1452:
URL: https://github.com/apache/apisix-dashboard/pull/1452#issuecomment-775716643


   https://github.com/apache/apisix-dashboard/pull/1452#discussion_r572544034
   It's strange why I can't reply directly
   Yes, in fact, I learned from your writing here


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix-dashboard] Jaycean commented on a change in pull request #1452: feat(be): refactor upstream unit test

Posted by GitBox <gi...@apache.org>.
Jaycean commented on a change in pull request #1452:
URL: https://github.com/apache/apisix-dashboard/pull/1452#discussion_r572632419



##########
File path: api/internal/handler/upstream/upstream_test.go
##########
@@ -19,285 +19,1719 @@ package upstream
 
 import (
 	"encoding/json"
-	"strings"
+	"errors"
+	"fmt"
+	"net/http"
 	"testing"
-	"time"
 
 	"github.com/shiningrush/droplet"
+	"github.com/shiningrush/droplet/data"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
 
-	"github.com/apisix/manager-api/internal/conf"
 	"github.com/apisix/manager-api/internal/core/entity"
-	"github.com/apisix/manager-api/internal/core/storage"
 	"github.com/apisix/manager-api/internal/core/store"
+	"github.com/apisix/manager-api/internal/handler"
+	"github.com/apisix/manager-api/internal/utils/consts"
 )
 
-var upstreamHandler *Handler
+func TestUpstream_Get(t *testing.T) {
+	tests := []struct {
+		caseDesc   string
+		giveInput  *GetInput
+		giveRet    *entity.Upstream
+		giveErr    error
+		wantErr    error
+		wantGetKey string
+		wantRet    interface{}
+	}{
+		{
+			caseDesc:   "upstream: get success",
+			giveInput:  &GetInput{ID: "u1"},
+			wantGetKey: "u1",
+			giveRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+		},
+		{
+			caseDesc:   "store get failed",
+			giveInput:  &GetInput{ID: "failed_key"},
+			wantGetKey: "failed_key",
+			giveErr:    fmt.Errorf("get failed"),
+			wantErr:    fmt.Errorf("get failed"),
+			wantRet: &data.SpecCodeResponse{
+				StatusCode: http.StatusInternalServerError,
+			},
+		},
+	}
 
-func TestUpstream(t *testing.T) {
-	// init
-	err := storage.InitETCDClient(conf.ETCDConfig)
-	assert.Nil(t, err)
-	err = store.InitStores()
-	assert.Nil(t, err)
+	for _, tc := range tests {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			getCalled := true
+			upstreamStore := &store.MockInterface{}
+			upstreamStore.On("Get", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				getCalled = true
+				assert.Equal(t, tc.wantGetKey, args.Get(0))
+			}).Return(tc.giveRet, tc.giveErr)
+
+			h := Handler{upstreamStore: upstreamStore}
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ret, err := h.Get(ctx)
+			assert.True(t, getCalled)
+			assert.Equal(t, tc.wantRet, ret)
+			assert.Equal(t, tc.wantErr, err)
+		})
+	}
+}
+
+func TestUpstreams_List(t *testing.T) {

Review comment:
       done.
   I modified the nodes data type in mockdata to test the format.
   




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix-dashboard] Jaycean commented on pull request #1452: feat(be): refactor upstream unit test

Posted by GitBox <gi...@apache.org>.
Jaycean commented on pull request #1452:
URL: https://github.com/apache/apisix-dashboard/pull/1452#issuecomment-774810574


   cc @starsz  @nic-chen  @imjoey 
   PTAL. Thks.


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix-dashboard] nic-chen commented on a change in pull request #1452: feat(be): refactor upstream unit test

Posted by GitBox <gi...@apache.org>.
nic-chen commented on a change in pull request #1452:
URL: https://github.com/apache/apisix-dashboard/pull/1452#discussion_r571956356



##########
File path: api/internal/handler/upstream/upstream.go
##########
@@ -270,27 +267,15 @@ func (h *Handler) Exist(c droplet.Context) (interface{}, error) {
 		return nil, err
 	}
 
-	sort := store.NewSort(nil)
-	filter := store.NewFilter([]string{"name", name})
-	pagination := store.NewPagination(0, 0)
-	query := store.NewQuery(sort, filter, pagination)
-	rows := store.NewFilterSelector(toRows(ret), query)

Review comment:
       @Jaycean  that's good, thanks.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix-dashboard] nic-chen commented on a change in pull request #1452: feat(be): refactor upstream unit test

Posted by GitBox <gi...@apache.org>.
nic-chen commented on a change in pull request #1452:
URL: https://github.com/apache/apisix-dashboard/pull/1452#discussion_r571882686



##########
File path: api/internal/handler/upstream/upstream.go
##########
@@ -270,27 +267,15 @@ func (h *Handler) Exist(c droplet.Context) (interface{}, error) {
 		return nil, err
 	}
 
-	sort := store.NewSort(nil)
-	filter := store.NewFilter([]string{"name", name})
-	pagination := store.NewPagination(0, 0)
-	query := store.NewQuery(sort, filter, pagination)
-	rows := store.NewFilterSelector(toRows(ret), query)

Review comment:
       maybe we could remove these functions now, please confirm whether these functions are invoked elsewhere.
   




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix-dashboard] juzhiyuan merged pull request #1452: feat(be): refactor upstream unit test

Posted by GitBox <gi...@apache.org>.
juzhiyuan merged pull request #1452:
URL: https://github.com/apache/apisix-dashboard/pull/1452


   


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix-dashboard] starsz commented on a change in pull request #1452: feat(be): refactor upstream unit test

Posted by GitBox <gi...@apache.org>.
starsz commented on a change in pull request #1452:
URL: https://github.com/apache/apisix-dashboard/pull/1452#discussion_r572542126



##########
File path: api/internal/handler/upstream/upstream_test.go
##########
@@ -19,285 +19,1719 @@ package upstream
 
 import (
 	"encoding/json"
-	"strings"
+	"errors"
+	"fmt"
+	"net/http"
 	"testing"
-	"time"
 
 	"github.com/shiningrush/droplet"
+	"github.com/shiningrush/droplet/data"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
 
-	"github.com/apisix/manager-api/internal/conf"
 	"github.com/apisix/manager-api/internal/core/entity"
-	"github.com/apisix/manager-api/internal/core/storage"
 	"github.com/apisix/manager-api/internal/core/store"
+	"github.com/apisix/manager-api/internal/handler"
+	"github.com/apisix/manager-api/internal/utils/consts"
 )
 
-var upstreamHandler *Handler
+func TestUpstream_Get(t *testing.T) {
+	tests := []struct {
+		caseDesc   string
+		giveInput  *GetInput
+		giveRet    *entity.Upstream
+		giveErr    error
+		wantErr    error
+		wantGetKey string
+		wantRet    interface{}
+	}{
+		{
+			caseDesc:   "upstream: get success",
+			giveInput:  &GetInput{ID: "u1"},
+			wantGetKey: "u1",
+			giveRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+		},
+		{
+			caseDesc:   "store get failed",
+			giveInput:  &GetInput{ID: "failed_key"},
+			wantGetKey: "failed_key",
+			giveErr:    fmt.Errorf("get failed"),
+			wantErr:    fmt.Errorf("get failed"),
+			wantRet: &data.SpecCodeResponse{
+				StatusCode: http.StatusInternalServerError,
+			},
+		},
+	}
 
-func TestUpstream(t *testing.T) {
-	// init
-	err := storage.InitETCDClient(conf.ETCDConfig)
-	assert.Nil(t, err)
-	err = store.InitStores()
-	assert.Nil(t, err)
+	for _, tc := range tests {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			getCalled := true
+			upstreamStore := &store.MockInterface{}
+			upstreamStore.On("Get", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				getCalled = true
+				assert.Equal(t, tc.wantGetKey, args.Get(0))
+			}).Return(tc.giveRet, tc.giveErr)
+
+			h := Handler{upstreamStore: upstreamStore}
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ret, err := h.Get(ctx)
+			assert.True(t, getCalled)
+			assert.Equal(t, tc.wantRet, ret)
+			assert.Equal(t, tc.wantErr, err)
+		})
+	}
+}
+
+func TestUpstreams_List(t *testing.T) {

Review comment:
       Maybe we should test the format in `List` function.

##########
File path: api/internal/handler/upstream/upstream_test.go
##########
@@ -19,285 +19,1719 @@ package upstream
 
 import (
 	"encoding/json"
-	"strings"
+	"errors"
+	"fmt"
+	"net/http"
 	"testing"
-	"time"
 
 	"github.com/shiningrush/droplet"
+	"github.com/shiningrush/droplet/data"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
 
-	"github.com/apisix/manager-api/internal/conf"
 	"github.com/apisix/manager-api/internal/core/entity"
-	"github.com/apisix/manager-api/internal/core/storage"
 	"github.com/apisix/manager-api/internal/core/store"
+	"github.com/apisix/manager-api/internal/handler"
+	"github.com/apisix/manager-api/internal/utils/consts"
 )
 
-var upstreamHandler *Handler
+func TestUpstream_Get(t *testing.T) {
+	tests := []struct {
+		caseDesc   string
+		giveInput  *GetInput
+		giveRet    *entity.Upstream
+		giveErr    error
+		wantErr    error
+		wantGetKey string
+		wantRet    interface{}
+	}{
+		{
+			caseDesc:   "upstream: get success",
+			giveInput:  &GetInput{ID: "u1"},
+			wantGetKey: "u1",
+			giveRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+		},
+		{
+			caseDesc:   "store get failed",
+			giveInput:  &GetInput{ID: "failed_key"},
+			wantGetKey: "failed_key",
+			giveErr:    fmt.Errorf("get failed"),
+			wantErr:    fmt.Errorf("get failed"),
+			wantRet: &data.SpecCodeResponse{
+				StatusCode: http.StatusInternalServerError,
+			},
+		},
+	}
 
-func TestUpstream(t *testing.T) {
-	// init
-	err := storage.InitETCDClient(conf.ETCDConfig)
-	assert.Nil(t, err)
-	err = store.InitStores()
-	assert.Nil(t, err)
+	for _, tc := range tests {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			getCalled := true
+			upstreamStore := &store.MockInterface{}
+			upstreamStore.On("Get", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				getCalled = true
+				assert.Equal(t, tc.wantGetKey, args.Get(0))
+			}).Return(tc.giveRet, tc.giveErr)
+
+			h := Handler{upstreamStore: upstreamStore}
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ret, err := h.Get(ctx)
+			assert.True(t, getCalled)
+			assert.Equal(t, tc.wantRet, ret)
+			assert.Equal(t, tc.wantErr, err)
+		})
+	}
+}
+
+func TestUpstreams_List(t *testing.T) {
+	mockData := []*entity.Upstream{
+		{
+			BaseInfo: entity.BaseInfo{
+				ID:         "u1",
+				CreateTime: 1609340491,
+				UpdateTime: 1609340491,
+			},
+			UpstreamDef: entity.UpstreamDef{
+				Name: "upstream1",
+				Key:  "server_addr",
+				Nodes: []map[string]interface{}{
+					{
+						"host":   "39.97.63.215",
+						"port":   float64(80),
+						"weight": float64(1),
+					},
+				},
+			},
+		},
+		{
+			BaseInfo: entity.BaseInfo{
+				ID:         "u2",
+				CreateTime: 1609340491,
+				UpdateTime: 1609340491,
+			},
+			UpstreamDef: entity.UpstreamDef{
+				Name: "upstream2",
+				Key:  "server_addr2",
+				Nodes: []map[string]interface{}{
+					{
+						"host":   "39.97.63.215",
+						"port":   float64(80),
+						"weight": float64(1),
+					},
+				},
+			},
+		},
+		{
+			BaseInfo: entity.BaseInfo{
+				ID:         "u3",
+				CreateTime: 1609340491,
+				UpdateTime: 1609340491,
+			},
+			UpstreamDef: entity.UpstreamDef{
+				Name: "upstream3",
+				Key:  "server_addr3",
+				Nodes: []map[string]interface{}{
+					{
+						"host":   "39.97.63.215",
+						"port":   float64(80),
+						"weight": float64(1),
+					},
+				},
+			},
+		},
+	}
 
-	upstreamHandler = &Handler{
-		upstreamStore: store.GetStore(store.HubKeyUpstream),
+	tests := []struct {
+		caseDesc  string
+		giveInput *ListInput
+		giveData  []*entity.Upstream
+		giveErr   error
+		wantErr   error
+		wantInput store.ListInput
+		wantRet   interface{}
+	}{
+		{
+			caseDesc: "list all upstream",
+			giveInput: &ListInput{
+				Pagination: store.Pagination{
+					PageSize:   10,
+					PageNumber: 10,
+				},
+			},
+			wantInput: store.ListInput{
+				PageSize:   10,
+				PageNumber: 10,
+			},
+			wantRet: &store.ListOutput{
+				Rows: []interface{}{
+					mockData[0],
+					mockData[1],
+					mockData[2],
+				},
+				TotalSize: 3,
+			},
+		},
+		{
+			caseDesc: "list upstream with 'upstream1'",
+			giveInput: &ListInput{
+				Name: "upstream1",
+				Pagination: store.Pagination{
+					PageSize:   10,
+					PageNumber: 10,
+				},
+			},
+			wantInput: store.ListInput{
+				PageSize:   10,
+				PageNumber: 10,
+			},
+			wantRet: &store.ListOutput{
+				Rows: []interface{}{
+					mockData[0],
+				},
+				TotalSize: 1,
+			},
+		},
 	}
-	assert.NotNil(t, upstreamHandler)
-
-	//create
-	ctx := droplet.NewContext()
-	upstream := &entity.Upstream{}
-	reqBody := `{
-		"id": "1",
-		"name": "upstream3",
-		"description": "upstream upstream",
-		"type": "roundrobin",
-		"nodes": [{
-			"host": "a.a.com",
-			"port": 80,
-			"weight": 1
-		}],
-		"timeout":{
-			"connect":15,
-			"send":15,
-			"read":15
-		},
-		"hash_on": "header",
-		"key": "server_addr",
-		"checks": {
-			"active": {
-				"timeout": 5,
-				"http_path": "/status",
-				"host": "foo.com",
-				"healthy": {
-					"interval": 2,
-					"successes": 1
-				},
-				"unhealthy": {
-					"interval": 1,
-					"http_failures": 2
-				},
-				"req_headers": ["User-Agent: curl/7.29.0"]
-			},
-			"passive": {
-				"healthy": {
-					"http_statuses": [200, 201],
-					"successes": 3
-				},
-				"unhealthy": {
-					"http_statuses": [500],
-					"http_failures": 3,
-					"tcp_failures": 3
+
+	for _, tc := range tests {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			getCalled := true
+			upstreamStore := &store.MockInterface{}
+			upstreamStore.On("List", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				getCalled = true
+				input := args.Get(0).(store.ListInput)
+				assert.Equal(t, tc.wantInput.PageSize, input.PageSize)
+				assert.Equal(t, tc.wantInput.PageNumber, input.PageNumber)
+			}).Return(func(input store.ListInput) *store.ListOutput {
+				var returnData []interface{}
+				for _, c := range mockData {
+					if input.Predicate(c) {
+						if input.Format == nil {
+							returnData = append(returnData, c)
+							continue
+						}
+						returnData = append(returnData, input.Format(c))
+					}
 				}
-			}
-		}
-	}`
-	err = json.Unmarshal([]byte(reqBody), upstream)
-	assert.Nil(t, err)
-	ctx.SetInput(upstream)
-	ret, err := upstreamHandler.Create(ctx)
-	assert.Nil(t, err)
-	objRet, ok := ret.(*entity.Upstream)
-	assert.True(t, ok)
-	assert.Equal(t, "1", objRet.ID)
-
-	//sleep
-	time.Sleep(time.Duration(100) * time.Millisecond)
-
-	//get
-	input := &GetInput{}
-	input.ID = "1"
-	ctx.SetInput(input)
-	ret, err = upstreamHandler.Get(ctx)
-	stored := ret.(*entity.Upstream)
-	assert.Nil(t, err)
-	assert.Equal(t, stored.ID, upstream.ID)
-
-	//update
-	upstream2 := &UpdateInput{}
-	upstream2.ID = "1"
-	reqBody = `{
-		"id": "1",
-		"name": "upstream3",
-		"description": "upstream upstream",
-		"type": "roundrobin",
-		"nodes": [{
-			"host": "a.a.com",
-			"port": 80,
-			"weight": 1
-		}],
-		"timeout":{
-			"connect":15,
-			"send":15,
-			"read":15
-		},
-		"enable_websocket": true,
-		"hash_on": "header",
-		"key": "server_addr",
-		"checks": {
-			"active": {
-				"timeout": 5,
-				"http_path": "/status",
-				"host": "foo.com",
-				"healthy": {
-					"interval": 2,
-					"successes": 1
-				},
-				"unhealthy": {
-					"interval": 1,
-					"http_failures": 2
-				},
-				"req_headers": ["User-Agent: curl/7.29.0"]
-			},
-			"passive": {
-				"healthy": {
-					"http_statuses": [200, 201],
-					"successes": 3
-				},
-				"unhealthy": {
-					"http_statuses": [500],
-					"http_failures": 3,
-					"tcp_failures": 3
+				return &store.ListOutput{
+					Rows:      returnData,
+					TotalSize: len(returnData),
 				}
-			}
-		}
-	}`
-	err = json.Unmarshal([]byte(reqBody), upstream2)
-	assert.Nil(t, err)
-	ctx.SetInput(upstream2)
-	ret, err = upstreamHandler.Update(ctx)
-	assert.Nil(t, err)
-	// check the returned value
-	objRet, ok = ret.(*entity.Upstream)
-	assert.True(t, ok)
-	assert.Equal(t, upstream2.ID, objRet.ID)
-
-	//list
-	listInput := &ListInput{}
-	reqBody = `{"page_size": 1, "page": 1}`
-	err = json.Unmarshal([]byte(reqBody), listInput)
-	assert.Nil(t, err)
-	ctx.SetInput(listInput)
-	retPage, err := upstreamHandler.List(ctx)
-	assert.Nil(t, err)
-	dataPage := retPage.(*store.ListOutput)
-	assert.Equal(t, len(dataPage.Rows), 1)
+			}, tc.giveErr)
 
-	//delete test data
-	inputDel := &BatchDelete{}
-	reqBody = `{"ids": "1"}`
-	err = json.Unmarshal([]byte(reqBody), inputDel)
-	assert.Nil(t, err)
-	ctx.SetInput(inputDel)
-	_, err = upstreamHandler.BatchDelete(ctx)
-	assert.Nil(t, err)
+			h := Handler{upstreamStore: upstreamStore}
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ret, err := h.List(ctx)
+			assert.True(t, getCalled)
+			assert.Equal(t, tc.wantRet, ret)
+			assert.Equal(t, tc.wantErr, err)
+		})
+	}
+}
+
+func TestUpstream_Create(t *testing.T) {
+	tests := []struct {
+		caseDesc  string
+		getCalled bool
+		giveInput *entity.Upstream
+		giveRet   interface{}
+		giveErr   error
+		wantInput *entity.Upstream
+		wantErr   error
+		wantRet   interface{}
+	}{
+		{
+			caseDesc:  "create success",
+			getCalled: true,
+			giveInput: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			giveRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantInput: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantErr: nil,
+		},
+		{
+			caseDesc:  "create failed, create return error",
+			getCalled: true,
+			giveInput: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			giveErr: fmt.Errorf("create failed"),
+			wantInput: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantErr: fmt.Errorf("create failed"),
+			wantRet: handler.SpecCodeResponse(fmt.Errorf("create failed")),
+		},
+	}
 
+	for _, tc := range tests {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			getCalled := false
+
+			upstreamStore := &store.MockInterface{}
+			upstreamStore.On("Create", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				getCalled = true
+				input := args.Get(1).(*entity.Upstream)
+				assert.Equal(t, tc.wantInput, input)
+			}).Return(tc.giveRet, tc.giveErr)
+
+			h := Handler{upstreamStore: upstreamStore}
+
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ret, err := h.Create(ctx)
+			assert.True(t, getCalled)
+			assert.Equal(t, tc.wantRet, ret)
+			assert.Equal(t, tc.wantErr, err)
+		})
+	}
 }
 
-func TestUpstream_Pass_Host(t *testing.T) {
-	//create
-	ctx := droplet.NewContext()
-	upstream := &entity.Upstream{}
-	reqBody := `{
-		"id": "2",
-		"nodes": [{
-			"host": "httpbin.org",
-			"port": 80,
-			"weight": 1
-		}],
-		"type": "roundrobin",
-		"pass_host": "node"
-	}`
-	err := json.Unmarshal([]byte(reqBody), upstream)
-	assert.Nil(t, err)
-	ctx.SetInput(upstream)
-	ret, err := upstreamHandler.Create(ctx)
-	assert.Nil(t, err)
-	objRet, ok := ret.(*entity.Upstream)
-	assert.True(t, ok)
-	assert.Equal(t, "2", objRet.ID)
-
-	//sleep
-	time.Sleep(time.Duration(20) * time.Millisecond)
-
-	//get
-	input := &GetInput{}
-	input.ID = "2"
-	ctx.SetInput(input)
-	ret, err = upstreamHandler.Get(ctx)
-	stored := ret.(*entity.Upstream)
-	assert.Nil(t, err)
-	assert.Equal(t, stored.ID, upstream.ID)
+func TestUpstream_Update(t *testing.T) {
+	tests := []struct {
+		caseDesc  string
+		getCalled bool
+		giveInput *UpdateInput
+		giveErr   error
+		giveRet   interface{}
+		wantInput *entity.Upstream
+		wantErr   error
+		wantRet   interface{}
+	}{
+		{
+			caseDesc:  "update success",
+			getCalled: true,
+			giveInput: &UpdateInput{
+				ID: "u1",
+				Upstream: entity.Upstream{
+					UpstreamDef: entity.UpstreamDef{
+						Name: "upstream1",
+						Timeout: map[string]interface{}{
+							"connect": 15,
+							"send":    15,
+							"read":    15,
+						},
+						Checks: map[string]interface{}{
+							"active": map[string]interface{}{
+								"timeout":   float64(5),
+								"http_path": "/status",
+								"host":      "foo.com",
+								"healthy": map[string]interface{}{
+									"interval":  2,
+									"successes": 1,
+								},
+								"unhealthy": map[string]interface{}{
+									"interval":      1,
+									"http_failures": 2,
+								},
+								"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+							},
+							"passive": map[string]interface{}{
+								"healthy": map[string]interface{}{
+									"http_statuses": []interface{}{float64(200), float64(201)},
+									"successes":     float64(3),
+								},
+								"unhealthy": map[string]interface{}{
+									"http_statuses": []interface{}{float64(500)},
+									"http_failures": 3,
+									"tcp_failures":  3,
+								},
+							},
+						},
+						Key: "server_addr",
+						Nodes: []map[string]interface{}{
+							{
+								"host":   "39.97.63.215",
+								"port":   float64(80),
+								"weight": float64(1),
+							},
+						},
+					},
+				},
+			},
+			giveRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": 3,
+								"tcp_failures":  3,
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantInput: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": 3,
+								"tcp_failures":  3,
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": 3,
+								"tcp_failures":  3,
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+		},
+		{
+			caseDesc: "create failed, different id",
+			giveInput: &UpdateInput{
+				ID: "u1",
+				Upstream: entity.Upstream{
+					BaseInfo: entity.BaseInfo{
+						ID: "u2",
+					},
+					UpstreamDef: entity.UpstreamDef{
+						Name: "upstream1",
+						Timeout: map[string]interface{}{
+							"connect": 15,
+							"send":    15,
+							"read":    15,
+						},
+						Checks: map[string]interface{}{
+							"active": map[string]interface{}{
+								"timeout":   float64(5),
+								"http_path": "/status",
+								"host":      "foo.com",
+								"healthy": map[string]interface{}{
+									"interval":  2,
+									"successes": 1,
+								},
+								"unhealthy": map[string]interface{}{
+									"interval":      1,
+									"http_failures": 2,
+								},
+								"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+							},
+							"passive": map[string]interface{}{
+								"healthy": map[string]interface{}{
+									"http_statuses": []interface{}{float64(200), float64(201)},
+									"successes":     float64(3),
+								},
+								"unhealthy": map[string]interface{}{
+									"http_statuses": []interface{}{float64(500)},
+									"http_failures": 3,
+									"tcp_failures":  3,
+								},
+							},
+						},
+						Key: "server_addr",
+						Nodes: []map[string]interface{}{
+							{
+								"host":   "39.97.63.215",
+								"port":   float64(80),
+								"weight": float64(1),
+							},
+						},
+					},
+				},
+			},
+			wantRet: &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
+			wantErr: fmt.Errorf("ID on path (u1) doesn't match ID on body (u2)"),
+		},

Review comment:
       Maybe we need to test `Update` failed.

##########
File path: api/internal/handler/upstream/upstream_test.go
##########
@@ -19,285 +19,1719 @@ package upstream
 
 import (
 	"encoding/json"
-	"strings"
+	"errors"
+	"fmt"
+	"net/http"
 	"testing"
-	"time"
 
 	"github.com/shiningrush/droplet"
+	"github.com/shiningrush/droplet/data"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
 
-	"github.com/apisix/manager-api/internal/conf"
 	"github.com/apisix/manager-api/internal/core/entity"
-	"github.com/apisix/manager-api/internal/core/storage"
 	"github.com/apisix/manager-api/internal/core/store"
+	"github.com/apisix/manager-api/internal/handler"
+	"github.com/apisix/manager-api/internal/utils/consts"
 )
 
-var upstreamHandler *Handler
+func TestUpstream_Get(t *testing.T) {
+	tests := []struct {
+		caseDesc   string
+		giveInput  *GetInput
+		giveRet    *entity.Upstream
+		giveErr    error
+		wantErr    error
+		wantGetKey string
+		wantRet    interface{}
+	}{
+		{
+			caseDesc:   "upstream: get success",
+			giveInput:  &GetInput{ID: "u1"},
+			wantGetKey: "u1",
+			giveRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+		},
+		{
+			caseDesc:   "store get failed",
+			giveInput:  &GetInput{ID: "failed_key"},
+			wantGetKey: "failed_key",
+			giveErr:    fmt.Errorf("get failed"),
+			wantErr:    fmt.Errorf("get failed"),
+			wantRet: &data.SpecCodeResponse{
+				StatusCode: http.StatusInternalServerError,
+			},
+		},
+	}
 
-func TestUpstream(t *testing.T) {
-	// init
-	err := storage.InitETCDClient(conf.ETCDConfig)
-	assert.Nil(t, err)
-	err = store.InitStores()
-	assert.Nil(t, err)
+	for _, tc := range tests {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			getCalled := true
+			upstreamStore := &store.MockInterface{}
+			upstreamStore.On("Get", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				getCalled = true
+				assert.Equal(t, tc.wantGetKey, args.Get(0))
+			}).Return(tc.giveRet, tc.giveErr)
+
+			h := Handler{upstreamStore: upstreamStore}
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ret, err := h.Get(ctx)
+			assert.True(t, getCalled)
+			assert.Equal(t, tc.wantRet, ret)
+			assert.Equal(t, tc.wantErr, err)
+		})
+	}
+}
+
+func TestUpstreams_List(t *testing.T) {
+	mockData := []*entity.Upstream{
+		{
+			BaseInfo: entity.BaseInfo{
+				ID:         "u1",
+				CreateTime: 1609340491,
+				UpdateTime: 1609340491,
+			},
+			UpstreamDef: entity.UpstreamDef{
+				Name: "upstream1",
+				Key:  "server_addr",
+				Nodes: []map[string]interface{}{
+					{
+						"host":   "39.97.63.215",
+						"port":   float64(80),
+						"weight": float64(1),
+					},
+				},
+			},
+		},
+		{
+			BaseInfo: entity.BaseInfo{
+				ID:         "u2",
+				CreateTime: 1609340491,
+				UpdateTime: 1609340491,
+			},
+			UpstreamDef: entity.UpstreamDef{
+				Name: "upstream2",
+				Key:  "server_addr2",
+				Nodes: []map[string]interface{}{
+					{
+						"host":   "39.97.63.215",
+						"port":   float64(80),
+						"weight": float64(1),
+					},
+				},
+			},
+		},
+		{
+			BaseInfo: entity.BaseInfo{
+				ID:         "u3",
+				CreateTime: 1609340491,
+				UpdateTime: 1609340491,
+			},
+			UpstreamDef: entity.UpstreamDef{
+				Name: "upstream3",
+				Key:  "server_addr3",
+				Nodes: []map[string]interface{}{
+					{
+						"host":   "39.97.63.215",
+						"port":   float64(80),
+						"weight": float64(1),
+					},
+				},
+			},
+		},
+	}
 
-	upstreamHandler = &Handler{
-		upstreamStore: store.GetStore(store.HubKeyUpstream),
+	tests := []struct {
+		caseDesc  string
+		giveInput *ListInput
+		giveData  []*entity.Upstream
+		giveErr   error
+		wantErr   error
+		wantInput store.ListInput
+		wantRet   interface{}
+	}{
+		{
+			caseDesc: "list all upstream",
+			giveInput: &ListInput{
+				Pagination: store.Pagination{
+					PageSize:   10,
+					PageNumber: 10,
+				},
+			},
+			wantInput: store.ListInput{
+				PageSize:   10,
+				PageNumber: 10,
+			},
+			wantRet: &store.ListOutput{
+				Rows: []interface{}{
+					mockData[0],
+					mockData[1],
+					mockData[2],
+				},
+				TotalSize: 3,
+			},
+		},
+		{
+			caseDesc: "list upstream with 'upstream1'",
+			giveInput: &ListInput{
+				Name: "upstream1",
+				Pagination: store.Pagination{
+					PageSize:   10,
+					PageNumber: 10,
+				},
+			},
+			wantInput: store.ListInput{
+				PageSize:   10,
+				PageNumber: 10,
+			},
+			wantRet: &store.ListOutput{
+				Rows: []interface{}{
+					mockData[0],
+				},
+				TotalSize: 1,
+			},
+		},
 	}
-	assert.NotNil(t, upstreamHandler)
-
-	//create
-	ctx := droplet.NewContext()
-	upstream := &entity.Upstream{}
-	reqBody := `{
-		"id": "1",
-		"name": "upstream3",
-		"description": "upstream upstream",
-		"type": "roundrobin",
-		"nodes": [{
-			"host": "a.a.com",
-			"port": 80,
-			"weight": 1
-		}],
-		"timeout":{
-			"connect":15,
-			"send":15,
-			"read":15
-		},
-		"hash_on": "header",
-		"key": "server_addr",
-		"checks": {
-			"active": {
-				"timeout": 5,
-				"http_path": "/status",
-				"host": "foo.com",
-				"healthy": {
-					"interval": 2,
-					"successes": 1
-				},
-				"unhealthy": {
-					"interval": 1,
-					"http_failures": 2
-				},
-				"req_headers": ["User-Agent: curl/7.29.0"]
-			},
-			"passive": {
-				"healthy": {
-					"http_statuses": [200, 201],
-					"successes": 3
-				},
-				"unhealthy": {
-					"http_statuses": [500],
-					"http_failures": 3,
-					"tcp_failures": 3
+
+	for _, tc := range tests {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			getCalled := true
+			upstreamStore := &store.MockInterface{}
+			upstreamStore.On("List", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				getCalled = true
+				input := args.Get(0).(store.ListInput)
+				assert.Equal(t, tc.wantInput.PageSize, input.PageSize)
+				assert.Equal(t, tc.wantInput.PageNumber, input.PageNumber)
+			}).Return(func(input store.ListInput) *store.ListOutput {
+				var returnData []interface{}
+				for _, c := range mockData {
+					if input.Predicate(c) {
+						if input.Format == nil {
+							returnData = append(returnData, c)
+							continue
+						}
+						returnData = append(returnData, input.Format(c))
+					}
 				}
-			}
-		}
-	}`
-	err = json.Unmarshal([]byte(reqBody), upstream)
-	assert.Nil(t, err)
-	ctx.SetInput(upstream)
-	ret, err := upstreamHandler.Create(ctx)
-	assert.Nil(t, err)
-	objRet, ok := ret.(*entity.Upstream)
-	assert.True(t, ok)
-	assert.Equal(t, "1", objRet.ID)
-
-	//sleep
-	time.Sleep(time.Duration(100) * time.Millisecond)
-
-	//get
-	input := &GetInput{}
-	input.ID = "1"
-	ctx.SetInput(input)
-	ret, err = upstreamHandler.Get(ctx)
-	stored := ret.(*entity.Upstream)
-	assert.Nil(t, err)
-	assert.Equal(t, stored.ID, upstream.ID)
-
-	//update
-	upstream2 := &UpdateInput{}
-	upstream2.ID = "1"
-	reqBody = `{
-		"id": "1",
-		"name": "upstream3",
-		"description": "upstream upstream",
-		"type": "roundrobin",
-		"nodes": [{
-			"host": "a.a.com",
-			"port": 80,
-			"weight": 1
-		}],
-		"timeout":{
-			"connect":15,
-			"send":15,
-			"read":15
-		},
-		"enable_websocket": true,
-		"hash_on": "header",
-		"key": "server_addr",
-		"checks": {
-			"active": {
-				"timeout": 5,
-				"http_path": "/status",
-				"host": "foo.com",
-				"healthy": {
-					"interval": 2,
-					"successes": 1
-				},
-				"unhealthy": {
-					"interval": 1,
-					"http_failures": 2
-				},
-				"req_headers": ["User-Agent: curl/7.29.0"]
-			},
-			"passive": {
-				"healthy": {
-					"http_statuses": [200, 201],
-					"successes": 3
-				},
-				"unhealthy": {
-					"http_statuses": [500],
-					"http_failures": 3,
-					"tcp_failures": 3
+				return &store.ListOutput{
+					Rows:      returnData,
+					TotalSize: len(returnData),
 				}
-			}
-		}
-	}`
-	err = json.Unmarshal([]byte(reqBody), upstream2)
-	assert.Nil(t, err)
-	ctx.SetInput(upstream2)
-	ret, err = upstreamHandler.Update(ctx)
-	assert.Nil(t, err)
-	// check the returned value
-	objRet, ok = ret.(*entity.Upstream)
-	assert.True(t, ok)
-	assert.Equal(t, upstream2.ID, objRet.ID)
-
-	//list
-	listInput := &ListInput{}
-	reqBody = `{"page_size": 1, "page": 1}`
-	err = json.Unmarshal([]byte(reqBody), listInput)
-	assert.Nil(t, err)
-	ctx.SetInput(listInput)
-	retPage, err := upstreamHandler.List(ctx)
-	assert.Nil(t, err)
-	dataPage := retPage.(*store.ListOutput)
-	assert.Equal(t, len(dataPage.Rows), 1)
+			}, tc.giveErr)
 
-	//delete test data
-	inputDel := &BatchDelete{}
-	reqBody = `{"ids": "1"}`
-	err = json.Unmarshal([]byte(reqBody), inputDel)
-	assert.Nil(t, err)
-	ctx.SetInput(inputDel)
-	_, err = upstreamHandler.BatchDelete(ctx)
-	assert.Nil(t, err)
+			h := Handler{upstreamStore: upstreamStore}
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ret, err := h.List(ctx)
+			assert.True(t, getCalled)
+			assert.Equal(t, tc.wantRet, ret)
+			assert.Equal(t, tc.wantErr, err)
+		})
+	}
+}
+
+func TestUpstream_Create(t *testing.T) {
+	tests := []struct {
+		caseDesc  string
+		getCalled bool
+		giveInput *entity.Upstream
+		giveRet   interface{}
+		giveErr   error
+		wantInput *entity.Upstream
+		wantErr   error
+		wantRet   interface{}
+	}{
+		{
+			caseDesc:  "create success",
+			getCalled: true,
+			giveInput: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			giveRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantInput: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantErr: nil,
+		},
+		{
+			caseDesc:  "create failed, create return error",
+			getCalled: true,
+			giveInput: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			giveErr: fmt.Errorf("create failed"),
+			wantInput: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantErr: fmt.Errorf("create failed"),
+			wantRet: handler.SpecCodeResponse(fmt.Errorf("create failed")),
+		},
+	}
 
+	for _, tc := range tests {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			getCalled := false
+
+			upstreamStore := &store.MockInterface{}
+			upstreamStore.On("Create", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				getCalled = true
+				input := args.Get(1).(*entity.Upstream)
+				assert.Equal(t, tc.wantInput, input)
+			}).Return(tc.giveRet, tc.giveErr)
+
+			h := Handler{upstreamStore: upstreamStore}
+
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ret, err := h.Create(ctx)
+			assert.True(t, getCalled)
+			assert.Equal(t, tc.wantRet, ret)
+			assert.Equal(t, tc.wantErr, err)
+		})
+	}
 }
 
-func TestUpstream_Pass_Host(t *testing.T) {
-	//create
-	ctx := droplet.NewContext()
-	upstream := &entity.Upstream{}
-	reqBody := `{
-		"id": "2",
-		"nodes": [{
-			"host": "httpbin.org",
-			"port": 80,
-			"weight": 1
-		}],
-		"type": "roundrobin",
-		"pass_host": "node"
-	}`
-	err := json.Unmarshal([]byte(reqBody), upstream)
-	assert.Nil(t, err)
-	ctx.SetInput(upstream)
-	ret, err := upstreamHandler.Create(ctx)
-	assert.Nil(t, err)
-	objRet, ok := ret.(*entity.Upstream)
-	assert.True(t, ok)
-	assert.Equal(t, "2", objRet.ID)
-
-	//sleep
-	time.Sleep(time.Duration(20) * time.Millisecond)
-
-	//get
-	input := &GetInput{}
-	input.ID = "2"
-	ctx.SetInput(input)
-	ret, err = upstreamHandler.Get(ctx)
-	stored := ret.(*entity.Upstream)
-	assert.Nil(t, err)
-	assert.Equal(t, stored.ID, upstream.ID)
+func TestUpstream_Update(t *testing.T) {
+	tests := []struct {
+		caseDesc  string
+		getCalled bool
+		giveInput *UpdateInput
+		giveErr   error
+		giveRet   interface{}
+		wantInput *entity.Upstream
+		wantErr   error
+		wantRet   interface{}
+	}{
+		{
+			caseDesc:  "update success",
+			getCalled: true,
+			giveInput: &UpdateInput{
+				ID: "u1",
+				Upstream: entity.Upstream{
+					UpstreamDef: entity.UpstreamDef{
+						Name: "upstream1",
+						Timeout: map[string]interface{}{
+							"connect": 15,
+							"send":    15,
+							"read":    15,
+						},
+						Checks: map[string]interface{}{
+							"active": map[string]interface{}{
+								"timeout":   float64(5),
+								"http_path": "/status",
+								"host":      "foo.com",
+								"healthy": map[string]interface{}{
+									"interval":  2,
+									"successes": 1,
+								},
+								"unhealthy": map[string]interface{}{
+									"interval":      1,
+									"http_failures": 2,
+								},
+								"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+							},
+							"passive": map[string]interface{}{
+								"healthy": map[string]interface{}{
+									"http_statuses": []interface{}{float64(200), float64(201)},
+									"successes":     float64(3),
+								},
+								"unhealthy": map[string]interface{}{
+									"http_statuses": []interface{}{float64(500)},
+									"http_failures": 3,
+									"tcp_failures":  3,
+								},
+							},
+						},
+						Key: "server_addr",
+						Nodes: []map[string]interface{}{
+							{
+								"host":   "39.97.63.215",
+								"port":   float64(80),
+								"weight": float64(1),
+							},
+						},
+					},
+				},
+			},
+			giveRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": 3,
+								"tcp_failures":  3,
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantInput: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": 3,
+								"tcp_failures":  3,
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": 3,
+								"tcp_failures":  3,
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+		},
+		{
+			caseDesc: "create failed, different id",
+			giveInput: &UpdateInput{
+				ID: "u1",
+				Upstream: entity.Upstream{
+					BaseInfo: entity.BaseInfo{
+						ID: "u2",
+					},
+					UpstreamDef: entity.UpstreamDef{
+						Name: "upstream1",
+						Timeout: map[string]interface{}{
+							"connect": 15,
+							"send":    15,
+							"read":    15,
+						},
+						Checks: map[string]interface{}{
+							"active": map[string]interface{}{
+								"timeout":   float64(5),
+								"http_path": "/status",
+								"host":      "foo.com",
+								"healthy": map[string]interface{}{
+									"interval":  2,
+									"successes": 1,
+								},
+								"unhealthy": map[string]interface{}{
+									"interval":      1,
+									"http_failures": 2,
+								},
+								"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+							},
+							"passive": map[string]interface{}{
+								"healthy": map[string]interface{}{
+									"http_statuses": []interface{}{float64(200), float64(201)},
+									"successes":     float64(3),
+								},
+								"unhealthy": map[string]interface{}{
+									"http_statuses": []interface{}{float64(500)},
+									"http_failures": 3,
+									"tcp_failures":  3,
+								},
+							},
+						},
+						Key: "server_addr",
+						Nodes: []map[string]interface{}{
+							{
+								"host":   "39.97.63.215",
+								"port":   float64(80),
+								"weight": float64(1),
+							},
+						},
+					},
+				},
+			},
+			wantRet: &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
+			wantErr: fmt.Errorf("ID on path (u1) doesn't match ID on body (u2)"),
+		},
+	}
 
-	//delete test data
-	inputDel := &BatchDelete{}
-	reqBody = `{"ids": "2"}`
-	err = json.Unmarshal([]byte(reqBody), inputDel)
-	assert.Nil(t, err)
-	ctx.SetInput(inputDel)
-	_, err = upstreamHandler.BatchDelete(ctx)
-	assert.Nil(t, err)
+	for _, tc := range tests {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			getCalled := false
+			upstreamStore := &store.MockInterface{}
+			upstreamStore.On("Update", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				getCalled = true
+				input := args.Get(1).(*entity.Upstream)
+				createIfNotExist := args.Get(2).(bool)
+				assert.Equal(t, tc.wantInput, input)
+				assert.True(t, createIfNotExist)
+			}).Return(tc.giveRet, tc.giveErr)
 
+			h := Handler{upstreamStore: upstreamStore}
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ret, err := h.Update(ctx)
+			assert.Equal(t, tc.getCalled, getCalled)
+			assert.Equal(t, tc.wantRet, ret)
+			assert.Equal(t, tc.wantErr, err)
+		})
+	}
 }
 
-func TestUpstream_Patch_Update(t *testing.T) {
-	//create
-	ctx := droplet.NewContext()
-	upstream := &entity.Upstream{}
-	reqBody := `{
-			"id": "3",
-			"nodes": [{
-				"host": "172.16.238.20",
-				"port": 1980,
-				"weight": 1
-			}],
-			"type": "roundrobin"
-		}`
-	err := json.Unmarshal([]byte(reqBody), upstream)
-	assert.Nil(t, err)
-	ctx.SetInput(upstream)
-	ret, err := upstreamHandler.Create(ctx)
-	assert.Nil(t, err)
-	objRet, ok := ret.(*entity.Upstream)
-	assert.True(t, ok)
-	assert.Equal(t, "3", objRet.ID)
-
-	//sleep
-	time.Sleep(time.Duration(20) * time.Millisecond)
-
-	reqBody1 := `{
-		"nodes": [{
-			"host": "172.16.238.20",
-			"port": 1981,
-			"weight": 1
-		}],
-		"type": "roundrobin"
-	}`
-	responesBody := `"nodes":[{"host":"172.16.238.20","port":1981,"weight":1}],"type":"roundrobin"}`
-
-	input2 := &PatchInput{}
-	input2.ID = "3"
-	input2.SubPath = ""
-	input2.Body = []byte(reqBody1)
-	ctx.SetInput(input2)
-
-	ret2, err := upstreamHandler.Patch(ctx)
-	assert.Nil(t, err)
-	_ret2, err := json.Marshal(ret2)
-	assert.Nil(t, err)
-	isContains := strings.Contains(string(_ret2), responesBody)
-	assert.True(t, isContains)
+func TestUpstream_Patch(t *testing.T) {
+	existUpstream := &entity.Upstream{
+		BaseInfo: entity.BaseInfo{
+			ID: "u1",
+		},
+		UpstreamDef: entity.UpstreamDef{
+			Name: "upstream1",
+			Timeout: map[string]interface{}{
+				"connect": 15,
+				"send":    15,
+				"read":    15,
+			},
+			Checks: map[string]interface{}{
+				"active": map[string]interface{}{
+					"timeout":   float64(5),
+					"http_path": "/status",
+					"host":      "foo.com",
+					"healthy": map[string]interface{}{
+						"interval":  float64(2),
+						"successes": float64(1),
+					},
+					"unhealthy": map[string]interface{}{
+						"interval":      float64(1),
+						"http_failures": float64(2),
+					},
+					"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+				},
+				"passive": map[string]interface{}{
+					"healthy": map[string]interface{}{
+						"http_statuses": []interface{}{float64(200), float64(201)},
+						"successes":     float64(3),
+					},
+					"unhealthy": map[string]interface{}{
+						"http_statuses": []interface{}{float64(500)},
+						"http_failures": 3,
+						"tcp_failures":  3,
+					},
+				},
+			},
+			Key: "server_addr",
+			Nodes: []interface{}{
+				map[string]interface{}{
+					"host":   "39.97.63.215",
+					"port":   float64(80),
+					"weight": float64(1),
+				},
+			},
+		},
+	}
 
-	//delete test data
-	inputDel2 := &BatchDelete{}
-	reqBody = `{"ids": "3"}`
-	err = json.Unmarshal([]byte(reqBody), inputDel2)
-	assert.Nil(t, err)
-	ctx.SetInput(inputDel2)
-	_, err = upstreamHandler.BatchDelete(ctx)
+	patchUpstream := &entity.Upstream{
+		BaseInfo: entity.BaseInfo{
+			ID: "u1",
+		},
+		UpstreamDef: entity.UpstreamDef{
+			Name: "upstream2",
+			Timeout: map[string]interface{}{
+				"connect": float64(20),
+				"send":    float64(20),
+				"read":    float64(20),
+			},
+			Checks: map[string]interface{}{
+				"active": map[string]interface{}{
+					"timeout":   float64(5),
+					"http_path": "/status",
+					"host":      "foo.com",
+					"healthy": map[string]interface{}{
+						"interval":  float64(2),
+						"successes": float64(1),
+					},
+					"unhealthy": map[string]interface{}{
+						"interval":      float64(1),
+						"http_failures": float64(2),
+					},
+					"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+				},
+				"passive": map[string]interface{}{
+					"healthy": map[string]interface{}{
+						"http_statuses": []interface{}{float64(200), float64(201)},
+						"successes":     float64(3),
+					},
+					"unhealthy": map[string]interface{}{
+						"http_statuses": []interface{}{float64(500)},
+						"http_failures": 3,
+						"tcp_failures":  3,
+					},
+				},
+			},
+			Key: "server_addr2",
+			Nodes: []interface{}{
+				map[string]interface{}{
+					"host":   "39.97.63.215",
+					"port":   float64(80),
+					"weight": float64(1),
+				},
+			},
+		},
+	}
+	patchUpstreamBytes, err := json.Marshal(patchUpstream)
 	assert.Nil(t, err)
 
+	tests := []struct {
+		caseDesc  string
+		getCalled bool
+		giveInput *PatchInput
+		giveErr   error
+		giveRet   interface{}
+		wantInput *entity.Upstream
+		wantErr   error
+		wantRet   interface{}
+	}{
+		{
+			caseDesc: "patch success",
+			giveRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream2",
+					Timeout: map[string]interface{}{
+						"connect": float64(20),
+						"send":    float64(20),
+						"read":    float64(20),
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  float64(2),
+								"successes": float64(1),
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      float64(1),
+								"http_failures": float64(2),
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr2",
+					Nodes: []interface{}{
+						map[string]interface{}{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			giveInput: &PatchInput{
+				ID:      "u1",
+				SubPath: "",
+				Body:    patchUpstreamBytes,
+			},
+			wantInput: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream2",
+					Timeout: map[string]interface{}{
+						"connect": float64(20),
+						"send":    float64(20),
+						"read":    float64(20),
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  float64(2),
+								"successes": float64(1),
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      float64(1),
+								"http_failures": float64(2),
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr2",
+					Nodes: []interface{}{
+						map[string]interface{}{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream2",
+					Timeout: map[string]interface{}{
+						"connect": float64(20),
+						"send":    float64(20),
+						"read":    float64(20),
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  float64(2),
+								"successes": float64(1),
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      float64(1),
+								"http_failures": float64(2),
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr2",
+					Nodes: []interface{}{
+						map[string]interface{}{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			getCalled: true,
+		},
+		{
+			caseDesc: "patch success by path",
+			giveInput: &PatchInput{
+				ID:      "u1",
+				SubPath: "/nodes",
+				Body:    []byte(`[{"host": "172.16.238.20","port": 1981,"weight": 1}]`),
+			},
+			giveRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": float64(20),
+						"send":    float64(20),
+						"read":    float64(20),
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr_patch",
+					Nodes: []interface{}{
+						map[string]interface{}{
+							"host":   "172.16.238.20",
+							"port":   float64(1981),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantInput: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": float64(15),
+						"send":    float64(15),
+						"read":    float64(15),
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  float64(2),
+								"successes": float64(1),
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      float64(1),
+								"http_failures": float64(2),
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []interface{}{
+						map[string]interface{}{
+							"host":   "172.16.238.20",
+							"port":   float64(1981),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": float64(20),
+						"send":    float64(20),
+						"read":    float64(20),
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr_patch",
+					Nodes: []interface{}{
+						map[string]interface{}{
+							"host":   "172.16.238.20",
+							"port":   float64(1981),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			getCalled: true,
+		},
+		{
+			caseDesc: "patch failed, path error",
+			giveInput: &PatchInput{
+				ID:      "u1",
+				SubPath: "error",
+				Body:    []byte("0"),
+			},
+			wantRet: handler.SpecCodeResponse(
+				errors.New("add operation does not apply: doc is missing path: \"error\": missing value")),
+			wantErr: errors.New("add operation does not apply: doc is missing path: \"error\": missing value"),
+		},
+	}
+	for _, tc := range tests {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			getCalled := false
+
+			upstreamStore := &store.MockInterface{}
+			upstreamStore.On("Get", mock.Anything, mock.Anything).Return(existUpstream, nil)
+			upstreamStore.On("Update", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				getCalled = true
+				input := args.Get(1).(*entity.Upstream)
+				createIfNotExist := args.Get(2).(bool)
+				assert.Equal(t, tc.wantInput, input)
+				assert.False(t, createIfNotExist)
+			}).Return(tc.giveRet, tc.giveErr)
+
+			h := Handler{upstreamStore: upstreamStore}
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ret, err := h.Patch(ctx)
+			assert.Equal(t, tc.getCalled, getCalled)
+			assert.Equal(t, tc.wantRet, ret)
+			if tc.wantErr != nil && err != nil {
+				assert.Error(t, tc.wantErr.(error), err.Error())
+			} else {
+				assert.Equal(t, tc.wantErr, err)
+			}

Review comment:
       Yes.The same question I had meet. 




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix-dashboard] Jaycean commented on a change in pull request #1452: feat(be): refactor upstream unit test

Posted by GitBox <gi...@apache.org>.
Jaycean commented on a change in pull request #1452:
URL: https://github.com/apache/apisix-dashboard/pull/1452#discussion_r571795912



##########
File path: api/internal/handler/upstream/upstream_test.go
##########
@@ -19,285 +19,1719 @@ package upstream
 
 import (
 	"encoding/json"
-	"strings"
+	"errors"
+	"fmt"
+	"net/http"
 	"testing"
-	"time"
 
 	"github.com/shiningrush/droplet"
+	"github.com/shiningrush/droplet/data"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
 
-	"github.com/apisix/manager-api/internal/conf"
 	"github.com/apisix/manager-api/internal/core/entity"
-	"github.com/apisix/manager-api/internal/core/storage"
 	"github.com/apisix/manager-api/internal/core/store"
+	"github.com/apisix/manager-api/internal/handler"
+	"github.com/apisix/manager-api/internal/utils/consts"
 )
 
-var upstreamHandler *Handler
+func TestUpstream_Get(t *testing.T) {
+	tests := []struct {
+		caseDesc   string
+		giveInput  *GetInput
+		giveRet    *entity.Upstream
+		giveErr    error
+		wantErr    error
+		wantGetKey string
+		wantRet    interface{}
+	}{
+		{
+			caseDesc:   "upstream: get success",
+			giveInput:  &GetInput{ID: "u1"},
+			wantGetKey: "u1",
+			giveRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+		},
+		{
+			caseDesc:   "store get failed",
+			giveInput:  &GetInput{ID: "failed_key"},
+			wantGetKey: "failed_key",
+			giveErr:    fmt.Errorf("get failed"),
+			wantErr:    fmt.Errorf("get failed"),
+			wantRet: &data.SpecCodeResponse{
+				StatusCode: http.StatusInternalServerError,
+			},
+		},
+	}
 
-func TestUpstream(t *testing.T) {
-	// init
-	err := storage.InitETCDClient(conf.ETCDConfig)
-	assert.Nil(t, err)
-	err = store.InitStores()
-	assert.Nil(t, err)
+	for _, tc := range tests {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			getCalled := true
+			upstreamStore := &store.MockInterface{}
+			upstreamStore.On("Get", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				getCalled = true
+				assert.Equal(t, tc.wantGetKey, args.Get(0))
+			}).Return(tc.giveRet, tc.giveErr)
+
+			h := Handler{upstreamStore: upstreamStore}
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ret, err := h.Get(ctx)
+			assert.True(t, getCalled)
+			assert.Equal(t, tc.wantRet, ret)
+			assert.Equal(t, tc.wantErr, err)
+		})
+	}
+}
+
+func TestUpstreams_List(t *testing.T) {
+	mockData := []*entity.Upstream{
+		{
+			BaseInfo: entity.BaseInfo{
+				ID:         "u1",
+				CreateTime: 1609340491,
+				UpdateTime: 1609340491,
+			},
+			UpstreamDef: entity.UpstreamDef{
+				Name: "upstream1",
+				Key:  "server_addr",
+				Nodes: []map[string]interface{}{
+					{
+						"host":   "39.97.63.215",
+						"port":   float64(80),
+						"weight": float64(1),
+					},
+				},
+			},
+		},
+		{
+			BaseInfo: entity.BaseInfo{
+				ID:         "u2",
+				CreateTime: 1609340491,
+				UpdateTime: 1609340491,
+			},
+			UpstreamDef: entity.UpstreamDef{
+				Name: "upstream2",
+				Key:  "server_addr2",
+				Nodes: []map[string]interface{}{
+					{
+						"host":   "39.97.63.215",
+						"port":   float64(80),
+						"weight": float64(1),
+					},
+				},
+			},
+		},
+		{
+			BaseInfo: entity.BaseInfo{
+				ID:         "u3",
+				CreateTime: 1609340491,
+				UpdateTime: 1609340491,
+			},
+			UpstreamDef: entity.UpstreamDef{
+				Name: "upstream3",
+				Key:  "server_addr3",
+				Nodes: []map[string]interface{}{
+					{
+						"host":   "39.97.63.215",
+						"port":   float64(80),
+						"weight": float64(1),
+					},
+				},
+			},
+		},
+	}
 
-	upstreamHandler = &Handler{
-		upstreamStore: store.GetStore(store.HubKeyUpstream),
+	tests := []struct {
+		caseDesc  string
+		giveInput *ListInput
+		giveData  []*entity.Upstream
+		giveErr   error
+		wantErr   error
+		wantInput store.ListInput
+		wantRet   interface{}
+	}{
+		{
+			caseDesc: "list all upstream",
+			giveInput: &ListInput{
+				Pagination: store.Pagination{
+					PageSize:   10,
+					PageNumber: 10,
+				},
+			},
+			wantInput: store.ListInput{
+				PageSize:   10,
+				PageNumber: 10,
+			},
+			wantRet: &store.ListOutput{
+				Rows: []interface{}{
+					mockData[0],
+					mockData[1],
+					mockData[2],
+				},
+				TotalSize: 3,
+			},
+		},
+		{
+			caseDesc: "list upstream with 'upstream1'",
+			giveInput: &ListInput{
+				Name: "upstream1",
+				Pagination: store.Pagination{
+					PageSize:   10,
+					PageNumber: 10,
+				},
+			},
+			wantInput: store.ListInput{
+				PageSize:   10,
+				PageNumber: 10,
+			},
+			wantRet: &store.ListOutput{
+				Rows: []interface{}{
+					mockData[0],
+				},
+				TotalSize: 1,
+			},
+		},
 	}
-	assert.NotNil(t, upstreamHandler)
-
-	//create
-	ctx := droplet.NewContext()
-	upstream := &entity.Upstream{}
-	reqBody := `{
-		"id": "1",
-		"name": "upstream3",
-		"description": "upstream upstream",
-		"type": "roundrobin",
-		"nodes": [{
-			"host": "a.a.com",
-			"port": 80,
-			"weight": 1
-		}],
-		"timeout":{
-			"connect":15,
-			"send":15,
-			"read":15
-		},
-		"hash_on": "header",
-		"key": "server_addr",
-		"checks": {
-			"active": {
-				"timeout": 5,
-				"http_path": "/status",
-				"host": "foo.com",
-				"healthy": {
-					"interval": 2,
-					"successes": 1
-				},
-				"unhealthy": {
-					"interval": 1,
-					"http_failures": 2
-				},
-				"req_headers": ["User-Agent: curl/7.29.0"]
-			},
-			"passive": {
-				"healthy": {
-					"http_statuses": [200, 201],
-					"successes": 3
-				},
-				"unhealthy": {
-					"http_statuses": [500],
-					"http_failures": 3,
-					"tcp_failures": 3
+
+	for _, tc := range tests {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			getCalled := true
+			upstreamStore := &store.MockInterface{}
+			upstreamStore.On("List", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				getCalled = true
+				input := args.Get(0).(store.ListInput)
+				assert.Equal(t, tc.wantInput.PageSize, input.PageSize)
+				assert.Equal(t, tc.wantInput.PageNumber, input.PageNumber)
+			}).Return(func(input store.ListInput) *store.ListOutput {
+				var returnData []interface{}
+				for _, c := range mockData {
+					if input.Predicate(c) {
+						if input.Format == nil {
+							returnData = append(returnData, c)
+							continue
+						}
+						returnData = append(returnData, input.Format(c))
+					}
 				}
-			}
-		}
-	}`
-	err = json.Unmarshal([]byte(reqBody), upstream)
-	assert.Nil(t, err)
-	ctx.SetInput(upstream)
-	ret, err := upstreamHandler.Create(ctx)
-	assert.Nil(t, err)
-	objRet, ok := ret.(*entity.Upstream)
-	assert.True(t, ok)
-	assert.Equal(t, "1", objRet.ID)
-
-	//sleep
-	time.Sleep(time.Duration(100) * time.Millisecond)
-
-	//get
-	input := &GetInput{}
-	input.ID = "1"
-	ctx.SetInput(input)
-	ret, err = upstreamHandler.Get(ctx)
-	stored := ret.(*entity.Upstream)
-	assert.Nil(t, err)
-	assert.Equal(t, stored.ID, upstream.ID)
-
-	//update
-	upstream2 := &UpdateInput{}
-	upstream2.ID = "1"
-	reqBody = `{
-		"id": "1",
-		"name": "upstream3",
-		"description": "upstream upstream",
-		"type": "roundrobin",
-		"nodes": [{
-			"host": "a.a.com",
-			"port": 80,
-			"weight": 1
-		}],
-		"timeout":{
-			"connect":15,
-			"send":15,
-			"read":15
-		},
-		"enable_websocket": true,
-		"hash_on": "header",
-		"key": "server_addr",
-		"checks": {
-			"active": {
-				"timeout": 5,
-				"http_path": "/status",
-				"host": "foo.com",
-				"healthy": {
-					"interval": 2,
-					"successes": 1
-				},
-				"unhealthy": {
-					"interval": 1,
-					"http_failures": 2
-				},
-				"req_headers": ["User-Agent: curl/7.29.0"]
-			},
-			"passive": {
-				"healthy": {
-					"http_statuses": [200, 201],
-					"successes": 3
-				},
-				"unhealthy": {
-					"http_statuses": [500],
-					"http_failures": 3,
-					"tcp_failures": 3
+				return &store.ListOutput{
+					Rows:      returnData,
+					TotalSize: len(returnData),
 				}
-			}
-		}
-	}`
-	err = json.Unmarshal([]byte(reqBody), upstream2)
-	assert.Nil(t, err)
-	ctx.SetInput(upstream2)
-	ret, err = upstreamHandler.Update(ctx)
-	assert.Nil(t, err)
-	// check the returned value
-	objRet, ok = ret.(*entity.Upstream)
-	assert.True(t, ok)
-	assert.Equal(t, upstream2.ID, objRet.ID)
-
-	//list
-	listInput := &ListInput{}
-	reqBody = `{"page_size": 1, "page": 1}`
-	err = json.Unmarshal([]byte(reqBody), listInput)
-	assert.Nil(t, err)
-	ctx.SetInput(listInput)
-	retPage, err := upstreamHandler.List(ctx)
-	assert.Nil(t, err)
-	dataPage := retPage.(*store.ListOutput)
-	assert.Equal(t, len(dataPage.Rows), 1)
+			}, tc.giveErr)
 
-	//delete test data
-	inputDel := &BatchDelete{}
-	reqBody = `{"ids": "1"}`
-	err = json.Unmarshal([]byte(reqBody), inputDel)
-	assert.Nil(t, err)
-	ctx.SetInput(inputDel)
-	_, err = upstreamHandler.BatchDelete(ctx)
-	assert.Nil(t, err)
+			h := Handler{upstreamStore: upstreamStore}
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ret, err := h.List(ctx)
+			assert.True(t, getCalled)
+			assert.Equal(t, tc.wantRet, ret)
+			assert.Equal(t, tc.wantErr, err)
+		})
+	}
+}
+
+func TestUpstream_Create(t *testing.T) {
+	tests := []struct {
+		caseDesc  string
+		getCalled bool
+		giveInput *entity.Upstream
+		giveRet   interface{}
+		giveErr   error
+		wantInput *entity.Upstream
+		wantErr   error
+		wantRet   interface{}
+	}{
+		{
+			caseDesc:  "create success",
+			getCalled: true,
+			giveInput: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			giveRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantInput: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantErr: nil,
+		},
+		{
+			caseDesc:  "create failed, create return error",
+			getCalled: true,
+			giveInput: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			giveErr: fmt.Errorf("create failed"),
+			wantInput: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantErr: fmt.Errorf("create failed"),
+			wantRet: handler.SpecCodeResponse(fmt.Errorf("create failed")),
+		},
+	}
 
+	for _, tc := range tests {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			getCalled := false
+
+			upstreamStore := &store.MockInterface{}
+			upstreamStore.On("Create", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				getCalled = true
+				input := args.Get(1).(*entity.Upstream)
+				assert.Equal(t, tc.wantInput, input)
+			}).Return(tc.giveRet, tc.giveErr)
+
+			h := Handler{upstreamStore: upstreamStore}
+
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ret, err := h.Create(ctx)
+			assert.True(t, getCalled)
+			assert.Equal(t, tc.wantRet, ret)
+			assert.Equal(t, tc.wantErr, err)
+		})
+	}
 }
 
-func TestUpstream_Pass_Host(t *testing.T) {
-	//create
-	ctx := droplet.NewContext()
-	upstream := &entity.Upstream{}
-	reqBody := `{
-		"id": "2",
-		"nodes": [{
-			"host": "httpbin.org",
-			"port": 80,
-			"weight": 1
-		}],
-		"type": "roundrobin",
-		"pass_host": "node"
-	}`
-	err := json.Unmarshal([]byte(reqBody), upstream)
-	assert.Nil(t, err)
-	ctx.SetInput(upstream)
-	ret, err := upstreamHandler.Create(ctx)
-	assert.Nil(t, err)
-	objRet, ok := ret.(*entity.Upstream)
-	assert.True(t, ok)
-	assert.Equal(t, "2", objRet.ID)
-
-	//sleep
-	time.Sleep(time.Duration(20) * time.Millisecond)
-
-	//get
-	input := &GetInput{}
-	input.ID = "2"
-	ctx.SetInput(input)
-	ret, err = upstreamHandler.Get(ctx)
-	stored := ret.(*entity.Upstream)
-	assert.Nil(t, err)
-	assert.Equal(t, stored.ID, upstream.ID)
+func TestUpstream_Update(t *testing.T) {
+	tests := []struct {
+		caseDesc  string
+		getCalled bool
+		giveInput *UpdateInput
+		giveErr   error
+		giveRet   interface{}
+		wantInput *entity.Upstream
+		wantErr   error
+		wantRet   interface{}
+	}{
+		{
+			caseDesc:  "update success",
+			getCalled: true,
+			giveInput: &UpdateInput{
+				ID: "u1",
+				Upstream: entity.Upstream{
+					UpstreamDef: entity.UpstreamDef{
+						Name: "upstream1",
+						Timeout: map[string]interface{}{
+							"connect": 15,
+							"send":    15,
+							"read":    15,
+						},
+						Checks: map[string]interface{}{
+							"active": map[string]interface{}{
+								"timeout":   float64(5),
+								"http_path": "/status",
+								"host":      "foo.com",
+								"healthy": map[string]interface{}{
+									"interval":  2,
+									"successes": 1,
+								},
+								"unhealthy": map[string]interface{}{
+									"interval":      1,
+									"http_failures": 2,
+								},
+								"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+							},
+							"passive": map[string]interface{}{
+								"healthy": map[string]interface{}{
+									"http_statuses": []interface{}{float64(200), float64(201)},
+									"successes":     float64(3),
+								},
+								"unhealthy": map[string]interface{}{
+									"http_statuses": []interface{}{float64(500)},
+									"http_failures": 3,
+									"tcp_failures":  3,
+								},
+							},
+						},
+						Key: "server_addr",
+						Nodes: []map[string]interface{}{
+							{
+								"host":   "39.97.63.215",
+								"port":   float64(80),
+								"weight": float64(1),
+							},
+						},
+					},
+				},
+			},
+			giveRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": 3,
+								"tcp_failures":  3,
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantInput: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": 3,
+								"tcp_failures":  3,
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": 3,
+								"tcp_failures":  3,
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+		},
+		{
+			caseDesc: "create failed, different id",
+			giveInput: &UpdateInput{
+				ID: "u1",
+				Upstream: entity.Upstream{
+					BaseInfo: entity.BaseInfo{
+						ID: "u2",
+					},
+					UpstreamDef: entity.UpstreamDef{
+						Name: "upstream1",
+						Timeout: map[string]interface{}{
+							"connect": 15,
+							"send":    15,
+							"read":    15,
+						},
+						Checks: map[string]interface{}{
+							"active": map[string]interface{}{
+								"timeout":   float64(5),
+								"http_path": "/status",
+								"host":      "foo.com",
+								"healthy": map[string]interface{}{
+									"interval":  2,
+									"successes": 1,
+								},
+								"unhealthy": map[string]interface{}{
+									"interval":      1,
+									"http_failures": 2,
+								},
+								"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+							},
+							"passive": map[string]interface{}{
+								"healthy": map[string]interface{}{
+									"http_statuses": []interface{}{float64(200), float64(201)},
+									"successes":     float64(3),
+								},
+								"unhealthy": map[string]interface{}{
+									"http_statuses": []interface{}{float64(500)},
+									"http_failures": 3,
+									"tcp_failures":  3,
+								},
+							},
+						},
+						Key: "server_addr",
+						Nodes: []map[string]interface{}{
+							{
+								"host":   "39.97.63.215",
+								"port":   float64(80),
+								"weight": float64(1),
+							},
+						},
+					},
+				},
+			},
+			wantRet: &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
+			wantErr: fmt.Errorf("ID on path (u1) doesn't match ID on body (u2)"),
+		},
+	}
 
-	//delete test data
-	inputDel := &BatchDelete{}
-	reqBody = `{"ids": "2"}`
-	err = json.Unmarshal([]byte(reqBody), inputDel)
-	assert.Nil(t, err)
-	ctx.SetInput(inputDel)
-	_, err = upstreamHandler.BatchDelete(ctx)
-	assert.Nil(t, err)
+	for _, tc := range tests {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			getCalled := false
+			upstreamStore := &store.MockInterface{}
+			upstreamStore.On("Update", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				getCalled = true
+				input := args.Get(1).(*entity.Upstream)
+				createIfNotExist := args.Get(2).(bool)
+				assert.Equal(t, tc.wantInput, input)
+				assert.True(t, createIfNotExist)
+			}).Return(tc.giveRet, tc.giveErr)
 
+			h := Handler{upstreamStore: upstreamStore}
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ret, err := h.Update(ctx)
+			assert.Equal(t, tc.getCalled, getCalled)
+			assert.Equal(t, tc.wantRet, ret)
+			assert.Equal(t, tc.wantErr, err)
+		})
+	}
 }
 
-func TestUpstream_Patch_Update(t *testing.T) {
-	//create
-	ctx := droplet.NewContext()
-	upstream := &entity.Upstream{}
-	reqBody := `{
-			"id": "3",
-			"nodes": [{
-				"host": "172.16.238.20",
-				"port": 1980,
-				"weight": 1
-			}],
-			"type": "roundrobin"
-		}`
-	err := json.Unmarshal([]byte(reqBody), upstream)
-	assert.Nil(t, err)
-	ctx.SetInput(upstream)
-	ret, err := upstreamHandler.Create(ctx)
-	assert.Nil(t, err)
-	objRet, ok := ret.(*entity.Upstream)
-	assert.True(t, ok)
-	assert.Equal(t, "3", objRet.ID)
-
-	//sleep
-	time.Sleep(time.Duration(20) * time.Millisecond)
-
-	reqBody1 := `{
-		"nodes": [{
-			"host": "172.16.238.20",
-			"port": 1981,
-			"weight": 1
-		}],
-		"type": "roundrobin"
-	}`
-	responesBody := `"nodes":[{"host":"172.16.238.20","port":1981,"weight":1}],"type":"roundrobin"}`
-
-	input2 := &PatchInput{}
-	input2.ID = "3"
-	input2.SubPath = ""
-	input2.Body = []byte(reqBody1)
-	ctx.SetInput(input2)
-
-	ret2, err := upstreamHandler.Patch(ctx)
-	assert.Nil(t, err)
-	_ret2, err := json.Marshal(ret2)
-	assert.Nil(t, err)
-	isContains := strings.Contains(string(_ret2), responesBody)
-	assert.True(t, isContains)
+func TestUpstream_Patch(t *testing.T) {
+	existUpstream := &entity.Upstream{
+		BaseInfo: entity.BaseInfo{
+			ID: "u1",
+		},
+		UpstreamDef: entity.UpstreamDef{
+			Name: "upstream1",
+			Timeout: map[string]interface{}{
+				"connect": 15,
+				"send":    15,
+				"read":    15,
+			},
+			Checks: map[string]interface{}{
+				"active": map[string]interface{}{
+					"timeout":   float64(5),
+					"http_path": "/status",
+					"host":      "foo.com",
+					"healthy": map[string]interface{}{
+						"interval":  float64(2),
+						"successes": float64(1),
+					},
+					"unhealthy": map[string]interface{}{
+						"interval":      float64(1),
+						"http_failures": float64(2),
+					},
+					"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+				},
+				"passive": map[string]interface{}{
+					"healthy": map[string]interface{}{
+						"http_statuses": []interface{}{float64(200), float64(201)},
+						"successes":     float64(3),
+					},
+					"unhealthy": map[string]interface{}{
+						"http_statuses": []interface{}{float64(500)},
+						"http_failures": 3,
+						"tcp_failures":  3,
+					},
+				},
+			},
+			Key: "server_addr",
+			Nodes: []interface{}{
+				map[string]interface{}{
+					"host":   "39.97.63.215",
+					"port":   float64(80),
+					"weight": float64(1),
+				},
+			},
+		},
+	}
 
-	//delete test data
-	inputDel2 := &BatchDelete{}
-	reqBody = `{"ids": "3"}`
-	err = json.Unmarshal([]byte(reqBody), inputDel2)
-	assert.Nil(t, err)
-	ctx.SetInput(inputDel2)
-	_, err = upstreamHandler.BatchDelete(ctx)
+	patchUpstream := &entity.Upstream{
+		BaseInfo: entity.BaseInfo{
+			ID: "u1",
+		},
+		UpstreamDef: entity.UpstreamDef{
+			Name: "upstream2",
+			Timeout: map[string]interface{}{
+				"connect": float64(20),
+				"send":    float64(20),
+				"read":    float64(20),
+			},
+			Checks: map[string]interface{}{
+				"active": map[string]interface{}{
+					"timeout":   float64(5),
+					"http_path": "/status",
+					"host":      "foo.com",
+					"healthy": map[string]interface{}{
+						"interval":  float64(2),
+						"successes": float64(1),
+					},
+					"unhealthy": map[string]interface{}{
+						"interval":      float64(1),
+						"http_failures": float64(2),
+					},
+					"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+				},
+				"passive": map[string]interface{}{
+					"healthy": map[string]interface{}{
+						"http_statuses": []interface{}{float64(200), float64(201)},
+						"successes":     float64(3),
+					},
+					"unhealthy": map[string]interface{}{
+						"http_statuses": []interface{}{float64(500)},
+						"http_failures": 3,
+						"tcp_failures":  3,
+					},
+				},
+			},
+			Key: "server_addr2",
+			Nodes: []interface{}{
+				map[string]interface{}{
+					"host":   "39.97.63.215",
+					"port":   float64(80),
+					"weight": float64(1),
+				},
+			},
+		},
+	}
+	patchUpstreamBytes, err := json.Marshal(patchUpstream)
 	assert.Nil(t, err)
 
+	tests := []struct {
+		caseDesc  string
+		getCalled bool
+		giveInput *PatchInput
+		giveErr   error
+		giveRet   interface{}
+		wantInput *entity.Upstream
+		wantErr   error
+		wantRet   interface{}
+	}{
+		{
+			caseDesc: "patch success",
+			giveRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream2",
+					Timeout: map[string]interface{}{
+						"connect": float64(20),
+						"send":    float64(20),
+						"read":    float64(20),
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  float64(2),
+								"successes": float64(1),
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      float64(1),
+								"http_failures": float64(2),
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr2",
+					Nodes: []interface{}{
+						map[string]interface{}{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			giveInput: &PatchInput{
+				ID:      "u1",
+				SubPath: "",
+				Body:    patchUpstreamBytes,
+			},
+			wantInput: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream2",
+					Timeout: map[string]interface{}{
+						"connect": float64(20),
+						"send":    float64(20),
+						"read":    float64(20),
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  float64(2),
+								"successes": float64(1),
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      float64(1),
+								"http_failures": float64(2),
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr2",
+					Nodes: []interface{}{
+						map[string]interface{}{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream2",
+					Timeout: map[string]interface{}{
+						"connect": float64(20),
+						"send":    float64(20),
+						"read":    float64(20),
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  float64(2),
+								"successes": float64(1),
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      float64(1),
+								"http_failures": float64(2),
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr2",
+					Nodes: []interface{}{
+						map[string]interface{}{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			getCalled: true,
+		},
+		{
+			caseDesc: "patch success by path",
+			giveInput: &PatchInput{
+				ID:      "u1",
+				SubPath: "/nodes",
+				Body:    []byte(`[{"host": "172.16.238.20","port": 1981,"weight": 1}]`),
+			},
+			giveRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": float64(20),
+						"send":    float64(20),
+						"read":    float64(20),
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr_patch",
+					Nodes: []interface{}{
+						map[string]interface{}{
+							"host":   "172.16.238.20",
+							"port":   float64(1981),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantInput: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": float64(15),
+						"send":    float64(15),
+						"read":    float64(15),
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  float64(2),
+								"successes": float64(1),
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      float64(1),
+								"http_failures": float64(2),
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []interface{}{
+						map[string]interface{}{
+							"host":   "172.16.238.20",
+							"port":   float64(1981),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": float64(20),
+						"send":    float64(20),
+						"read":    float64(20),
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr_patch",
+					Nodes: []interface{}{
+						map[string]interface{}{
+							"host":   "172.16.238.20",
+							"port":   float64(1981),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			getCalled: true,
+		},
+		{
+			caseDesc: "patch failed, path error",
+			giveInput: &PatchInput{
+				ID:      "u1",
+				SubPath: "error",
+				Body:    []byte("0"),
+			},
+			wantRet: handler.SpecCodeResponse(
+				errors.New("add operation does not apply: doc is missing path: \"error\": missing value")),
+			wantErr: errors.New("add operation does not apply: doc is missing path: \"error\": missing value"),
+		},
+	}
+	for _, tc := range tests {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			getCalled := false
+
+			upstreamStore := &store.MockInterface{}
+			upstreamStore.On("Get", mock.Anything, mock.Anything).Return(existUpstream, nil)
+			upstreamStore.On("Update", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				getCalled = true
+				input := args.Get(1).(*entity.Upstream)
+				createIfNotExist := args.Get(2).(bool)
+				assert.Equal(t, tc.wantInput, input)
+				assert.False(t, createIfNotExist)
+			}).Return(tc.giveRet, tc.giveErr)
+
+			h := Handler{upstreamStore: upstreamStore}
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ret, err := h.Patch(ctx)
+			assert.Equal(t, tc.getCalled, getCalled)
+			assert.Equal(t, tc.wantRet, ret)
+			if tc.wantErr != nil && err != nil {
+				assert.Error(t, tc.wantErr.(error), err.Error())
+			} else {
+				assert.Equal(t, tc.wantErr, err)
+			}

Review comment:
       The patch method uses the package:`github.com/evanphx/json -Patch/V5`, when the error message is returned, the error is converted to match the error type, otherwise it cannot be matched.
   Using wantret errors.New Create, geterr returns error.WithStack , error type with stack.
   ```
   expected: *errors.errorString(&errors.errorString{s:"add operation does not apply: doc is missing path: \"error\": missing value"})
   actual  : *errors.withStack(add operation does not apply: doc is missing path: "error": missing value)
   ```




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix-dashboard] Jaycean commented on pull request #1452: feat(be): refactor upstream unit test

Posted by GitBox <gi...@apache.org>.
Jaycean commented on pull request #1452:
URL: https://github.com/apache/apisix-dashboard/pull/1452#issuecomment-775079236


   > @Jaycean What is the current code coverage of `upstream.go`? It's a little strange that this pr doesn't show coverage
   
   yeah,My local run test coverage is 79.1%
   ```
       --- PASS: TestUpstream_ListUpstreamNames/get_upstream_list_names (0.00s)
       --- PASS: TestUpstream_ListUpstreamNames/get_upstream_list_names_nil (0.00s)
   PASS
   coverage: 79.1% of statements
   ok      github.com/apisix/manager-api/internal/handler/upstream 1.091s  coverage: 79.1% of statements
   ```


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix-dashboard] Jaycean edited a comment on pull request #1452: feat(be): refactor upstream unit test

Posted by GitBox <gi...@apache.org>.
Jaycean edited a comment on pull request #1452:
URL: https://github.com/apache/apisix-dashboard/pull/1452#issuecomment-775079236


   > @Jaycean What is the current code coverage of `upstream.go`? It's a little strange that this pr doesn't show coverage
   
   yeah,My local run test coverage is 79.1%
   ```
       --- PASS: TestUpstream_ListUpstreamNames/get_upstream_list_names (0.00s)
       --- PASS: TestUpstream_ListUpstreamNames/get_upstream_list_names_nil (0.00s)
   PASS
   coverage: 79.1% of statements
   ok      github.com/apisix/manager-api/internal/handler/upstream 1.091s  coverage: 79.1% of statements
   ```
   
   It's really a bit strange. I'll see if there's a similar situation in other PR, no codecov report.


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix-dashboard] imjoey commented on a change in pull request #1452: feat(be): refactor upstream unit test

Posted by GitBox <gi...@apache.org>.
imjoey commented on a change in pull request #1452:
URL: https://github.com/apache/apisix-dashboard/pull/1452#discussion_r573030977



##########
File path: api/internal/handler/upstream/upstream_test.go
##########
@@ -19,285 +19,1719 @@ package upstream
 
 import (
 	"encoding/json"
-	"strings"
+	"errors"
+	"fmt"
+	"net/http"
 	"testing"
-	"time"
 
 	"github.com/shiningrush/droplet"
+	"github.com/shiningrush/droplet/data"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
 
-	"github.com/apisix/manager-api/internal/conf"
 	"github.com/apisix/manager-api/internal/core/entity"
-	"github.com/apisix/manager-api/internal/core/storage"
 	"github.com/apisix/manager-api/internal/core/store"
+	"github.com/apisix/manager-api/internal/handler"
+	"github.com/apisix/manager-api/internal/utils/consts"
 )
 
-var upstreamHandler *Handler
+func TestUpstream_Get(t *testing.T) {
+	tests := []struct {
+		caseDesc   string
+		giveInput  *GetInput
+		giveRet    *entity.Upstream
+		giveErr    error
+		wantErr    error
+		wantGetKey string
+		wantRet    interface{}
+	}{
+		{
+			caseDesc:   "upstream: get success",
+			giveInput:  &GetInput{ID: "u1"},
+			wantGetKey: "u1",
+			giveRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+		},
+		{
+			caseDesc:   "store get failed",
+			giveInput:  &GetInput{ID: "failed_key"},
+			wantGetKey: "failed_key",
+			giveErr:    fmt.Errorf("get failed"),
+			wantErr:    fmt.Errorf("get failed"),
+			wantRet: &data.SpecCodeResponse{
+				StatusCode: http.StatusInternalServerError,
+			},
+		},
+	}
 
-func TestUpstream(t *testing.T) {
-	// init
-	err := storage.InitETCDClient(conf.ETCDConfig)
-	assert.Nil(t, err)
-	err = store.InitStores()
-	assert.Nil(t, err)
+	for _, tc := range tests {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			getCalled := true
+			upstreamStore := &store.MockInterface{}
+			upstreamStore.On("Get", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				getCalled = true
+				assert.Equal(t, tc.wantGetKey, args.Get(0))
+			}).Return(tc.giveRet, tc.giveErr)
+
+			h := Handler{upstreamStore: upstreamStore}
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ret, err := h.Get(ctx)
+			assert.True(t, getCalled)
+			assert.Equal(t, tc.wantRet, ret)
+			assert.Equal(t, tc.wantErr, err)
+		})
+	}
+}
+
+func TestUpstreams_List(t *testing.T) {
+	mockData := []*entity.Upstream{
+		{
+			BaseInfo: entity.BaseInfo{
+				ID:         "u1",
+				CreateTime: 1609340491,
+				UpdateTime: 1609340491,
+			},
+			UpstreamDef: entity.UpstreamDef{
+				Name: "upstream1",
+				Key:  "server_addr",
+				Nodes: []map[string]interface{}{
+					{
+						"host":   "39.97.63.215",
+						"port":   float64(80),
+						"weight": float64(1),
+					},
+				},
+			},
+		},
+		{
+			BaseInfo: entity.BaseInfo{
+				ID:         "u2",
+				CreateTime: 1609340491,
+				UpdateTime: 1609340491,
+			},
+			UpstreamDef: entity.UpstreamDef{
+				Name: "upstream2",
+				Key:  "server_addr2",
+				Nodes: []map[string]interface{}{
+					{
+						"host":   "39.97.63.215",
+						"port":   float64(80),
+						"weight": float64(1),
+					},
+				},
+			},
+		},
+		{
+			BaseInfo: entity.BaseInfo{
+				ID:         "u3",
+				CreateTime: 1609340491,
+				UpdateTime: 1609340491,
+			},
+			UpstreamDef: entity.UpstreamDef{
+				Name: "upstream3",
+				Key:  "server_addr3",
+				Nodes: []map[string]interface{}{
+					{
+						"host":   "39.97.63.215",
+						"port":   float64(80),
+						"weight": float64(1),
+					},
+				},
+			},
+		},
+	}
 
-	upstreamHandler = &Handler{
-		upstreamStore: store.GetStore(store.HubKeyUpstream),
+	tests := []struct {
+		caseDesc  string
+		giveInput *ListInput
+		giveData  []*entity.Upstream
+		giveErr   error
+		wantErr   error
+		wantInput store.ListInput
+		wantRet   interface{}
+	}{
+		{
+			caseDesc: "list all upstream",
+			giveInput: &ListInput{
+				Pagination: store.Pagination{
+					PageSize:   10,
+					PageNumber: 10,
+				},
+			},
+			wantInput: store.ListInput{
+				PageSize:   10,
+				PageNumber: 10,
+			},
+			wantRet: &store.ListOutput{
+				Rows: []interface{}{
+					mockData[0],
+					mockData[1],
+					mockData[2],
+				},
+				TotalSize: 3,
+			},
+		},
+		{
+			caseDesc: "list upstream with 'upstream1'",
+			giveInput: &ListInput{
+				Name: "upstream1",
+				Pagination: store.Pagination{
+					PageSize:   10,
+					PageNumber: 10,
+				},
+			},
+			wantInput: store.ListInput{
+				PageSize:   10,
+				PageNumber: 10,
+			},
+			wantRet: &store.ListOutput{
+				Rows: []interface{}{
+					mockData[0],
+				},
+				TotalSize: 1,
+			},
+		},
 	}
-	assert.NotNil(t, upstreamHandler)
-
-	//create
-	ctx := droplet.NewContext()
-	upstream := &entity.Upstream{}
-	reqBody := `{
-		"id": "1",
-		"name": "upstream3",
-		"description": "upstream upstream",
-		"type": "roundrobin",
-		"nodes": [{
-			"host": "a.a.com",
-			"port": 80,
-			"weight": 1
-		}],
-		"timeout":{
-			"connect":15,
-			"send":15,
-			"read":15
-		},
-		"hash_on": "header",
-		"key": "server_addr",
-		"checks": {
-			"active": {
-				"timeout": 5,
-				"http_path": "/status",
-				"host": "foo.com",
-				"healthy": {
-					"interval": 2,
-					"successes": 1
-				},
-				"unhealthy": {
-					"interval": 1,
-					"http_failures": 2
-				},
-				"req_headers": ["User-Agent: curl/7.29.0"]
-			},
-			"passive": {
-				"healthy": {
-					"http_statuses": [200, 201],
-					"successes": 3
-				},
-				"unhealthy": {
-					"http_statuses": [500],
-					"http_failures": 3,
-					"tcp_failures": 3
+
+	for _, tc := range tests {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			getCalled := true
+			upstreamStore := &store.MockInterface{}
+			upstreamStore.On("List", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				getCalled = true
+				input := args.Get(0).(store.ListInput)
+				assert.Equal(t, tc.wantInput.PageSize, input.PageSize)
+				assert.Equal(t, tc.wantInput.PageNumber, input.PageNumber)
+			}).Return(func(input store.ListInput) *store.ListOutput {
+				var returnData []interface{}
+				for _, c := range mockData {
+					if input.Predicate(c) {
+						if input.Format == nil {
+							returnData = append(returnData, c)
+							continue
+						}
+						returnData = append(returnData, input.Format(c))
+					}
 				}
-			}
-		}
-	}`
-	err = json.Unmarshal([]byte(reqBody), upstream)
-	assert.Nil(t, err)
-	ctx.SetInput(upstream)
-	ret, err := upstreamHandler.Create(ctx)
-	assert.Nil(t, err)
-	objRet, ok := ret.(*entity.Upstream)
-	assert.True(t, ok)
-	assert.Equal(t, "1", objRet.ID)
-
-	//sleep
-	time.Sleep(time.Duration(100) * time.Millisecond)
-
-	//get
-	input := &GetInput{}
-	input.ID = "1"
-	ctx.SetInput(input)
-	ret, err = upstreamHandler.Get(ctx)
-	stored := ret.(*entity.Upstream)
-	assert.Nil(t, err)
-	assert.Equal(t, stored.ID, upstream.ID)
-
-	//update
-	upstream2 := &UpdateInput{}
-	upstream2.ID = "1"
-	reqBody = `{
-		"id": "1",
-		"name": "upstream3",
-		"description": "upstream upstream",
-		"type": "roundrobin",
-		"nodes": [{
-			"host": "a.a.com",
-			"port": 80,
-			"weight": 1
-		}],
-		"timeout":{
-			"connect":15,
-			"send":15,
-			"read":15
-		},
-		"enable_websocket": true,
-		"hash_on": "header",
-		"key": "server_addr",
-		"checks": {
-			"active": {
-				"timeout": 5,
-				"http_path": "/status",
-				"host": "foo.com",
-				"healthy": {
-					"interval": 2,
-					"successes": 1
-				},
-				"unhealthy": {
-					"interval": 1,
-					"http_failures": 2
-				},
-				"req_headers": ["User-Agent: curl/7.29.0"]
-			},
-			"passive": {
-				"healthy": {
-					"http_statuses": [200, 201],
-					"successes": 3
-				},
-				"unhealthy": {
-					"http_statuses": [500],
-					"http_failures": 3,
-					"tcp_failures": 3
+				return &store.ListOutput{
+					Rows:      returnData,
+					TotalSize: len(returnData),
 				}
-			}
-		}
-	}`
-	err = json.Unmarshal([]byte(reqBody), upstream2)
-	assert.Nil(t, err)
-	ctx.SetInput(upstream2)
-	ret, err = upstreamHandler.Update(ctx)
-	assert.Nil(t, err)
-	// check the returned value
-	objRet, ok = ret.(*entity.Upstream)
-	assert.True(t, ok)
-	assert.Equal(t, upstream2.ID, objRet.ID)
-
-	//list
-	listInput := &ListInput{}
-	reqBody = `{"page_size": 1, "page": 1}`
-	err = json.Unmarshal([]byte(reqBody), listInput)
-	assert.Nil(t, err)
-	ctx.SetInput(listInput)
-	retPage, err := upstreamHandler.List(ctx)
-	assert.Nil(t, err)
-	dataPage := retPage.(*store.ListOutput)
-	assert.Equal(t, len(dataPage.Rows), 1)
+			}, tc.giveErr)
 
-	//delete test data
-	inputDel := &BatchDelete{}
-	reqBody = `{"ids": "1"}`
-	err = json.Unmarshal([]byte(reqBody), inputDel)
-	assert.Nil(t, err)
-	ctx.SetInput(inputDel)
-	_, err = upstreamHandler.BatchDelete(ctx)
-	assert.Nil(t, err)
+			h := Handler{upstreamStore: upstreamStore}
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ret, err := h.List(ctx)
+			assert.True(t, getCalled)
+			assert.Equal(t, tc.wantRet, ret)
+			assert.Equal(t, tc.wantErr, err)
+		})
+	}
+}
+
+func TestUpstream_Create(t *testing.T) {
+	tests := []struct {
+		caseDesc  string
+		getCalled bool
+		giveInput *entity.Upstream
+		giveRet   interface{}
+		giveErr   error
+		wantInput *entity.Upstream
+		wantErr   error
+		wantRet   interface{}
+	}{
+		{
+			caseDesc:  "create success",
+			getCalled: true,
+			giveInput: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			giveRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantInput: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantErr: nil,
+		},
+		{
+			caseDesc:  "create failed, create return error",
+			getCalled: true,
+			giveInput: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			giveErr: fmt.Errorf("create failed"),
+			wantInput: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantErr: fmt.Errorf("create failed"),
+			wantRet: handler.SpecCodeResponse(fmt.Errorf("create failed")),
+		},
+	}
 
+	for _, tc := range tests {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			getCalled := false
+
+			upstreamStore := &store.MockInterface{}
+			upstreamStore.On("Create", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				getCalled = true
+				input := args.Get(1).(*entity.Upstream)
+				assert.Equal(t, tc.wantInput, input)
+			}).Return(tc.giveRet, tc.giveErr)
+
+			h := Handler{upstreamStore: upstreamStore}
+
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ret, err := h.Create(ctx)
+			assert.True(t, getCalled)
+			assert.Equal(t, tc.wantRet, ret)
+			assert.Equal(t, tc.wantErr, err)
+		})
+	}
 }
 
-func TestUpstream_Pass_Host(t *testing.T) {
-	//create
-	ctx := droplet.NewContext()
-	upstream := &entity.Upstream{}
-	reqBody := `{
-		"id": "2",
-		"nodes": [{
-			"host": "httpbin.org",
-			"port": 80,
-			"weight": 1
-		}],
-		"type": "roundrobin",
-		"pass_host": "node"
-	}`
-	err := json.Unmarshal([]byte(reqBody), upstream)
-	assert.Nil(t, err)
-	ctx.SetInput(upstream)
-	ret, err := upstreamHandler.Create(ctx)
-	assert.Nil(t, err)
-	objRet, ok := ret.(*entity.Upstream)
-	assert.True(t, ok)
-	assert.Equal(t, "2", objRet.ID)
-
-	//sleep
-	time.Sleep(time.Duration(20) * time.Millisecond)
-
-	//get
-	input := &GetInput{}
-	input.ID = "2"
-	ctx.SetInput(input)
-	ret, err = upstreamHandler.Get(ctx)
-	stored := ret.(*entity.Upstream)
-	assert.Nil(t, err)
-	assert.Equal(t, stored.ID, upstream.ID)
+func TestUpstream_Update(t *testing.T) {
+	tests := []struct {
+		caseDesc  string
+		getCalled bool
+		giveInput *UpdateInput
+		giveErr   error
+		giveRet   interface{}
+		wantInput *entity.Upstream
+		wantErr   error
+		wantRet   interface{}
+	}{
+		{
+			caseDesc:  "update success",
+			getCalled: true,
+			giveInput: &UpdateInput{
+				ID: "u1",
+				Upstream: entity.Upstream{
+					UpstreamDef: entity.UpstreamDef{
+						Name: "upstream1",
+						Timeout: map[string]interface{}{
+							"connect": 15,
+							"send":    15,
+							"read":    15,
+						},
+						Checks: map[string]interface{}{
+							"active": map[string]interface{}{
+								"timeout":   float64(5),
+								"http_path": "/status",
+								"host":      "foo.com",
+								"healthy": map[string]interface{}{
+									"interval":  2,
+									"successes": 1,
+								},
+								"unhealthy": map[string]interface{}{
+									"interval":      1,
+									"http_failures": 2,
+								},
+								"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+							},
+							"passive": map[string]interface{}{
+								"healthy": map[string]interface{}{
+									"http_statuses": []interface{}{float64(200), float64(201)},
+									"successes":     float64(3),
+								},
+								"unhealthy": map[string]interface{}{
+									"http_statuses": []interface{}{float64(500)},
+									"http_failures": 3,
+									"tcp_failures":  3,
+								},
+							},
+						},
+						Key: "server_addr",
+						Nodes: []map[string]interface{}{
+							{
+								"host":   "39.97.63.215",
+								"port":   float64(80),
+								"weight": float64(1),
+							},
+						},
+					},
+				},
+			},
+			giveRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": 3,
+								"tcp_failures":  3,
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantInput: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": 3,
+								"tcp_failures":  3,
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": 3,
+								"tcp_failures":  3,
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+		},
+		{
+			caseDesc: "create failed, different id",
+			giveInput: &UpdateInput{
+				ID: "u1",
+				Upstream: entity.Upstream{
+					BaseInfo: entity.BaseInfo{
+						ID: "u2",
+					},
+					UpstreamDef: entity.UpstreamDef{
+						Name: "upstream1",
+						Timeout: map[string]interface{}{
+							"connect": 15,
+							"send":    15,
+							"read":    15,
+						},
+						Checks: map[string]interface{}{
+							"active": map[string]interface{}{
+								"timeout":   float64(5),
+								"http_path": "/status",
+								"host":      "foo.com",
+								"healthy": map[string]interface{}{
+									"interval":  2,
+									"successes": 1,
+								},
+								"unhealthy": map[string]interface{}{
+									"interval":      1,
+									"http_failures": 2,
+								},
+								"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+							},
+							"passive": map[string]interface{}{
+								"healthy": map[string]interface{}{
+									"http_statuses": []interface{}{float64(200), float64(201)},
+									"successes":     float64(3),
+								},
+								"unhealthy": map[string]interface{}{
+									"http_statuses": []interface{}{float64(500)},
+									"http_failures": 3,
+									"tcp_failures":  3,
+								},
+							},
+						},
+						Key: "server_addr",
+						Nodes: []map[string]interface{}{
+							{
+								"host":   "39.97.63.215",
+								"port":   float64(80),
+								"weight": float64(1),
+							},
+						},
+					},
+				},
+			},
+			wantRet: &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
+			wantErr: fmt.Errorf("ID on path (u1) doesn't match ID on body (u2)"),
+		},
+	}
 
-	//delete test data
-	inputDel := &BatchDelete{}
-	reqBody = `{"ids": "2"}`
-	err = json.Unmarshal([]byte(reqBody), inputDel)
-	assert.Nil(t, err)
-	ctx.SetInput(inputDel)
-	_, err = upstreamHandler.BatchDelete(ctx)
-	assert.Nil(t, err)
+	for _, tc := range tests {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			getCalled := false
+			upstreamStore := &store.MockInterface{}
+			upstreamStore.On("Update", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				getCalled = true
+				input := args.Get(1).(*entity.Upstream)
+				createIfNotExist := args.Get(2).(bool)
+				assert.Equal(t, tc.wantInput, input)
+				assert.True(t, createIfNotExist)
+			}).Return(tc.giveRet, tc.giveErr)
 
+			h := Handler{upstreamStore: upstreamStore}
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ret, err := h.Update(ctx)
+			assert.Equal(t, tc.getCalled, getCalled)
+			assert.Equal(t, tc.wantRet, ret)
+			assert.Equal(t, tc.wantErr, err)
+		})
+	}
 }
 
-func TestUpstream_Patch_Update(t *testing.T) {
-	//create
-	ctx := droplet.NewContext()
-	upstream := &entity.Upstream{}
-	reqBody := `{
-			"id": "3",
-			"nodes": [{
-				"host": "172.16.238.20",
-				"port": 1980,
-				"weight": 1
-			}],
-			"type": "roundrobin"
-		}`
-	err := json.Unmarshal([]byte(reqBody), upstream)
-	assert.Nil(t, err)
-	ctx.SetInput(upstream)
-	ret, err := upstreamHandler.Create(ctx)
-	assert.Nil(t, err)
-	objRet, ok := ret.(*entity.Upstream)
-	assert.True(t, ok)
-	assert.Equal(t, "3", objRet.ID)
-
-	//sleep
-	time.Sleep(time.Duration(20) * time.Millisecond)
-
-	reqBody1 := `{
-		"nodes": [{
-			"host": "172.16.238.20",
-			"port": 1981,
-			"weight": 1
-		}],
-		"type": "roundrobin"
-	}`
-	responesBody := `"nodes":[{"host":"172.16.238.20","port":1981,"weight":1}],"type":"roundrobin"}`
-
-	input2 := &PatchInput{}
-	input2.ID = "3"
-	input2.SubPath = ""
-	input2.Body = []byte(reqBody1)
-	ctx.SetInput(input2)
-
-	ret2, err := upstreamHandler.Patch(ctx)
-	assert.Nil(t, err)
-	_ret2, err := json.Marshal(ret2)
-	assert.Nil(t, err)
-	isContains := strings.Contains(string(_ret2), responesBody)
-	assert.True(t, isContains)
+func TestUpstream_Patch(t *testing.T) {
+	existUpstream := &entity.Upstream{
+		BaseInfo: entity.BaseInfo{
+			ID: "u1",
+		},
+		UpstreamDef: entity.UpstreamDef{
+			Name: "upstream1",
+			Timeout: map[string]interface{}{
+				"connect": 15,
+				"send":    15,
+				"read":    15,
+			},
+			Checks: map[string]interface{}{
+				"active": map[string]interface{}{
+					"timeout":   float64(5),
+					"http_path": "/status",
+					"host":      "foo.com",
+					"healthy": map[string]interface{}{
+						"interval":  float64(2),
+						"successes": float64(1),
+					},
+					"unhealthy": map[string]interface{}{
+						"interval":      float64(1),
+						"http_failures": float64(2),
+					},
+					"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+				},
+				"passive": map[string]interface{}{
+					"healthy": map[string]interface{}{
+						"http_statuses": []interface{}{float64(200), float64(201)},
+						"successes":     float64(3),
+					},
+					"unhealthy": map[string]interface{}{
+						"http_statuses": []interface{}{float64(500)},
+						"http_failures": 3,
+						"tcp_failures":  3,
+					},
+				},
+			},
+			Key: "server_addr",
+			Nodes: []interface{}{
+				map[string]interface{}{
+					"host":   "39.97.63.215",
+					"port":   float64(80),
+					"weight": float64(1),
+				},
+			},
+		},
+	}
 
-	//delete test data
-	inputDel2 := &BatchDelete{}
-	reqBody = `{"ids": "3"}`
-	err = json.Unmarshal([]byte(reqBody), inputDel2)
-	assert.Nil(t, err)
-	ctx.SetInput(inputDel2)
-	_, err = upstreamHandler.BatchDelete(ctx)
+	patchUpstream := &entity.Upstream{
+		BaseInfo: entity.BaseInfo{
+			ID: "u1",
+		},
+		UpstreamDef: entity.UpstreamDef{
+			Name: "upstream2",
+			Timeout: map[string]interface{}{
+				"connect": float64(20),
+				"send":    float64(20),
+				"read":    float64(20),
+			},
+			Checks: map[string]interface{}{
+				"active": map[string]interface{}{
+					"timeout":   float64(5),
+					"http_path": "/status",
+					"host":      "foo.com",
+					"healthy": map[string]interface{}{
+						"interval":  float64(2),
+						"successes": float64(1),
+					},
+					"unhealthy": map[string]interface{}{
+						"interval":      float64(1),
+						"http_failures": float64(2),
+					},
+					"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+				},
+				"passive": map[string]interface{}{
+					"healthy": map[string]interface{}{
+						"http_statuses": []interface{}{float64(200), float64(201)},
+						"successes":     float64(3),
+					},
+					"unhealthy": map[string]interface{}{
+						"http_statuses": []interface{}{float64(500)},
+						"http_failures": 3,
+						"tcp_failures":  3,
+					},
+				},
+			},
+			Key: "server_addr2",
+			Nodes: []interface{}{
+				map[string]interface{}{
+					"host":   "39.97.63.215",
+					"port":   float64(80),
+					"weight": float64(1),
+				},
+			},
+		},
+	}
+	patchUpstreamBytes, err := json.Marshal(patchUpstream)
 	assert.Nil(t, err)
 
+	tests := []struct {
+		caseDesc  string
+		getCalled bool
+		giveInput *PatchInput
+		giveErr   error
+		giveRet   interface{}
+		wantInput *entity.Upstream
+		wantErr   error
+		wantRet   interface{}
+	}{
+		{
+			caseDesc: "patch success",
+			giveRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream2",
+					Timeout: map[string]interface{}{
+						"connect": float64(20),
+						"send":    float64(20),
+						"read":    float64(20),
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  float64(2),
+								"successes": float64(1),
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      float64(1),
+								"http_failures": float64(2),
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr2",
+					Nodes: []interface{}{
+						map[string]interface{}{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			giveInput: &PatchInput{
+				ID:      "u1",
+				SubPath: "",
+				Body:    patchUpstreamBytes,
+			},
+			wantInput: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream2",
+					Timeout: map[string]interface{}{
+						"connect": float64(20),
+						"send":    float64(20),
+						"read":    float64(20),
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  float64(2),
+								"successes": float64(1),
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      float64(1),
+								"http_failures": float64(2),
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr2",
+					Nodes: []interface{}{
+						map[string]interface{}{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream2",
+					Timeout: map[string]interface{}{
+						"connect": float64(20),
+						"send":    float64(20),
+						"read":    float64(20),
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  float64(2),
+								"successes": float64(1),
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      float64(1),
+								"http_failures": float64(2),
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr2",
+					Nodes: []interface{}{
+						map[string]interface{}{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			getCalled: true,
+		},
+		{
+			caseDesc: "patch success by path",
+			giveInput: &PatchInput{
+				ID:      "u1",
+				SubPath: "/nodes",
+				Body:    []byte(`[{"host": "172.16.238.20","port": 1981,"weight": 1}]`),
+			},
+			giveRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": float64(20),
+						"send":    float64(20),
+						"read":    float64(20),
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr_patch",
+					Nodes: []interface{}{
+						map[string]interface{}{
+							"host":   "172.16.238.20",
+							"port":   float64(1981),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantInput: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": float64(15),
+						"send":    float64(15),
+						"read":    float64(15),
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  float64(2),
+								"successes": float64(1),
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      float64(1),
+								"http_failures": float64(2),
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []interface{}{
+						map[string]interface{}{
+							"host":   "172.16.238.20",
+							"port":   float64(1981),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": float64(20),
+						"send":    float64(20),
+						"read":    float64(20),
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr_patch",
+					Nodes: []interface{}{
+						map[string]interface{}{
+							"host":   "172.16.238.20",
+							"port":   float64(1981),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			getCalled: true,
+		},
+		{
+			caseDesc: "patch failed, path error",
+			giveInput: &PatchInput{
+				ID:      "u1",
+				SubPath: "error",
+				Body:    []byte("0"),
+			},
+			wantRet: handler.SpecCodeResponse(
+				errors.New("add operation does not apply: doc is missing path: \"error\": missing value")),
+			wantErr: errors.New("add operation does not apply: doc is missing path: \"error\": missing value"),
+		},
+	}
+	for _, tc := range tests {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			getCalled := false
+
+			upstreamStore := &store.MockInterface{}
+			upstreamStore.On("Get", mock.Anything, mock.Anything).Return(existUpstream, nil)
+			upstreamStore.On("Update", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				getCalled = true
+				input := args.Get(1).(*entity.Upstream)
+				createIfNotExist := args.Get(2).(bool)
+				assert.Equal(t, tc.wantInput, input)
+				assert.False(t, createIfNotExist)
+			}).Return(tc.giveRet, tc.giveErr)
+
+			h := Handler{upstreamStore: upstreamStore}
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ret, err := h.Patch(ctx)
+			assert.Equal(t, tc.getCalled, getCalled)
+			assert.Equal(t, tc.wantRet, ret)
+			if tc.wantErr != nil && err != nil {
+				assert.Error(t, tc.wantErr.(error), err.Error())
+			} else {
+				assert.Equal(t, tc.wantErr, err)
+			}

Review comment:
       @Jaycean OK, gotcha. Thanks for the explanations.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix-dashboard] codecov-io edited a comment on pull request #1452: feat(be): refactor upstream unit test

Posted by GitBox <gi...@apache.org>.
codecov-io edited a comment on pull request #1452:
URL: https://github.com/apache/apisix-dashboard/pull/1452#issuecomment-775714401


   # [Codecov](https://codecov.io/gh/apache/apisix-dashboard/pull/1452?src=pr&el=h1) Report
   > Merging [#1452](https://codecov.io/gh/apache/apisix-dashboard/pull/1452?src=pr&el=desc) (28ed890) into [master](https://codecov.io/gh/apache/apisix-dashboard/commit/8796fa1b00617f83b495798cc7b86418d1456533?el=desc) (8796fa1) will **increase** coverage by `1.10%`.
   > The diff coverage is `100.00%`.
   
   [![Impacted file tree graph](https://codecov.io/gh/apache/apisix-dashboard/pull/1452/graphs/tree.svg?width=650&height=150&src=pr&token=Q1HERXN96P)](https://codecov.io/gh/apache/apisix-dashboard/pull/1452?src=pr&el=tree)
   
   ```diff
   @@            Coverage Diff             @@
   ##           master    #1452      +/-   ##
   ==========================================
   + Coverage   67.61%   68.72%   +1.10%     
   ==========================================
     Files          48       48              
     Lines        3042     3038       -4     
   ==========================================
   + Hits         2057     2088      +31     
   + Misses        741      712      -29     
   + Partials      244      238       -6     
   ```
   
   
   | [Impacted Files](https://codecov.io/gh/apache/apisix-dashboard/pull/1452?src=pr&el=tree) | Coverage Δ | |
   |---|---|---|
   | [api/internal/handler/upstream/upstream.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1452/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2hhbmRsZXIvdXBzdHJlYW0vdXBzdHJlYW0uZ28=) | `88.78% <100.00%> (+5.73%)` | :arrow_up: |
   | [api/internal/core/store/selector.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1452/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2NvcmUvc3RvcmUvc2VsZWN0b3IuZ28=) | `75.92% <0.00%> (-11.12%)` | :arrow_down: |
   | [api/internal/log/log.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1452/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2xvZy9sb2cuZ28=) | `50.00% <0.00%> (-10.00%)` | :arrow_down: |
   | [api/internal/core/entity/query.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1452/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2NvcmUvZW50aXR5L3F1ZXJ5Lmdv) | `0.00% <0.00%> (-9.62%)` | :arrow_down: |
   | [api/internal/core/store/query.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1452/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2NvcmUvc3RvcmUvcXVlcnkuZ28=) | `88.09% <0.00%> (-9.53%)` | :arrow_down: |
   | [api/internal/core/storage/etcd.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1452/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2NvcmUvc3RvcmFnZS9ldGNkLmdv) | `50.00% <0.00%> (+6.36%)` | :arrow_up: |
   | [api/internal/handler/ssl/ssl.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1452/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2hhbmRsZXIvc3NsL3NzbC5nbw==) | `68.87% <0.00%> (+20.72%)` | :arrow_up: |
   
   ------
   
   [Continue to review full report at Codecov](https://codecov.io/gh/apache/apisix-dashboard/pull/1452?src=pr&el=continue).
   > **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
   > `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
   > Powered by [Codecov](https://codecov.io/gh/apache/apisix-dashboard/pull/1452?src=pr&el=footer). Last update [8796fa1...28ed890](https://codecov.io/gh/apache/apisix-dashboard/pull/1452?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).
   


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix-dashboard] Jaycean edited a comment on pull request #1452: feat(be): refactor upstream unit test

Posted by GitBox <gi...@apache.org>.
Jaycean edited a comment on pull request #1452:
URL: https://github.com/apache/apisix-dashboard/pull/1452#issuecomment-775716643


   ```
   @starsz starsz 3 hours ago Contributor
   Yes.The same question I had meet.
   ```
   It's strange why I can't reply directly
   Yes, in fact, I learned from your writing here


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix-dashboard] liuxiran commented on a change in pull request #1452: feat(be): refactor upstream unit test

Posted by GitBox <gi...@apache.org>.
liuxiran commented on a change in pull request #1452:
URL: https://github.com/apache/apisix-dashboard/pull/1452#discussion_r571816080



##########
File path: api/internal/handler/upstream/upstream_test.go
##########
@@ -19,285 +19,1719 @@ package upstream
 
 import (
 	"encoding/json"
-	"strings"
+	"errors"
+	"fmt"
+	"net/http"
 	"testing"
-	"time"
 
 	"github.com/shiningrush/droplet"
+	"github.com/shiningrush/droplet/data"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
 
-	"github.com/apisix/manager-api/internal/conf"
 	"github.com/apisix/manager-api/internal/core/entity"
-	"github.com/apisix/manager-api/internal/core/storage"
 	"github.com/apisix/manager-api/internal/core/store"
+	"github.com/apisix/manager-api/internal/handler"
+	"github.com/apisix/manager-api/internal/utils/consts"
 )
 
-var upstreamHandler *Handler
+func TestUpstream_Get(t *testing.T) {
+	tests := []struct {
+		caseDesc   string
+		giveInput  *GetInput
+		giveRet    *entity.Upstream
+		giveErr    error
+		wantErr    error
+		wantGetKey string
+		wantRet    interface{}
+	}{
+		{
+			caseDesc:   "upstream: get success",
+			giveInput:  &GetInput{ID: "u1"},
+			wantGetKey: "u1",
+			giveRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+		},
+		{
+			caseDesc:   "store get failed",
+			giveInput:  &GetInput{ID: "failed_key"},
+			wantGetKey: "failed_key",
+			giveErr:    fmt.Errorf("get failed"),
+			wantErr:    fmt.Errorf("get failed"),
+			wantRet: &data.SpecCodeResponse{
+				StatusCode: http.StatusInternalServerError,
+			},
+		},
+	}
 
-func TestUpstream(t *testing.T) {
-	// init
-	err := storage.InitETCDClient(conf.ETCDConfig)
-	assert.Nil(t, err)
-	err = store.InitStores()
-	assert.Nil(t, err)
+	for _, tc := range tests {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			getCalled := true
+			upstreamStore := &store.MockInterface{}
+			upstreamStore.On("Get", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				getCalled = true
+				assert.Equal(t, tc.wantGetKey, args.Get(0))
+			}).Return(tc.giveRet, tc.giveErr)
+
+			h := Handler{upstreamStore: upstreamStore}
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ret, err := h.Get(ctx)
+			assert.True(t, getCalled)
+			assert.Equal(t, tc.wantRet, ret)
+			assert.Equal(t, tc.wantErr, err)
+		})
+	}
+}
+
+func TestUpstreams_List(t *testing.T) {
+	mockData := []*entity.Upstream{

Review comment:
       I found that `LIst` support name as filter[1], it would be better add  a case to test it ^_^
   
   reference:
   [1]: https://github.com/apache/apisix-dashboard/blob/a87028a4098afd2fb09cf1724028cff4da568b42/api/internal/handler/upstream/upstream.go#L91




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix-dashboard] liuxiran commented on a change in pull request #1452: feat(be): refactor upstream unit test

Posted by GitBox <gi...@apache.org>.
liuxiran commented on a change in pull request #1452:
URL: https://github.com/apache/apisix-dashboard/pull/1452#discussion_r571816080



##########
File path: api/internal/handler/upstream/upstream_test.go
##########
@@ -19,285 +19,1719 @@ package upstream
 
 import (
 	"encoding/json"
-	"strings"
+	"errors"
+	"fmt"
+	"net/http"
 	"testing"
-	"time"
 
 	"github.com/shiningrush/droplet"
+	"github.com/shiningrush/droplet/data"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
 
-	"github.com/apisix/manager-api/internal/conf"
 	"github.com/apisix/manager-api/internal/core/entity"
-	"github.com/apisix/manager-api/internal/core/storage"
 	"github.com/apisix/manager-api/internal/core/store"
+	"github.com/apisix/manager-api/internal/handler"
+	"github.com/apisix/manager-api/internal/utils/consts"
 )
 
-var upstreamHandler *Handler
+func TestUpstream_Get(t *testing.T) {
+	tests := []struct {
+		caseDesc   string
+		giveInput  *GetInput
+		giveRet    *entity.Upstream
+		giveErr    error
+		wantErr    error
+		wantGetKey string
+		wantRet    interface{}
+	}{
+		{
+			caseDesc:   "upstream: get success",
+			giveInput:  &GetInput{ID: "u1"},
+			wantGetKey: "u1",
+			giveRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+		},
+		{
+			caseDesc:   "store get failed",
+			giveInput:  &GetInput{ID: "failed_key"},
+			wantGetKey: "failed_key",
+			giveErr:    fmt.Errorf("get failed"),
+			wantErr:    fmt.Errorf("get failed"),
+			wantRet: &data.SpecCodeResponse{
+				StatusCode: http.StatusInternalServerError,
+			},
+		},
+	}
 
-func TestUpstream(t *testing.T) {
-	// init
-	err := storage.InitETCDClient(conf.ETCDConfig)
-	assert.Nil(t, err)
-	err = store.InitStores()
-	assert.Nil(t, err)
+	for _, tc := range tests {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			getCalled := true
+			upstreamStore := &store.MockInterface{}
+			upstreamStore.On("Get", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				getCalled = true
+				assert.Equal(t, tc.wantGetKey, args.Get(0))
+			}).Return(tc.giveRet, tc.giveErr)
+
+			h := Handler{upstreamStore: upstreamStore}
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ret, err := h.Get(ctx)
+			assert.True(t, getCalled)
+			assert.Equal(t, tc.wantRet, ret)
+			assert.Equal(t, tc.wantErr, err)
+		})
+	}
+}
+
+func TestUpstreams_List(t *testing.T) {
+	mockData := []*entity.Upstream{

Review comment:
       I found that `List` support name as filter[1], it would be better add  a case to test it ^_^
   
   reference:
   [1]: https://github.com/apache/apisix-dashboard/blob/a87028a4098afd2fb09cf1724028cff4da568b42/api/internal/handler/upstream/upstream.go#L91




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix-dashboard] codecov-io edited a comment on pull request #1452: feat(be): refactor upstream unit test

Posted by GitBox <gi...@apache.org>.
codecov-io edited a comment on pull request #1452:
URL: https://github.com/apache/apisix-dashboard/pull/1452#issuecomment-775714401


   # [Codecov](https://codecov.io/gh/apache/apisix-dashboard/pull/1452?src=pr&el=h1) Report
   > Merging [#1452](https://codecov.io/gh/apache/apisix-dashboard/pull/1452?src=pr&el=desc) (28ed890) into [master](https://codecov.io/gh/apache/apisix-dashboard/commit/8796fa1b00617f83b495798cc7b86418d1456533?el=desc) (8796fa1) will **decrease** coverage by `7.54%`.
   > The diff coverage is `100.00%`.
   
   [![Impacted file tree graph](https://codecov.io/gh/apache/apisix-dashboard/pull/1452/graphs/tree.svg?width=650&height=150&src=pr&token=Q1HERXN96P)](https://codecov.io/gh/apache/apisix-dashboard/pull/1452?src=pr&el=tree)
   
   ```diff
   @@            Coverage Diff             @@
   ##           master    #1452      +/-   ##
   ==========================================
   - Coverage   67.61%   60.07%   -7.55%     
   ==========================================
     Files          48       48              
     Lines        3042     3038       -4     
   ==========================================
   - Hits         2057     1825     -232     
   - Misses        741      973     +232     
   + Partials      244      240       -4     
   ```
   
   
   | [Impacted Files](https://codecov.io/gh/apache/apisix-dashboard/pull/1452?src=pr&el=tree) | Coverage Δ | |
   |---|---|---|
   | [api/internal/handler/upstream/upstream.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1452/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2hhbmRsZXIvdXBzdHJlYW0vdXBzdHJlYW0uZ28=) | `86.91% <100.00%> (+3.86%)` | :arrow_up: |
   | [api/internal/core/entity/entity.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1452/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2NvcmUvZW50aXR5L2VudGl0eS5nbw==) | `18.75% <0.00%> (-81.25%)` | :arrow_down: |
   | [...l/handler/route\_online\_debug/route\_online\_debug.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1452/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2hhbmRsZXIvcm91dGVfb25saW5lX2RlYnVnL3JvdXRlX29ubGluZV9kZWJ1Zy5nbw==) | `7.14% <0.00%> (-66.67%)` | :arrow_down: |
   | [api/internal/handler/data\_loader/route\_import.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1452/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2hhbmRsZXIvZGF0YV9sb2FkZXIvcm91dGVfaW1wb3J0Lmdv) | `30.24% <0.00%> (-34.68%)` | :arrow_down: |
   | [api/internal/log/log.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1452/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2xvZy9sb2cuZ28=) | `30.00% <0.00%> (-30.00%)` | :arrow_down: |
   | [api/internal/utils/consts/api\_error.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1452/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL3V0aWxzL2NvbnN0cy9hcGlfZXJyb3IuZ28=) | `25.00% <0.00%> (-25.00%)` | :arrow_down: |
   | [api/internal/core/store/storehub.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1452/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2NvcmUvc3RvcmUvc3RvcmVodWIuZ28=) | `45.91% <0.00%> (-24.49%)` | :arrow_down: |
   | [api/internal/utils/json\_patch.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1452/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL3V0aWxzL2pzb25fcGF0Y2guZ28=) | `34.48% <0.00%> (-24.14%)` | :arrow_down: |
   | [api/internal/filter/schema.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1452/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2ZpbHRlci9zY2hlbWEuZ28=) | `31.93% <0.00%> (-23.53%)` | :arrow_down: |
   | [api/internal/handler/global\_rule/global\_rule.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1452/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2hhbmRsZXIvZ2xvYmFsX3J1bGUvZ2xvYmFsX3J1bGUuZ28=) | `66.12% <0.00%> (-17.75%)` | :arrow_down: |
   | ... and [13 more](https://codecov.io/gh/apache/apisix-dashboard/pull/1452/diff?src=pr&el=tree-more) | |
   
   ------
   
   [Continue to review full report at Codecov](https://codecov.io/gh/apache/apisix-dashboard/pull/1452?src=pr&el=continue).
   > **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
   > `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
   > Powered by [Codecov](https://codecov.io/gh/apache/apisix-dashboard/pull/1452?src=pr&el=footer). Last update [8796fa1...28ed890](https://codecov.io/gh/apache/apisix-dashboard/pull/1452?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).
   


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix-dashboard] codecov-io commented on pull request #1452: feat(be): refactor upstream unit test

Posted by GitBox <gi...@apache.org>.
codecov-io commented on pull request #1452:
URL: https://github.com/apache/apisix-dashboard/pull/1452#issuecomment-775714401


   # [Codecov](https://codecov.io/gh/apache/apisix-dashboard/pull/1452?src=pr&el=h1) Report
   > Merging [#1452](https://codecov.io/gh/apache/apisix-dashboard/pull/1452?src=pr&el=desc) (28ed890) into [master](https://codecov.io/gh/apache/apisix-dashboard/commit/8796fa1b00617f83b495798cc7b86418d1456533?el=desc) (8796fa1) will **decrease** coverage by `15.98%`.
   > The diff coverage is `100.00%`.
   
   [![Impacted file tree graph](https://codecov.io/gh/apache/apisix-dashboard/pull/1452/graphs/tree.svg?width=650&height=150&src=pr&token=Q1HERXN96P)](https://codecov.io/gh/apache/apisix-dashboard/pull/1452?src=pr&el=tree)
   
   ```diff
   @@             Coverage Diff             @@
   ##           master    #1452       +/-   ##
   ===========================================
   - Coverage   67.61%   51.63%   -15.99%     
   ===========================================
     Files          48       39        -9     
     Lines        3042     2597      -445     
   ===========================================
   - Hits         2057     1341      -716     
   - Misses        741     1082      +341     
   + Partials      244      174       -70     
   ```
   
   
   | [Impacted Files](https://codecov.io/gh/apache/apisix-dashboard/pull/1452?src=pr&el=tree) | Coverage Δ | |
   |---|---|---|
   | [api/internal/handler/upstream/upstream.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1452/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2hhbmRsZXIvdXBzdHJlYW0vdXBzdHJlYW0uZ28=) | `66.35% <100.00%> (-16.70%)` | :arrow_down: |
   | [api/internal/utils/version.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1452/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL3V0aWxzL3ZlcnNpb24uZ28=) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
   | [api/internal/filter/request\_id.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1452/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2ZpbHRlci9yZXF1ZXN0X2lkLmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
   | [api/internal/core/entity/entity.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1452/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2NvcmUvZW50aXR5L2VudGl0eS5nbw==) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
   | [api/internal/core/store/storehub.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1452/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2NvcmUvc3RvcmUvc3RvcmVodWIuZ28=) | `0.00% <0.00%> (-70.41%)` | :arrow_down: |
   | [api/internal/filter/cors.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1452/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2ZpbHRlci9jb3JzLmdv) | `0.00% <0.00%> (-66.67%)` | :arrow_down: |
   | [api/internal/filter/schema.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1452/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2ZpbHRlci9zY2hlbWEuZ28=) | `0.00% <0.00%> (-55.47%)` | :arrow_down: |
   | [api/internal/utils/consts/api\_error.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1452/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL3V0aWxzL2NvbnN0cy9hcGlfZXJyb3IuZ28=) | `0.00% <0.00%> (-50.00%)` | :arrow_down: |
   | [api/internal/handler/data\_loader/route\_import.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1452/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2hhbmRsZXIvZGF0YV9sb2FkZXIvcm91dGVfaW1wb3J0Lmdv) | `27.41% <0.00%> (-37.50%)` | :arrow_down: |
   | [api/internal/handler/server\_info/server\_info.go](https://codecov.io/gh/apache/apisix-dashboard/pull/1452/diff?src=pr&el=tree#diff-YXBpL2ludGVybmFsL2hhbmRsZXIvc2VydmVyX2luZm8vc2VydmVyX2luZm8uZ28=) | `57.14% <0.00%> (-33.34%)` | :arrow_down: |
   | ... and [33 more](https://codecov.io/gh/apache/apisix-dashboard/pull/1452/diff?src=pr&el=tree-more) | |
   
   ------
   
   [Continue to review full report at Codecov](https://codecov.io/gh/apache/apisix-dashboard/pull/1452?src=pr&el=continue).
   > **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
   > `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
   > Powered by [Codecov](https://codecov.io/gh/apache/apisix-dashboard/pull/1452?src=pr&el=footer). Last update [8796fa1...28ed890](https://codecov.io/gh/apache/apisix-dashboard/pull/1452?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).
   


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix-dashboard] Jaycean commented on a change in pull request #1452: feat(be): refactor upstream unit test

Posted by GitBox <gi...@apache.org>.
Jaycean commented on a change in pull request #1452:
URL: https://github.com/apache/apisix-dashboard/pull/1452#discussion_r571888603



##########
File path: api/internal/handler/upstream/upstream.go
##########
@@ -270,27 +267,15 @@ func (h *Handler) Exist(c droplet.Context) (interface{}, error) {
 		return nil, err
 	}
 
-	sort := store.NewSort(nil)
-	filter := store.NewFilter([]string{"name", name})
-	pagination := store.NewPagination(0, 0)
-	query := store.NewQuery(sort, filter, pagination)
-	rows := store.NewFilterSelector(toRows(ret), query)

Review comment:
       I suggest using a new issue to record this problem. Later, I will check whether these methods are used in other places, and submit a new PR to delete them. If my idea is wrong, please correct me.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix-dashboard] imjoey commented on a change in pull request #1452: feat(be): refactor upstream unit test

Posted by GitBox <gi...@apache.org>.
imjoey commented on a change in pull request #1452:
URL: https://github.com/apache/apisix-dashboard/pull/1452#discussion_r571786346



##########
File path: api/internal/handler/upstream/upstream_test.go
##########
@@ -19,285 +19,1719 @@ package upstream
 
 import (
 	"encoding/json"
-	"strings"
+	"errors"
+	"fmt"
+	"net/http"
 	"testing"
-	"time"
 
 	"github.com/shiningrush/droplet"
+	"github.com/shiningrush/droplet/data"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
 
-	"github.com/apisix/manager-api/internal/conf"
 	"github.com/apisix/manager-api/internal/core/entity"
-	"github.com/apisix/manager-api/internal/core/storage"
 	"github.com/apisix/manager-api/internal/core/store"
+	"github.com/apisix/manager-api/internal/handler"
+	"github.com/apisix/manager-api/internal/utils/consts"
 )
 
-var upstreamHandler *Handler
+func TestUpstream_Get(t *testing.T) {
+	tests := []struct {
+		caseDesc   string
+		giveInput  *GetInput
+		giveRet    *entity.Upstream
+		giveErr    error
+		wantErr    error
+		wantGetKey string
+		wantRet    interface{}
+	}{
+		{
+			caseDesc:   "upstream: get success",
+			giveInput:  &GetInput{ID: "u1"},
+			wantGetKey: "u1",
+			giveRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+		},
+		{
+			caseDesc:   "store get failed",
+			giveInput:  &GetInput{ID: "failed_key"},
+			wantGetKey: "failed_key",
+			giveErr:    fmt.Errorf("get failed"),
+			wantErr:    fmt.Errorf("get failed"),
+			wantRet: &data.SpecCodeResponse{
+				StatusCode: http.StatusInternalServerError,
+			},
+		},
+	}
 
-func TestUpstream(t *testing.T) {
-	// init
-	err := storage.InitETCDClient(conf.ETCDConfig)
-	assert.Nil(t, err)
-	err = store.InitStores()
-	assert.Nil(t, err)
+	for _, tc := range tests {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			getCalled := true
+			upstreamStore := &store.MockInterface{}
+			upstreamStore.On("Get", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				getCalled = true
+				assert.Equal(t, tc.wantGetKey, args.Get(0))
+			}).Return(tc.giveRet, tc.giveErr)
+
+			h := Handler{upstreamStore: upstreamStore}
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ret, err := h.Get(ctx)
+			assert.True(t, getCalled)
+			assert.Equal(t, tc.wantRet, ret)
+			assert.Equal(t, tc.wantErr, err)
+		})
+	}
+}
+
+func TestUpstreams_List(t *testing.T) {
+	mockData := []*entity.Upstream{
+		{
+			BaseInfo: entity.BaseInfo{
+				ID:         "u1",
+				CreateTime: 1609340491,
+				UpdateTime: 1609340491,
+			},
+			UpstreamDef: entity.UpstreamDef{
+				Name: "upstream1",
+				Key:  "server_addr",
+				Nodes: []map[string]interface{}{
+					{
+						"host":   "39.97.63.215",
+						"port":   float64(80),
+						"weight": float64(1),
+					},
+				},
+			},
+		},
+		{
+			BaseInfo: entity.BaseInfo{
+				ID:         "u2",
+				CreateTime: 1609340491,
+				UpdateTime: 1609340491,
+			},
+			UpstreamDef: entity.UpstreamDef{
+				Name: "upstream2",
+				Key:  "server_addr2",
+				Nodes: []map[string]interface{}{
+					{
+						"host":   "39.97.63.215",
+						"port":   float64(80),
+						"weight": float64(1),
+					},
+				},
+			},
+		},
+		{
+			BaseInfo: entity.BaseInfo{
+				ID:         "u3",
+				CreateTime: 1609340491,
+				UpdateTime: 1609340491,
+			},
+			UpstreamDef: entity.UpstreamDef{
+				Name: "upstream3",
+				Key:  "server_addr3",
+				Nodes: []map[string]interface{}{
+					{
+						"host":   "39.97.63.215",
+						"port":   float64(80),
+						"weight": float64(1),
+					},
+				},
+			},
+		},
+	}
 
-	upstreamHandler = &Handler{
-		upstreamStore: store.GetStore(store.HubKeyUpstream),
+	tests := []struct {
+		caseDesc  string
+		giveInput *ListInput
+		giveData  []*entity.Upstream
+		giveErr   error
+		wantErr   error
+		wantInput store.ListInput
+		wantRet   interface{}
+	}{
+		{
+			caseDesc: "list all upstream",
+			giveInput: &ListInput{
+				Pagination: store.Pagination{
+					PageSize:   10,
+					PageNumber: 10,
+				},
+			},
+			wantInput: store.ListInput{
+				PageSize:   10,
+				PageNumber: 10,
+			},
+			wantRet: &store.ListOutput{
+				Rows: []interface{}{
+					mockData[0],
+					mockData[1],
+					mockData[2],
+				},
+				TotalSize: 3,
+			},
+		},
+		{
+			caseDesc: "list upstream with 'upstream1'",
+			giveInput: &ListInput{
+				Name: "upstream1",
+				Pagination: store.Pagination{
+					PageSize:   10,
+					PageNumber: 10,
+				},
+			},
+			wantInput: store.ListInput{
+				PageSize:   10,
+				PageNumber: 10,
+			},
+			wantRet: &store.ListOutput{
+				Rows: []interface{}{
+					mockData[0],
+				},
+				TotalSize: 1,
+			},
+		},
 	}
-	assert.NotNil(t, upstreamHandler)
-
-	//create
-	ctx := droplet.NewContext()
-	upstream := &entity.Upstream{}
-	reqBody := `{
-		"id": "1",
-		"name": "upstream3",
-		"description": "upstream upstream",
-		"type": "roundrobin",
-		"nodes": [{
-			"host": "a.a.com",
-			"port": 80,
-			"weight": 1
-		}],
-		"timeout":{
-			"connect":15,
-			"send":15,
-			"read":15
-		},
-		"hash_on": "header",
-		"key": "server_addr",
-		"checks": {
-			"active": {
-				"timeout": 5,
-				"http_path": "/status",
-				"host": "foo.com",
-				"healthy": {
-					"interval": 2,
-					"successes": 1
-				},
-				"unhealthy": {
-					"interval": 1,
-					"http_failures": 2
-				},
-				"req_headers": ["User-Agent: curl/7.29.0"]
-			},
-			"passive": {
-				"healthy": {
-					"http_statuses": [200, 201],
-					"successes": 3
-				},
-				"unhealthy": {
-					"http_statuses": [500],
-					"http_failures": 3,
-					"tcp_failures": 3
+
+	for _, tc := range tests {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			getCalled := true
+			upstreamStore := &store.MockInterface{}
+			upstreamStore.On("List", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				getCalled = true
+				input := args.Get(0).(store.ListInput)
+				assert.Equal(t, tc.wantInput.PageSize, input.PageSize)
+				assert.Equal(t, tc.wantInput.PageNumber, input.PageNumber)
+			}).Return(func(input store.ListInput) *store.ListOutput {
+				var returnData []interface{}
+				for _, c := range mockData {
+					if input.Predicate(c) {
+						if input.Format == nil {
+							returnData = append(returnData, c)
+							continue
+						}
+						returnData = append(returnData, input.Format(c))
+					}
 				}
-			}
-		}
-	}`
-	err = json.Unmarshal([]byte(reqBody), upstream)
-	assert.Nil(t, err)
-	ctx.SetInput(upstream)
-	ret, err := upstreamHandler.Create(ctx)
-	assert.Nil(t, err)
-	objRet, ok := ret.(*entity.Upstream)
-	assert.True(t, ok)
-	assert.Equal(t, "1", objRet.ID)
-
-	//sleep
-	time.Sleep(time.Duration(100) * time.Millisecond)
-
-	//get
-	input := &GetInput{}
-	input.ID = "1"
-	ctx.SetInput(input)
-	ret, err = upstreamHandler.Get(ctx)
-	stored := ret.(*entity.Upstream)
-	assert.Nil(t, err)
-	assert.Equal(t, stored.ID, upstream.ID)
-
-	//update
-	upstream2 := &UpdateInput{}
-	upstream2.ID = "1"
-	reqBody = `{
-		"id": "1",
-		"name": "upstream3",
-		"description": "upstream upstream",
-		"type": "roundrobin",
-		"nodes": [{
-			"host": "a.a.com",
-			"port": 80,
-			"weight": 1
-		}],
-		"timeout":{
-			"connect":15,
-			"send":15,
-			"read":15
-		},
-		"enable_websocket": true,
-		"hash_on": "header",
-		"key": "server_addr",
-		"checks": {
-			"active": {
-				"timeout": 5,
-				"http_path": "/status",
-				"host": "foo.com",
-				"healthy": {
-					"interval": 2,
-					"successes": 1
-				},
-				"unhealthy": {
-					"interval": 1,
-					"http_failures": 2
-				},
-				"req_headers": ["User-Agent: curl/7.29.0"]
-			},
-			"passive": {
-				"healthy": {
-					"http_statuses": [200, 201],
-					"successes": 3
-				},
-				"unhealthy": {
-					"http_statuses": [500],
-					"http_failures": 3,
-					"tcp_failures": 3
+				return &store.ListOutput{
+					Rows:      returnData,
+					TotalSize: len(returnData),
 				}
-			}
-		}
-	}`
-	err = json.Unmarshal([]byte(reqBody), upstream2)
-	assert.Nil(t, err)
-	ctx.SetInput(upstream2)
-	ret, err = upstreamHandler.Update(ctx)
-	assert.Nil(t, err)
-	// check the returned value
-	objRet, ok = ret.(*entity.Upstream)
-	assert.True(t, ok)
-	assert.Equal(t, upstream2.ID, objRet.ID)
-
-	//list
-	listInput := &ListInput{}
-	reqBody = `{"page_size": 1, "page": 1}`
-	err = json.Unmarshal([]byte(reqBody), listInput)
-	assert.Nil(t, err)
-	ctx.SetInput(listInput)
-	retPage, err := upstreamHandler.List(ctx)
-	assert.Nil(t, err)
-	dataPage := retPage.(*store.ListOutput)
-	assert.Equal(t, len(dataPage.Rows), 1)
+			}, tc.giveErr)
 
-	//delete test data
-	inputDel := &BatchDelete{}
-	reqBody = `{"ids": "1"}`
-	err = json.Unmarshal([]byte(reqBody), inputDel)
-	assert.Nil(t, err)
-	ctx.SetInput(inputDel)
-	_, err = upstreamHandler.BatchDelete(ctx)
-	assert.Nil(t, err)
+			h := Handler{upstreamStore: upstreamStore}
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ret, err := h.List(ctx)
+			assert.True(t, getCalled)
+			assert.Equal(t, tc.wantRet, ret)
+			assert.Equal(t, tc.wantErr, err)
+		})
+	}
+}
+
+func TestUpstream_Create(t *testing.T) {
+	tests := []struct {
+		caseDesc  string
+		getCalled bool
+		giveInput *entity.Upstream
+		giveRet   interface{}
+		giveErr   error
+		wantInput *entity.Upstream
+		wantErr   error
+		wantRet   interface{}
+	}{
+		{
+			caseDesc:  "create success",
+			getCalled: true,
+			giveInput: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			giveRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantInput: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantErr: nil,
+		},
+		{
+			caseDesc:  "create failed, create return error",
+			getCalled: true,
+			giveInput: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			giveErr: fmt.Errorf("create failed"),
+			wantInput: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantErr: fmt.Errorf("create failed"),
+			wantRet: handler.SpecCodeResponse(fmt.Errorf("create failed")),
+		},
+	}
 
+	for _, tc := range tests {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			getCalled := false
+
+			upstreamStore := &store.MockInterface{}
+			upstreamStore.On("Create", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				getCalled = true
+				input := args.Get(1).(*entity.Upstream)
+				assert.Equal(t, tc.wantInput, input)
+			}).Return(tc.giveRet, tc.giveErr)
+
+			h := Handler{upstreamStore: upstreamStore}
+
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ret, err := h.Create(ctx)
+			assert.True(t, getCalled)
+			assert.Equal(t, tc.wantRet, ret)
+			assert.Equal(t, tc.wantErr, err)
+		})
+	}
 }
 
-func TestUpstream_Pass_Host(t *testing.T) {
-	//create
-	ctx := droplet.NewContext()
-	upstream := &entity.Upstream{}
-	reqBody := `{
-		"id": "2",
-		"nodes": [{
-			"host": "httpbin.org",
-			"port": 80,
-			"weight": 1
-		}],
-		"type": "roundrobin",
-		"pass_host": "node"
-	}`
-	err := json.Unmarshal([]byte(reqBody), upstream)
-	assert.Nil(t, err)
-	ctx.SetInput(upstream)
-	ret, err := upstreamHandler.Create(ctx)
-	assert.Nil(t, err)
-	objRet, ok := ret.(*entity.Upstream)
-	assert.True(t, ok)
-	assert.Equal(t, "2", objRet.ID)
-
-	//sleep
-	time.Sleep(time.Duration(20) * time.Millisecond)
-
-	//get
-	input := &GetInput{}
-	input.ID = "2"
-	ctx.SetInput(input)
-	ret, err = upstreamHandler.Get(ctx)
-	stored := ret.(*entity.Upstream)
-	assert.Nil(t, err)
-	assert.Equal(t, stored.ID, upstream.ID)
+func TestUpstream_Update(t *testing.T) {
+	tests := []struct {
+		caseDesc  string
+		getCalled bool
+		giveInput *UpdateInput
+		giveErr   error
+		giveRet   interface{}
+		wantInput *entity.Upstream
+		wantErr   error
+		wantRet   interface{}
+	}{
+		{
+			caseDesc:  "update success",
+			getCalled: true,
+			giveInput: &UpdateInput{
+				ID: "u1",
+				Upstream: entity.Upstream{
+					UpstreamDef: entity.UpstreamDef{
+						Name: "upstream1",
+						Timeout: map[string]interface{}{
+							"connect": 15,
+							"send":    15,
+							"read":    15,
+						},
+						Checks: map[string]interface{}{
+							"active": map[string]interface{}{
+								"timeout":   float64(5),
+								"http_path": "/status",
+								"host":      "foo.com",
+								"healthy": map[string]interface{}{
+									"interval":  2,
+									"successes": 1,
+								},
+								"unhealthy": map[string]interface{}{
+									"interval":      1,
+									"http_failures": 2,
+								},
+								"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+							},
+							"passive": map[string]interface{}{
+								"healthy": map[string]interface{}{
+									"http_statuses": []interface{}{float64(200), float64(201)},
+									"successes":     float64(3),
+								},
+								"unhealthy": map[string]interface{}{
+									"http_statuses": []interface{}{float64(500)},
+									"http_failures": 3,
+									"tcp_failures":  3,
+								},
+							},
+						},
+						Key: "server_addr",
+						Nodes: []map[string]interface{}{
+							{
+								"host":   "39.97.63.215",
+								"port":   float64(80),
+								"weight": float64(1),
+							},
+						},
+					},
+				},
+			},
+			giveRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": 3,
+								"tcp_failures":  3,
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantInput: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": 3,
+								"tcp_failures":  3,
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": 3,
+								"tcp_failures":  3,
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+		},
+		{
+			caseDesc: "create failed, different id",
+			giveInput: &UpdateInput{
+				ID: "u1",
+				Upstream: entity.Upstream{
+					BaseInfo: entity.BaseInfo{
+						ID: "u2",
+					},
+					UpstreamDef: entity.UpstreamDef{
+						Name: "upstream1",
+						Timeout: map[string]interface{}{
+							"connect": 15,
+							"send":    15,
+							"read":    15,
+						},
+						Checks: map[string]interface{}{
+							"active": map[string]interface{}{
+								"timeout":   float64(5),
+								"http_path": "/status",
+								"host":      "foo.com",
+								"healthy": map[string]interface{}{
+									"interval":  2,
+									"successes": 1,
+								},
+								"unhealthy": map[string]interface{}{
+									"interval":      1,
+									"http_failures": 2,
+								},
+								"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+							},
+							"passive": map[string]interface{}{
+								"healthy": map[string]interface{}{
+									"http_statuses": []interface{}{float64(200), float64(201)},
+									"successes":     float64(3),
+								},
+								"unhealthy": map[string]interface{}{
+									"http_statuses": []interface{}{float64(500)},
+									"http_failures": 3,
+									"tcp_failures":  3,
+								},
+							},
+						},
+						Key: "server_addr",
+						Nodes: []map[string]interface{}{
+							{
+								"host":   "39.97.63.215",
+								"port":   float64(80),
+								"weight": float64(1),
+							},
+						},
+					},
+				},
+			},
+			wantRet: &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
+			wantErr: fmt.Errorf("ID on path (u1) doesn't match ID on body (u2)"),
+		},
+	}
 
-	//delete test data
-	inputDel := &BatchDelete{}
-	reqBody = `{"ids": "2"}`
-	err = json.Unmarshal([]byte(reqBody), inputDel)
-	assert.Nil(t, err)
-	ctx.SetInput(inputDel)
-	_, err = upstreamHandler.BatchDelete(ctx)
-	assert.Nil(t, err)
+	for _, tc := range tests {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			getCalled := false
+			upstreamStore := &store.MockInterface{}
+			upstreamStore.On("Update", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				getCalled = true
+				input := args.Get(1).(*entity.Upstream)
+				createIfNotExist := args.Get(2).(bool)
+				assert.Equal(t, tc.wantInput, input)
+				assert.True(t, createIfNotExist)
+			}).Return(tc.giveRet, tc.giveErr)
 
+			h := Handler{upstreamStore: upstreamStore}
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ret, err := h.Update(ctx)
+			assert.Equal(t, tc.getCalled, getCalled)
+			assert.Equal(t, tc.wantRet, ret)
+			assert.Equal(t, tc.wantErr, err)
+		})
+	}
 }
 
-func TestUpstream_Patch_Update(t *testing.T) {
-	//create
-	ctx := droplet.NewContext()
-	upstream := &entity.Upstream{}
-	reqBody := `{
-			"id": "3",
-			"nodes": [{
-				"host": "172.16.238.20",
-				"port": 1980,
-				"weight": 1
-			}],
-			"type": "roundrobin"
-		}`
-	err := json.Unmarshal([]byte(reqBody), upstream)
-	assert.Nil(t, err)
-	ctx.SetInput(upstream)
-	ret, err := upstreamHandler.Create(ctx)
-	assert.Nil(t, err)
-	objRet, ok := ret.(*entity.Upstream)
-	assert.True(t, ok)
-	assert.Equal(t, "3", objRet.ID)
-
-	//sleep
-	time.Sleep(time.Duration(20) * time.Millisecond)
-
-	reqBody1 := `{
-		"nodes": [{
-			"host": "172.16.238.20",
-			"port": 1981,
-			"weight": 1
-		}],
-		"type": "roundrobin"
-	}`
-	responesBody := `"nodes":[{"host":"172.16.238.20","port":1981,"weight":1}],"type":"roundrobin"}`
-
-	input2 := &PatchInput{}
-	input2.ID = "3"
-	input2.SubPath = ""
-	input2.Body = []byte(reqBody1)
-	ctx.SetInput(input2)
-
-	ret2, err := upstreamHandler.Patch(ctx)
-	assert.Nil(t, err)
-	_ret2, err := json.Marshal(ret2)
-	assert.Nil(t, err)
-	isContains := strings.Contains(string(_ret2), responesBody)
-	assert.True(t, isContains)
+func TestUpstream_Patch(t *testing.T) {
+	existUpstream := &entity.Upstream{
+		BaseInfo: entity.BaseInfo{
+			ID: "u1",
+		},
+		UpstreamDef: entity.UpstreamDef{
+			Name: "upstream1",
+			Timeout: map[string]interface{}{
+				"connect": 15,
+				"send":    15,
+				"read":    15,
+			},
+			Checks: map[string]interface{}{
+				"active": map[string]interface{}{
+					"timeout":   float64(5),
+					"http_path": "/status",
+					"host":      "foo.com",
+					"healthy": map[string]interface{}{
+						"interval":  float64(2),
+						"successes": float64(1),
+					},
+					"unhealthy": map[string]interface{}{
+						"interval":      float64(1),
+						"http_failures": float64(2),
+					},
+					"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+				},
+				"passive": map[string]interface{}{
+					"healthy": map[string]interface{}{
+						"http_statuses": []interface{}{float64(200), float64(201)},
+						"successes":     float64(3),
+					},
+					"unhealthy": map[string]interface{}{
+						"http_statuses": []interface{}{float64(500)},
+						"http_failures": 3,
+						"tcp_failures":  3,
+					},
+				},
+			},
+			Key: "server_addr",
+			Nodes: []interface{}{
+				map[string]interface{}{
+					"host":   "39.97.63.215",
+					"port":   float64(80),
+					"weight": float64(1),
+				},
+			},
+		},
+	}
 
-	//delete test data
-	inputDel2 := &BatchDelete{}
-	reqBody = `{"ids": "3"}`
-	err = json.Unmarshal([]byte(reqBody), inputDel2)
-	assert.Nil(t, err)
-	ctx.SetInput(inputDel2)
-	_, err = upstreamHandler.BatchDelete(ctx)
+	patchUpstream := &entity.Upstream{
+		BaseInfo: entity.BaseInfo{
+			ID: "u1",
+		},
+		UpstreamDef: entity.UpstreamDef{
+			Name: "upstream2",
+			Timeout: map[string]interface{}{
+				"connect": float64(20),
+				"send":    float64(20),
+				"read":    float64(20),
+			},
+			Checks: map[string]interface{}{
+				"active": map[string]interface{}{
+					"timeout":   float64(5),
+					"http_path": "/status",
+					"host":      "foo.com",
+					"healthy": map[string]interface{}{
+						"interval":  float64(2),
+						"successes": float64(1),
+					},
+					"unhealthy": map[string]interface{}{
+						"interval":      float64(1),
+						"http_failures": float64(2),
+					},
+					"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+				},
+				"passive": map[string]interface{}{
+					"healthy": map[string]interface{}{
+						"http_statuses": []interface{}{float64(200), float64(201)},
+						"successes":     float64(3),
+					},
+					"unhealthy": map[string]interface{}{
+						"http_statuses": []interface{}{float64(500)},
+						"http_failures": 3,
+						"tcp_failures":  3,
+					},
+				},
+			},
+			Key: "server_addr2",
+			Nodes: []interface{}{
+				map[string]interface{}{
+					"host":   "39.97.63.215",
+					"port":   float64(80),
+					"weight": float64(1),
+				},
+			},
+		},
+	}
+	patchUpstreamBytes, err := json.Marshal(patchUpstream)
 	assert.Nil(t, err)
 
+	tests := []struct {
+		caseDesc  string
+		getCalled bool
+		giveInput *PatchInput
+		giveErr   error
+		giveRet   interface{}
+		wantInput *entity.Upstream
+		wantErr   error
+		wantRet   interface{}
+	}{
+		{
+			caseDesc: "patch success",
+			giveRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream2",
+					Timeout: map[string]interface{}{
+						"connect": float64(20),
+						"send":    float64(20),
+						"read":    float64(20),
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  float64(2),
+								"successes": float64(1),
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      float64(1),
+								"http_failures": float64(2),
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr2",
+					Nodes: []interface{}{
+						map[string]interface{}{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			giveInput: &PatchInput{
+				ID:      "u1",
+				SubPath: "",
+				Body:    patchUpstreamBytes,
+			},
+			wantInput: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream2",
+					Timeout: map[string]interface{}{
+						"connect": float64(20),
+						"send":    float64(20),
+						"read":    float64(20),
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  float64(2),
+								"successes": float64(1),
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      float64(1),
+								"http_failures": float64(2),
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr2",
+					Nodes: []interface{}{
+						map[string]interface{}{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream2",
+					Timeout: map[string]interface{}{
+						"connect": float64(20),
+						"send":    float64(20),
+						"read":    float64(20),
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  float64(2),
+								"successes": float64(1),
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      float64(1),
+								"http_failures": float64(2),
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr2",
+					Nodes: []interface{}{
+						map[string]interface{}{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			getCalled: true,
+		},
+		{
+			caseDesc: "patch success by path",
+			giveInput: &PatchInput{
+				ID:      "u1",
+				SubPath: "/nodes",
+				Body:    []byte(`[{"host": "172.16.238.20","port": 1981,"weight": 1}]`),
+			},
+			giveRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": float64(20),
+						"send":    float64(20),
+						"read":    float64(20),
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr_patch",
+					Nodes: []interface{}{
+						map[string]interface{}{
+							"host":   "172.16.238.20",
+							"port":   float64(1981),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantInput: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": float64(15),
+						"send":    float64(15),
+						"read":    float64(15),
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  float64(2),
+								"successes": float64(1),
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      float64(1),
+								"http_failures": float64(2),
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []interface{}{
+						map[string]interface{}{
+							"host":   "172.16.238.20",
+							"port":   float64(1981),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": float64(20),
+						"send":    float64(20),
+						"read":    float64(20),
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr_patch",
+					Nodes: []interface{}{
+						map[string]interface{}{
+							"host":   "172.16.238.20",
+							"port":   float64(1981),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			getCalled: true,
+		},
+		{
+			caseDesc: "patch failed, path error",
+			giveInput: &PatchInput{
+				ID:      "u1",
+				SubPath: "error",
+				Body:    []byte("0"),
+			},
+			wantRet: handler.SpecCodeResponse(
+				errors.New("add operation does not apply: doc is missing path: \"error\": missing value")),
+			wantErr: errors.New("add operation does not apply: doc is missing path: \"error\": missing value"),
+		},
+	}
+	for _, tc := range tests {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			getCalled := false
+
+			upstreamStore := &store.MockInterface{}
+			upstreamStore.On("Get", mock.Anything, mock.Anything).Return(existUpstream, nil)
+			upstreamStore.On("Update", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				getCalled = true
+				input := args.Get(1).(*entity.Upstream)
+				createIfNotExist := args.Get(2).(bool)
+				assert.Equal(t, tc.wantInput, input)
+				assert.False(t, createIfNotExist)
+			}).Return(tc.giveRet, tc.giveErr)
+
+			h := Handler{upstreamStore: upstreamStore}
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ret, err := h.Patch(ctx)
+			assert.Equal(t, tc.getCalled, getCalled)
+			assert.Equal(t, tc.wantRet, ret)
+			if tc.wantErr != nil && err != nil {
+				assert.Error(t, tc.wantErr.(error), err.Error())
+			} else {
+				assert.Equal(t, tc.wantErr, err)
+			}

Review comment:
       @Jaycean I'm a little confused. Could we use `assert.Equal(t, tc.wantErr, err)` here? Is this only for pretty logs for developers? Thanks.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix-dashboard] Jaycean commented on a change in pull request #1452: feat(be): refactor upstream unit test

Posted by GitBox <gi...@apache.org>.
Jaycean commented on a change in pull request #1452:
URL: https://github.com/apache/apisix-dashboard/pull/1452#discussion_r572632768



##########
File path: api/internal/handler/upstream/upstream_test.go
##########
@@ -19,285 +19,1719 @@ package upstream
 
 import (
 	"encoding/json"
-	"strings"
+	"errors"
+	"fmt"
+	"net/http"
 	"testing"
-	"time"
 
 	"github.com/shiningrush/droplet"
+	"github.com/shiningrush/droplet/data"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
 
-	"github.com/apisix/manager-api/internal/conf"
 	"github.com/apisix/manager-api/internal/core/entity"
-	"github.com/apisix/manager-api/internal/core/storage"
 	"github.com/apisix/manager-api/internal/core/store"
+	"github.com/apisix/manager-api/internal/handler"
+	"github.com/apisix/manager-api/internal/utils/consts"
 )
 
-var upstreamHandler *Handler
+func TestUpstream_Get(t *testing.T) {
+	tests := []struct {
+		caseDesc   string
+		giveInput  *GetInput
+		giveRet    *entity.Upstream
+		giveErr    error
+		wantErr    error
+		wantGetKey string
+		wantRet    interface{}
+	}{
+		{
+			caseDesc:   "upstream: get success",
+			giveInput:  &GetInput{ID: "u1"},
+			wantGetKey: "u1",
+			giveRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+		},
+		{
+			caseDesc:   "store get failed",
+			giveInput:  &GetInput{ID: "failed_key"},
+			wantGetKey: "failed_key",
+			giveErr:    fmt.Errorf("get failed"),
+			wantErr:    fmt.Errorf("get failed"),
+			wantRet: &data.SpecCodeResponse{
+				StatusCode: http.StatusInternalServerError,
+			},
+		},
+	}
 
-func TestUpstream(t *testing.T) {
-	// init
-	err := storage.InitETCDClient(conf.ETCDConfig)
-	assert.Nil(t, err)
-	err = store.InitStores()
-	assert.Nil(t, err)
+	for _, tc := range tests {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			getCalled := true
+			upstreamStore := &store.MockInterface{}
+			upstreamStore.On("Get", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				getCalled = true
+				assert.Equal(t, tc.wantGetKey, args.Get(0))
+			}).Return(tc.giveRet, tc.giveErr)
+
+			h := Handler{upstreamStore: upstreamStore}
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ret, err := h.Get(ctx)
+			assert.True(t, getCalled)
+			assert.Equal(t, tc.wantRet, ret)
+			assert.Equal(t, tc.wantErr, err)
+		})
+	}
+}
+
+func TestUpstreams_List(t *testing.T) {
+	mockData := []*entity.Upstream{
+		{
+			BaseInfo: entity.BaseInfo{
+				ID:         "u1",
+				CreateTime: 1609340491,
+				UpdateTime: 1609340491,
+			},
+			UpstreamDef: entity.UpstreamDef{
+				Name: "upstream1",
+				Key:  "server_addr",
+				Nodes: []map[string]interface{}{
+					{
+						"host":   "39.97.63.215",
+						"port":   float64(80),
+						"weight": float64(1),
+					},
+				},
+			},
+		},
+		{
+			BaseInfo: entity.BaseInfo{
+				ID:         "u2",
+				CreateTime: 1609340491,
+				UpdateTime: 1609340491,
+			},
+			UpstreamDef: entity.UpstreamDef{
+				Name: "upstream2",
+				Key:  "server_addr2",
+				Nodes: []map[string]interface{}{
+					{
+						"host":   "39.97.63.215",
+						"port":   float64(80),
+						"weight": float64(1),
+					},
+				},
+			},
+		},
+		{
+			BaseInfo: entity.BaseInfo{
+				ID:         "u3",
+				CreateTime: 1609340491,
+				UpdateTime: 1609340491,
+			},
+			UpstreamDef: entity.UpstreamDef{
+				Name: "upstream3",
+				Key:  "server_addr3",
+				Nodes: []map[string]interface{}{
+					{
+						"host":   "39.97.63.215",
+						"port":   float64(80),
+						"weight": float64(1),
+					},
+				},
+			},
+		},
+	}
 
-	upstreamHandler = &Handler{
-		upstreamStore: store.GetStore(store.HubKeyUpstream),
+	tests := []struct {
+		caseDesc  string
+		giveInput *ListInput
+		giveData  []*entity.Upstream
+		giveErr   error
+		wantErr   error
+		wantInput store.ListInput
+		wantRet   interface{}
+	}{
+		{
+			caseDesc: "list all upstream",
+			giveInput: &ListInput{
+				Pagination: store.Pagination{
+					PageSize:   10,
+					PageNumber: 10,
+				},
+			},
+			wantInput: store.ListInput{
+				PageSize:   10,
+				PageNumber: 10,
+			},
+			wantRet: &store.ListOutput{
+				Rows: []interface{}{
+					mockData[0],
+					mockData[1],
+					mockData[2],
+				},
+				TotalSize: 3,
+			},
+		},
+		{
+			caseDesc: "list upstream with 'upstream1'",
+			giveInput: &ListInput{
+				Name: "upstream1",
+				Pagination: store.Pagination{
+					PageSize:   10,
+					PageNumber: 10,
+				},
+			},
+			wantInput: store.ListInput{
+				PageSize:   10,
+				PageNumber: 10,
+			},
+			wantRet: &store.ListOutput{
+				Rows: []interface{}{
+					mockData[0],
+				},
+				TotalSize: 1,
+			},
+		},
 	}
-	assert.NotNil(t, upstreamHandler)
-
-	//create
-	ctx := droplet.NewContext()
-	upstream := &entity.Upstream{}
-	reqBody := `{
-		"id": "1",
-		"name": "upstream3",
-		"description": "upstream upstream",
-		"type": "roundrobin",
-		"nodes": [{
-			"host": "a.a.com",
-			"port": 80,
-			"weight": 1
-		}],
-		"timeout":{
-			"connect":15,
-			"send":15,
-			"read":15
-		},
-		"hash_on": "header",
-		"key": "server_addr",
-		"checks": {
-			"active": {
-				"timeout": 5,
-				"http_path": "/status",
-				"host": "foo.com",
-				"healthy": {
-					"interval": 2,
-					"successes": 1
-				},
-				"unhealthy": {
-					"interval": 1,
-					"http_failures": 2
-				},
-				"req_headers": ["User-Agent: curl/7.29.0"]
-			},
-			"passive": {
-				"healthy": {
-					"http_statuses": [200, 201],
-					"successes": 3
-				},
-				"unhealthy": {
-					"http_statuses": [500],
-					"http_failures": 3,
-					"tcp_failures": 3
+
+	for _, tc := range tests {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			getCalled := true
+			upstreamStore := &store.MockInterface{}
+			upstreamStore.On("List", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				getCalled = true
+				input := args.Get(0).(store.ListInput)
+				assert.Equal(t, tc.wantInput.PageSize, input.PageSize)
+				assert.Equal(t, tc.wantInput.PageNumber, input.PageNumber)
+			}).Return(func(input store.ListInput) *store.ListOutput {
+				var returnData []interface{}
+				for _, c := range mockData {
+					if input.Predicate(c) {
+						if input.Format == nil {
+							returnData = append(returnData, c)
+							continue
+						}
+						returnData = append(returnData, input.Format(c))
+					}
 				}
-			}
-		}
-	}`
-	err = json.Unmarshal([]byte(reqBody), upstream)
-	assert.Nil(t, err)
-	ctx.SetInput(upstream)
-	ret, err := upstreamHandler.Create(ctx)
-	assert.Nil(t, err)
-	objRet, ok := ret.(*entity.Upstream)
-	assert.True(t, ok)
-	assert.Equal(t, "1", objRet.ID)
-
-	//sleep
-	time.Sleep(time.Duration(100) * time.Millisecond)
-
-	//get
-	input := &GetInput{}
-	input.ID = "1"
-	ctx.SetInput(input)
-	ret, err = upstreamHandler.Get(ctx)
-	stored := ret.(*entity.Upstream)
-	assert.Nil(t, err)
-	assert.Equal(t, stored.ID, upstream.ID)
-
-	//update
-	upstream2 := &UpdateInput{}
-	upstream2.ID = "1"
-	reqBody = `{
-		"id": "1",
-		"name": "upstream3",
-		"description": "upstream upstream",
-		"type": "roundrobin",
-		"nodes": [{
-			"host": "a.a.com",
-			"port": 80,
-			"weight": 1
-		}],
-		"timeout":{
-			"connect":15,
-			"send":15,
-			"read":15
-		},
-		"enable_websocket": true,
-		"hash_on": "header",
-		"key": "server_addr",
-		"checks": {
-			"active": {
-				"timeout": 5,
-				"http_path": "/status",
-				"host": "foo.com",
-				"healthy": {
-					"interval": 2,
-					"successes": 1
-				},
-				"unhealthy": {
-					"interval": 1,
-					"http_failures": 2
-				},
-				"req_headers": ["User-Agent: curl/7.29.0"]
-			},
-			"passive": {
-				"healthy": {
-					"http_statuses": [200, 201],
-					"successes": 3
-				},
-				"unhealthy": {
-					"http_statuses": [500],
-					"http_failures": 3,
-					"tcp_failures": 3
+				return &store.ListOutput{
+					Rows:      returnData,
+					TotalSize: len(returnData),
 				}
-			}
-		}
-	}`
-	err = json.Unmarshal([]byte(reqBody), upstream2)
-	assert.Nil(t, err)
-	ctx.SetInput(upstream2)
-	ret, err = upstreamHandler.Update(ctx)
-	assert.Nil(t, err)
-	// check the returned value
-	objRet, ok = ret.(*entity.Upstream)
-	assert.True(t, ok)
-	assert.Equal(t, upstream2.ID, objRet.ID)
-
-	//list
-	listInput := &ListInput{}
-	reqBody = `{"page_size": 1, "page": 1}`
-	err = json.Unmarshal([]byte(reqBody), listInput)
-	assert.Nil(t, err)
-	ctx.SetInput(listInput)
-	retPage, err := upstreamHandler.List(ctx)
-	assert.Nil(t, err)
-	dataPage := retPage.(*store.ListOutput)
-	assert.Equal(t, len(dataPage.Rows), 1)
+			}, tc.giveErr)
 
-	//delete test data
-	inputDel := &BatchDelete{}
-	reqBody = `{"ids": "1"}`
-	err = json.Unmarshal([]byte(reqBody), inputDel)
-	assert.Nil(t, err)
-	ctx.SetInput(inputDel)
-	_, err = upstreamHandler.BatchDelete(ctx)
-	assert.Nil(t, err)
+			h := Handler{upstreamStore: upstreamStore}
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ret, err := h.List(ctx)
+			assert.True(t, getCalled)
+			assert.Equal(t, tc.wantRet, ret)
+			assert.Equal(t, tc.wantErr, err)
+		})
+	}
+}
+
+func TestUpstream_Create(t *testing.T) {
+	tests := []struct {
+		caseDesc  string
+		getCalled bool
+		giveInput *entity.Upstream
+		giveRet   interface{}
+		giveErr   error
+		wantInput *entity.Upstream
+		wantErr   error
+		wantRet   interface{}
+	}{
+		{
+			caseDesc:  "create success",
+			getCalled: true,
+			giveInput: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			giveRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantInput: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantErr: nil,
+		},
+		{
+			caseDesc:  "create failed, create return error",
+			getCalled: true,
+			giveInput: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			giveErr: fmt.Errorf("create failed"),
+			wantInput: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": float64(3),
+								"tcp_failures":  float64(3),
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantErr: fmt.Errorf("create failed"),
+			wantRet: handler.SpecCodeResponse(fmt.Errorf("create failed")),
+		},
+	}
 
+	for _, tc := range tests {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			getCalled := false
+
+			upstreamStore := &store.MockInterface{}
+			upstreamStore.On("Create", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				getCalled = true
+				input := args.Get(1).(*entity.Upstream)
+				assert.Equal(t, tc.wantInput, input)
+			}).Return(tc.giveRet, tc.giveErr)
+
+			h := Handler{upstreamStore: upstreamStore}
+
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ret, err := h.Create(ctx)
+			assert.True(t, getCalled)
+			assert.Equal(t, tc.wantRet, ret)
+			assert.Equal(t, tc.wantErr, err)
+		})
+	}
 }
 
-func TestUpstream_Pass_Host(t *testing.T) {
-	//create
-	ctx := droplet.NewContext()
-	upstream := &entity.Upstream{}
-	reqBody := `{
-		"id": "2",
-		"nodes": [{
-			"host": "httpbin.org",
-			"port": 80,
-			"weight": 1
-		}],
-		"type": "roundrobin",
-		"pass_host": "node"
-	}`
-	err := json.Unmarshal([]byte(reqBody), upstream)
-	assert.Nil(t, err)
-	ctx.SetInput(upstream)
-	ret, err := upstreamHandler.Create(ctx)
-	assert.Nil(t, err)
-	objRet, ok := ret.(*entity.Upstream)
-	assert.True(t, ok)
-	assert.Equal(t, "2", objRet.ID)
-
-	//sleep
-	time.Sleep(time.Duration(20) * time.Millisecond)
-
-	//get
-	input := &GetInput{}
-	input.ID = "2"
-	ctx.SetInput(input)
-	ret, err = upstreamHandler.Get(ctx)
-	stored := ret.(*entity.Upstream)
-	assert.Nil(t, err)
-	assert.Equal(t, stored.ID, upstream.ID)
+func TestUpstream_Update(t *testing.T) {
+	tests := []struct {
+		caseDesc  string
+		getCalled bool
+		giveInput *UpdateInput
+		giveErr   error
+		giveRet   interface{}
+		wantInput *entity.Upstream
+		wantErr   error
+		wantRet   interface{}
+	}{
+		{
+			caseDesc:  "update success",
+			getCalled: true,
+			giveInput: &UpdateInput{
+				ID: "u1",
+				Upstream: entity.Upstream{
+					UpstreamDef: entity.UpstreamDef{
+						Name: "upstream1",
+						Timeout: map[string]interface{}{
+							"connect": 15,
+							"send":    15,
+							"read":    15,
+						},
+						Checks: map[string]interface{}{
+							"active": map[string]interface{}{
+								"timeout":   float64(5),
+								"http_path": "/status",
+								"host":      "foo.com",
+								"healthy": map[string]interface{}{
+									"interval":  2,
+									"successes": 1,
+								},
+								"unhealthy": map[string]interface{}{
+									"interval":      1,
+									"http_failures": 2,
+								},
+								"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+							},
+							"passive": map[string]interface{}{
+								"healthy": map[string]interface{}{
+									"http_statuses": []interface{}{float64(200), float64(201)},
+									"successes":     float64(3),
+								},
+								"unhealthy": map[string]interface{}{
+									"http_statuses": []interface{}{float64(500)},
+									"http_failures": 3,
+									"tcp_failures":  3,
+								},
+							},
+						},
+						Key: "server_addr",
+						Nodes: []map[string]interface{}{
+							{
+								"host":   "39.97.63.215",
+								"port":   float64(80),
+								"weight": float64(1),
+							},
+						},
+					},
+				},
+			},
+			giveRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": 3,
+								"tcp_failures":  3,
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantInput: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": 3,
+								"tcp_failures":  3,
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+			wantRet: &entity.Upstream{
+				BaseInfo: entity.BaseInfo{
+					ID: "u1",
+				},
+				UpstreamDef: entity.UpstreamDef{
+					Name: "upstream1",
+					Timeout: map[string]interface{}{
+						"connect": 15,
+						"send":    15,
+						"read":    15,
+					},
+					Checks: map[string]interface{}{
+						"active": map[string]interface{}{
+							"timeout":   float64(5),
+							"http_path": "/status",
+							"host":      "foo.com",
+							"healthy": map[string]interface{}{
+								"interval":  2,
+								"successes": 1,
+							},
+							"unhealthy": map[string]interface{}{
+								"interval":      1,
+								"http_failures": 2,
+							},
+							"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+						},
+						"passive": map[string]interface{}{
+							"healthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(200), float64(201)},
+								"successes":     float64(3),
+							},
+							"unhealthy": map[string]interface{}{
+								"http_statuses": []interface{}{float64(500)},
+								"http_failures": 3,
+								"tcp_failures":  3,
+							},
+						},
+					},
+					Key: "server_addr",
+					Nodes: []map[string]interface{}{
+						{
+							"host":   "39.97.63.215",
+							"port":   float64(80),
+							"weight": float64(1),
+						},
+					},
+				},
+			},
+		},
+		{
+			caseDesc: "create failed, different id",
+			giveInput: &UpdateInput{
+				ID: "u1",
+				Upstream: entity.Upstream{
+					BaseInfo: entity.BaseInfo{
+						ID: "u2",
+					},
+					UpstreamDef: entity.UpstreamDef{
+						Name: "upstream1",
+						Timeout: map[string]interface{}{
+							"connect": 15,
+							"send":    15,
+							"read":    15,
+						},
+						Checks: map[string]interface{}{
+							"active": map[string]interface{}{
+								"timeout":   float64(5),
+								"http_path": "/status",
+								"host":      "foo.com",
+								"healthy": map[string]interface{}{
+									"interval":  2,
+									"successes": 1,
+								},
+								"unhealthy": map[string]interface{}{
+									"interval":      1,
+									"http_failures": 2,
+								},
+								"req_headers": []interface{}{"User-Agent: curl/7.29.0"},
+							},
+							"passive": map[string]interface{}{
+								"healthy": map[string]interface{}{
+									"http_statuses": []interface{}{float64(200), float64(201)},
+									"successes":     float64(3),
+								},
+								"unhealthy": map[string]interface{}{
+									"http_statuses": []interface{}{float64(500)},
+									"http_failures": 3,
+									"tcp_failures":  3,
+								},
+							},
+						},
+						Key: "server_addr",
+						Nodes: []map[string]interface{}{
+							{
+								"host":   "39.97.63.215",
+								"port":   float64(80),
+								"weight": float64(1),
+							},
+						},
+					},
+				},
+			},
+			wantRet: &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
+			wantErr: fmt.Errorf("ID on path (u1) doesn't match ID on body (u2)"),
+		},

Review comment:
       Update I added a test case that could not be updated due to ID matching failure, but my description is wrong and has been fixed.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix-dashboard] nic-chen commented on pull request #1452: feat(be): refactor upstream unit test

Posted by GitBox <gi...@apache.org>.
nic-chen commented on pull request #1452:
URL: https://github.com/apache/apisix-dashboard/pull/1452#issuecomment-775062816


   @Jaycean  What is the current code coverage of `upstream.go`? It's a little strange that this pr doesn't show coverage
   
   


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org