blob: 3132843da8d57a5fc36dcf94c6f72e0610b3c58b [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package service
import (
"errors"
"fmt"
"net/http"
"testing"
"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/core/entity"
"github.com/apisix/manager-api/internal/core/store"
"github.com/apisix/manager-api/internal/handler"
)
func TestService_Get(t *testing.T) {
tests := []struct {
caseDesc string
giveInput *GetInput
giveRet *entity.Service
giveErr error
wantErr error
wantGetKey string
wantRet interface{}
}{
{
caseDesc: "normal",
giveInput: &GetInput{ID: "s1"},
wantGetKey: "s1",
giveRet: &entity.Service{
BaseInfo: entity.BaseInfo{
ID: "s1",
},
Plugins: map[string]interface{}{
"limit-count": map[string]interface{}{
"count": 2,
"time_window": 60,
"rejected_code": 503,
"key": "remote_addr",
},
},
},
wantRet: &entity.Service{
BaseInfo: entity.BaseInfo{
ID: "s1",
},
Plugins: map[string]interface{}{
"limit-count": map[string]interface{}{
"count": 2,
"time_window": 60,
"rejected_code": 503,
"key": "remote_addr",
},
},
},
},
{
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,
},
},
}
for _, tc := range tests {
t.Run(tc.caseDesc, func(t *testing.T) {
getCalled := true
mStore := &store.MockInterface{}
mStore.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{serviceStore: mStore}
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 TestService_List(t *testing.T) {
tests := []struct {
caseDesc string
giveInput *ListInput
giveData []*entity.Service
giveErr error
wantErr error
wantInput store.ListInput
wantRet interface{}
}{
{
caseDesc: "list all service",
giveInput: &ListInput{
Pagination: store.Pagination{
PageSize: 10,
PageNumber: 10,
},
},
wantInput: store.ListInput{
PageSize: 10,
PageNumber: 10,
},
giveData: []*entity.Service{
{Name: "s1"},
{Name: "s2"},
{Name: "test_service"},
{Name: "service_test"},
},
wantRet: &store.ListOutput{
Rows: []interface{}{
&entity.Service{Name: "s1"},
&entity.Service{Name: "s2"},
&entity.Service{Name: "test_service"},
&entity.Service{Name: "service_test"},
},
TotalSize: 4,
},
},
{
caseDesc: "list service with 'service'",
giveInput: &ListInput{
Name: "service",
Pagination: store.Pagination{
PageSize: 10,
PageNumber: 10,
},
},
wantInput: store.ListInput{
PageSize: 10,
PageNumber: 10,
},
giveData: []*entity.Service{
{BaseInfo: entity.BaseInfo{CreateTime: 1609376661}, Name: "s1"},
{BaseInfo: entity.BaseInfo{CreateTime: 1609376662}, Name: "s2"},
{BaseInfo: entity.BaseInfo{CreateTime: 1609376663}, Name: "test_service"},
{BaseInfo: entity.BaseInfo{CreateTime: 1609376664}, Name: "service_test"},
},
wantRet: &store.ListOutput{
Rows: []interface{}{
&entity.Service{BaseInfo: entity.BaseInfo{CreateTime: 1609376663}, Name: "test_service"},
&entity.Service{BaseInfo: entity.BaseInfo{CreateTime: 1609376664}, Name: "service_test"},
},
TotalSize: 2,
},
},
{
caseDesc: "list service with key s1",
giveInput: &ListInput{
Name: "s1",
Pagination: store.Pagination{
PageSize: 10,
PageNumber: 10,
},
},
wantInput: store.ListInput{
PageSize: 10,
PageNumber: 10,
},
giveData: []*entity.Service{
{Name: "s1"},
{Name: "s2"},
{Name: "test_service"},
{Name: "service_test"},
},
wantRet: &store.ListOutput{
Rows: []interface{}{
&entity.Service{Name: "s1"},
},
TotalSize: 1,
},
},
{
caseDesc: "list service and format",
giveInput: &ListInput{
Pagination: store.Pagination{
PageSize: 10,
PageNumber: 10,
},
},
wantInput: store.ListInput{
PageSize: 10,
PageNumber: 10,
},
giveData: []*entity.Service{
{
Name: "s1",
Upstream: &entity.UpstreamDef{
Nodes: []interface{}{
map[string]interface{}{
"host": "39.97.63.215",
"port": float64(80),
"weight": float64(1),
"priority": float64(10),
},
},
},
},
{Name: "s2"},
{Name: "test_service"},
{Name: "service_test"},
},
wantRet: &store.ListOutput{
Rows: []interface{}{
&entity.Service{Name: "s1", Upstream: &entity.UpstreamDef{
Nodes: []*entity.Node{
{
Host: "39.97.63.215",
Port: 80,
Weight: 1,
Priority: 10,
},
},
}},
&entity.Service{Name: "s2"},
&entity.Service{Name: "test_service"},
&entity.Service{Name: "service_test"},
},
TotalSize: 4,
},
},
}
for _, tc := range tests {
t.Run(tc.caseDesc, func(t *testing.T) {
getCalled := true
mStore := &store.MockInterface{}
mStore.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 tc.giveData {
if input.Predicate(c) {
if input.Format == nil {
returnData = append(returnData, c)
continue
}
returnData = append(returnData, input.Format(c))
}
}
return &store.ListOutput{
Rows: returnData,
TotalSize: len(returnData),
}
}, tc.giveErr)
h := Handler{serviceStore: mStore}
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 TestService_Create(t *testing.T) {
tests := []struct {
caseDesc string
getCalled bool
giveInput *entity.Service
giveRet interface{}
giveErr error
wantInput *entity.Service
wantErr error
wantRet interface{}
upstreamInput string
upstreamRet interface{}
upstreamErr interface{}
nameExistRet []interface{}
}{
{
caseDesc: "create success",
getCalled: true,
giveInput: &entity.Service{
Name: "s1",
UpstreamID: "u1",
Desc: "test service",
},
wantInput: &entity.Service{
Name: "s1",
UpstreamID: "u1",
Desc: "test service",
},
upstreamInput: "u1",
upstreamRet: entity.Upstream{
BaseInfo: entity.BaseInfo{
ID: "u1",
},
},
},
{
caseDesc: "create failed, upstream not found",
getCalled: false,
giveInput: &entity.Service{
Name: "s1",
UpstreamID: "u1",
Desc: "test service",
},
wantInput: &entity.Service{
Name: "s1",
UpstreamID: "u1",
Desc: "test service",
},
wantErr: fmt.Errorf("upstream id: u1 not found"),
wantRet: &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
upstreamInput: "u1",
upstreamErr: data.ErrNotFound,
},
{
caseDesc: "create failed, upstream return error",
getCalled: false,
giveInput: &entity.Service{
Name: "s1",
UpstreamID: "u1",
Desc: "test service",
},
wantInput: &entity.Service{
Name: "s1",
UpstreamID: "u1",
Desc: "test service",
},
wantErr: fmt.Errorf("unknown error"),
wantRet: &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
upstreamInput: "u1",
upstreamErr: fmt.Errorf("unknown error"),
},
{
caseDesc: "create failed, create return error",
getCalled: true,
giveInput: &entity.Service{
Name: "s1",
UpstreamID: "u1",
Desc: "test service",
},
giveErr: fmt.Errorf("create failed"),
wantInput: &entity.Service{
Name: "s1",
UpstreamID: "u1",
Desc: "test service",
},
upstreamInput: "u1",
upstreamRet: entity.Upstream{
BaseInfo: entity.BaseInfo{
ID: "u1",
},
},
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
serviceStore := &store.MockInterface{}
serviceStore.On("Create", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
getCalled = true
input := args.Get(1).(*entity.Service)
assert.Equal(t, tc.wantInput, input)
}).Return(tc.giveRet, tc.giveErr)
upstreamStore := &store.MockInterface{}
upstreamStore.On("Get", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
id := args.Get(0).(string)
assert.Equal(t, tc.upstreamInput, id)
}).Return(tc.upstreamRet, tc.upstreamErr)
serviceStore.On("List", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
}).Return(func(input store.ListInput) *store.ListOutput {
return &store.ListOutput{
Rows: tc.nameExistRet,
TotalSize: len(tc.nameExistRet),
}
}, nil)
h := Handler{serviceStore: serviceStore, upstreamStore: upstreamStore}
ctx := droplet.NewContext()
ctx.SetInput(tc.giveInput)
ret, err := h.Create(ctx)
assert.Equal(t, tc.getCalled, getCalled)
assert.Equal(t, tc.wantRet, ret)
assert.Equal(t, tc.wantErr, err)
})
}
}
func TestService_Update(t *testing.T) {
tests := []struct {
caseDesc string
getCalled bool
giveInput *UpdateInput
giveErr error
giveRet interface{}
wantInput *entity.Service
wantErr error
wantRet interface{}
upstreamInput string
upstreamRet interface{}
upstreamErr interface{}
nameExistRet []interface{}
}{
{
caseDesc: "create success",
getCalled: true,
giveInput: &UpdateInput{
ID: "s1",
Service: entity.Service{
Name: "s1",
UpstreamID: "u1",
Desc: "test service",
},
},
wantInput: &entity.Service{
BaseInfo: entity.BaseInfo{
ID: "s1",
},
Name: "s1",
UpstreamID: "u1",
Desc: "test service",
},
upstreamInput: "u1",
upstreamRet: entity.Upstream{
BaseInfo: entity.BaseInfo{
ID: "u1",
},
},
},
{
caseDesc: "create failed, different id",
giveInput: &UpdateInput{
ID: "s1",
Service: entity.Service{
BaseInfo: entity.BaseInfo{
ID: "s2",
},
Name: "s1",
UpstreamID: "u1",
Desc: "test service",
},
},
wantRet: &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
wantErr: fmt.Errorf("ID on path (s1) doesn't match ID on body (s2)"),
},
{
caseDesc: "update failed, upstream not found",
giveInput: &UpdateInput{
ID: "s1",
Service: entity.Service{
Name: "s1",
UpstreamID: "u1",
Desc: "test service",
},
},
wantInput: &entity.Service{
Name: "s1",
UpstreamID: "u1",
Desc: "test service",
},
wantErr: fmt.Errorf("upstream id: u1 not found"),
wantRet: &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
upstreamInput: "u1",
upstreamErr: data.ErrNotFound,
},
{
caseDesc: "update failed, upstream return error",
giveInput: &UpdateInput{
ID: "s1",
Service: entity.Service{
Name: "s1",
UpstreamID: "u1",
Desc: "test service",
},
},
wantInput: &entity.Service{
Name: "s1",
UpstreamID: "u1",
Desc: "test service",
},
wantErr: fmt.Errorf("unknown error"),
wantRet: &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
upstreamInput: "u1",
upstreamErr: fmt.Errorf("unknown error"),
},
{
caseDesc: "update failed, update return error",
getCalled: true,
giveInput: &UpdateInput{
ID: "s1",
Service: entity.Service{
Name: "s1",
UpstreamID: "u1",
Desc: "test service",
},
},
giveErr: fmt.Errorf("update failed"),
upstreamInput: "u1",
upstreamRet: entity.Upstream{
BaseInfo: entity.BaseInfo{
ID: "u1",
},
},
wantInput: &entity.Service{
BaseInfo: entity.BaseInfo{ID: "s1"},
Name: "s1",
UpstreamID: "u1",
Desc: "test service",
},
wantErr: fmt.Errorf("update failed"),
wantRet: handler.SpecCodeResponse(fmt.Errorf("update failed")),
},
}
for _, tc := range tests {
t.Run(tc.caseDesc, func(t *testing.T) {
getCalled := false
serviceStore := &store.MockInterface{}
serviceStore.On("Update", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
getCalled = true
input := args.Get(1).(*entity.Service)
createIfNotExist := args.Get(2).(bool)
assert.Equal(t, tc.wantInput, input)
assert.True(t, createIfNotExist)
}).Return(tc.giveRet, tc.giveErr)
upstreamStore := &store.MockInterface{}
upstreamStore.On("Get", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
id := args.Get(0).(string)
assert.Equal(t, tc.upstreamInput, id)
}).Return(tc.upstreamRet, tc.upstreamErr)
serviceStore.On("List", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
}).Return(func(input store.ListInput) *store.ListOutput {
return &store.ListOutput{
Rows: tc.nameExistRet,
TotalSize: len(tc.nameExistRet),
}
}, nil)
h := Handler{serviceStore: serviceStore, 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 TestService_Patch(t *testing.T) {
existService := &entity.Service{
BaseInfo: entity.BaseInfo{
ID: "s1",
CreateTime: 1609340491,
UpdateTime: 1609340491,
},
Name: "exist_service",
UpstreamID: "u1",
EnableWebsocket: false,
Labels: map[string]string{
"version": "v1",
},
Plugins: map[string]interface{}{
"limit-count": map[string]interface{}{
"count": 2,
"time_window": 60,
"rejected_code": 503,
"key": "remote_addr",
},
},
}
tests := []struct {
caseDesc string
giveInput *PatchInput
giveErr error
giveRet interface{}
wantInput *entity.Service
wantErr error
wantRet interface{}
serviceInput string
serviceRet *entity.Service
serviceErr error
called bool
}{
{
caseDesc: "patch all success",
giveInput: &PatchInput{
ID: "s1",
SubPath: "",
Body: []byte(`{
"name":"patched",
"upstream_id":"u2",
"enable_websocket":true,
"labels":{
"version":"v1",
"build":"16"
},
"plugins":{
"limit-count":{
"count":2,
"time_window":60,
"rejected_code": 504,
"key":"remote_addr"
},
"key-auth":{
"key":"auth-one"
}
}
}`),
},
wantInput: &entity.Service{
BaseInfo: entity.BaseInfo{
ID: "s1",
CreateTime: 1609340491,
UpdateTime: 1609340491,
},
Name: "patched",
UpstreamID: "u2",
EnableWebsocket: true,
Labels: map[string]string{
"version": "v1",
"build": "16",
},
Plugins: map[string]interface{}{
"limit-count": map[string]interface{}{
"count": float64(2),
"time_window": float64(60),
"rejected_code": float64(504),
"key": "remote_addr",
},
"key-auth": map[string]interface{}{
"key": "auth-one",
},
},
},
serviceInput: "s1",
serviceRet: existService,
called: true,
},
{
caseDesc: "patch part of service success",
giveInput: &PatchInput{
ID: "s1",
SubPath: "",
Body: []byte(`{
"name":"patched",
"upstream_id":"u2",
"enable_websocket":true,
"labels":{
"version":"v1",
"build":"16"
}
}`),
},
wantInput: &entity.Service{
BaseInfo: entity.BaseInfo{
ID: "s1",
CreateTime: 1609340491,
UpdateTime: 1609340491,
},
Name: "patched",
UpstreamID: "u2",
EnableWebsocket: true,
Labels: map[string]string{
"version": "v1",
"build": "16",
},
Plugins: map[string]interface{}{
"limit-count": map[string]interface{}{
"count": float64(2),
"time_window": float64(60),
"rejected_code": float64(503),
"key": "remote_addr",
},
},
},
serviceInput: "s1",
serviceRet: existService,
called: true,
},
{
caseDesc: "patch name success with sub path",
giveInput: &PatchInput{
ID: "s1",
SubPath: "/upstream_id",
Body: []byte(`{"upstream_id":"u3"}`),
},
wantInput: &entity.Service{
BaseInfo: entity.BaseInfo{
ID: "s1",
CreateTime: 1609340491,
UpdateTime: 1609340491,
},
Name: "exist_service",
UpstreamID: map[string]interface{}{
"upstream_id": "u3",
},
EnableWebsocket: false,
Labels: map[string]string{
"version": "v1",
},
Plugins: map[string]interface{}{
"limit-count": map[string]interface{}{
"count": float64(2),
"time_window": float64(60),
"rejected_code": float64(503),
"key": "remote_addr",
},
},
},
serviceInput: "s1",
serviceRet: existService,
called: true,
},
{
caseDesc: "patch labels success",
giveInput: &PatchInput{
ID: "s1",
SubPath: "/labels",
Body: []byte(`{"version": "v3"}`),
},
wantInput: &entity.Service{
BaseInfo: entity.BaseInfo{
ID: "s1",
CreateTime: 1609340491,
UpdateTime: 1609340491,
},
Name: "exist_service",
EnableWebsocket: false,
Labels: map[string]string{
"version": "v3",
},
UpstreamID: "u1",
Plugins: map[string]interface{}{
"limit-count": map[string]interface{}{
"count": float64(2),
"time_window": float64(60),
"rejected_code": float64(503),
"key": "remote_addr",
},
},
},
serviceInput: "s1",
serviceRet: existService,
called: true,
},
{
caseDesc: "patch failed, service store get error",
giveInput: &PatchInput{
ID: "s1",
Body: []byte{},
},
serviceInput: "s1",
serviceErr: fmt.Errorf("get error"),
wantRet: handler.SpecCodeResponse(fmt.Errorf("get error")),
wantErr: fmt.Errorf("get error"),
called: false,
},
}
for _, tc := range tests {
t.Run(tc.caseDesc, func(t *testing.T) {
getCalled := false
serviceStore := &store.MockInterface{}
serviceStore.On("Update", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
getCalled = true
input := args.Get(1).(*entity.Service)
createIfNotExist := args.Get(2).(bool)
assert.Equal(t, tc.wantInput, input)
assert.False(t, createIfNotExist)
}).Return(tc.giveRet, tc.giveErr)
serviceStore.On("Get", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
input := args.Get(0).(string)
assert.Equal(t, tc.serviceInput, input)
}).Return(tc.serviceRet, tc.serviceErr)
h := Handler{serviceStore: serviceStore}
ctx := droplet.NewContext()
ctx.SetInput(tc.giveInput)
ret, err := h.Patch(ctx)
assert.Equal(t, tc.called, getCalled)
assert.Equal(t, tc.wantRet, ret)
assert.Equal(t, tc.wantErr, err)
})
}
}
func TestServices_Delete(t *testing.T) {
tests := []struct {
caseDesc string
giveInput *BatchDelete
giveErr error
wantInput []string
wantErr error
wantRet interface{}
routeMockData []*entity.Route
routeMockErr error
getCalled bool
}{
{
caseDesc: "delete success",
giveInput: &BatchDelete{
IDs: "s1",
},
wantInput: []string{"s1"},
getCalled: true,
},
{
caseDesc: "batch delete success",
giveInput: &BatchDelete{
IDs: "s1,s2",
},
wantInput: []string{"s1", "s2"},
getCalled: true,
},
{
caseDesc: "delete failed",
giveInput: &BatchDelete{
IDs: "s1",
},
giveErr: fmt.Errorf("delete error"),
wantInput: []string{"s1"},
wantRet: handler.SpecCodeResponse(fmt.Errorf("delete error")),
wantErr: fmt.Errorf("delete error"),
getCalled: true,
},
{
caseDesc: "delete failed, route is using",
giveInput: &BatchDelete{
IDs: "s1",
},
wantInput: []string{"s1"},
routeMockData: []*entity.Route{
&entity.Route{
BaseInfo: entity.BaseInfo{
ID: "r1",
CreateTime: 1609746531,
},
Name: "route1",
Desc: "test_route",
UpstreamID: "u1",
ServiceID: "s1",
Labels: map[string]string{
"version": "v1",
},
},
},
routeMockErr: nil,
getCalled: false,
wantRet: &data.SpecCodeResponse{StatusCode: 400},
wantErr: errors.New("route: route1 is using this service"),
},
{
caseDesc: "delete failed, route list error",
giveInput: &BatchDelete{
IDs: "s1",
},
wantInput: []string{"s1"},
routeMockData: nil,
routeMockErr: errors.New("route list error"),
wantRet: handler.SpecCodeResponse(errors.New("route list error")),
wantErr: errors.New("route list error"),
getCalled: false,
},
}
for _, tc := range tests {
t.Run(tc.caseDesc, func(t *testing.T) {
getCalled := false
serviceStore := &store.MockInterface{}
serviceStore.On("BatchDelete", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
getCalled = true
input := args.Get(1).([]string)
assert.Equal(t, tc.wantInput, input)
}).Return(tc.giveErr)
routeStore := &store.MockInterface{}
routeStore.On("List", mock.Anything).Return(func(input store.ListInput) *store.ListOutput {
var returnData []interface{}
for _, c := range tc.routeMockData {
if input.Predicate(c) {
if input.Format == nil {
returnData = append(returnData, c)
continue
}
returnData = append(returnData, input.Format(c))
}
}
return &store.ListOutput{
Rows: returnData,
TotalSize: len(returnData),
}
}, tc.routeMockErr)
h := Handler{serviceStore: serviceStore, routeStore: routeStore}
ctx := droplet.NewContext()
ctx.SetInput(tc.giveInput)
ret, err := h.BatchDelete(ctx)
assert.Equal(t, tc.getCalled, getCalled)
assert.Equal(t, tc.wantRet, ret)
assert.Equal(t, tc.wantErr, err)
})
}
}