blob: aab0731acdbd786e49bc4acabe8c7211cf67fc3c [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 route
import (
"encoding/json"
"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"
"github.com/apisix/manager-api/internal/utils/consts"
)
type testCase struct {
caseDesc string
giveInput interface{}
mockInput interface{}
mockRet interface{}
mockErr interface{}
wantRet interface{}
wantErr interface{}
called bool
serviceInput string
scriptRet interface{}
scriptErr error
serviceRet interface{}
serviceErr error
upstreamRet interface{}
upstreamErr error
nameExistRet []interface{}
}
var DagScript = `
{
"rule":{
"root":"451106f8-560c-43a4-acf2-2a6ed0ea57b8",
"451106f8-560c-43a4-acf2-2a6ed0ea57b8":[
[
"code == 403",
"b93d622c-92ef-48b4-b6bb-57e1ce893ee3"
],
[
"",
"988ef5c2-c896-4606-a666-3d4cbe24a731"
]
]
},
"conf":{
"451106f8-560c-43a4-acf2-2a6ed0ea57b8":{
"name":"uri-blocker",
"conf":{
"block_rules":[
"root.exe",
"root.m+"
],
"rejected_code":403
}
},
"988ef5c2-c896-4606-a666-3d4cbe24a731":{
"name":"kafka-logger",
"conf":{
"batch_max_size":1000,
"broker_list":{
},
"buffer_duration":60,
"inactive_timeout":5,
"include_req_body":false,
"kafka_topic":"1",
"key":"2",
"max_retry_count":0,
"name":"kafka logger",
"retry_delay":1,
"timeout":3
}
},
"b93d622c-92ef-48b4-b6bb-57e1ce893ee3":{
"name":"fault-injection",
"conf":{
"abort":{
"body":"200",
"http_status":300
},
"delay":{
"duration":500
}
}
}
},
"chart":{
"hovered":{
},
"links":{
"3a110c30-d6f3-40b1-a8ac-b828cfaa2489":{
"from":{
"nodeId":"3365eca3-4bc8-4769-bab3-1485dfd6a43c",
"portId":"port3"
},
"id":"3a110c30-d6f3-40b1-a8ac-b828cfaa2489",
"to":{
"nodeId":"b93d622c-92ef-48b4-b6bb-57e1ce893ee3",
"portId":"port1"
}
},
"c1958993-c1ef-44b1-bb32-7fc6f34870c2":{
"from":{
"nodeId":"3365eca3-4bc8-4769-bab3-1485dfd6a43c",
"portId":"port2"
},
"id":"c1958993-c1ef-44b1-bb32-7fc6f34870c2",
"to":{
"nodeId":"988ef5c2-c896-4606-a666-3d4cbe24a731",
"portId":"port1"
}
},
"f9c42bf6-c8aa-4e86-8498-8dfbc5c53c23":{
"from":{
"nodeId":"451106f8-560c-43a4-acf2-2a6ed0ea57b8",
"portId":"port2"
},
"id":"f9c42bf6-c8aa-4e86-8498-8dfbc5c53c23",
"to":{
"nodeId":"3365eca3-4bc8-4769-bab3-1485dfd6a43c",
"portId":"port1"
}
}
},
"nodes":{
"3365eca3-4bc8-4769-bab3-1485dfd6a43c":{
"id":"3365eca3-4bc8-4769-bab3-1485dfd6a43c",
"orientation":0,
"ports":{
"port1":{
"id":"port1",
"position":{
"x":107,
"y":0
},
"type":"input"
},
"port2":{
"id":"port2",
"position":{
"x":92,
"y":96
},
"properties":{
"value":"no"
},
"type":"output"
},
"port3":{
"id":"port3",
"position":{
"x":122,
"y":96
},
"properties":{
"value":"yes"
},
"type":"output"
}
},
"position":{
"x":750.2627969928922,
"y":301.0370335799397
},
"properties":{
"customData":{
"name":"code == 403",
"type":1
}
},
"size":{
"height":96,
"width":214
},
"type":"判断条件"
},
"451106f8-560c-43a4-acf2-2a6ed0ea57b8":{
"id":"451106f8-560c-43a4-acf2-2a6ed0ea57b8",
"orientation":0,
"ports":{
"port1":{
"id":"port1",
"position":{
"x":100,
"y":0
},
"properties":{
"custom":"property"
},
"type":"input"
},
"port2":{
"id":"port2",
"position":{
"x":100,
"y":96
},
"properties":{
"custom":"property"
},
"type":"output"
}
},
"position":{
"x":741.5684544145346,
"y":126.75879247285502
},
"properties":{
"customData":{
"data":{
"block_rules":[
"root.exe",
"root.m+"
],
"rejected_code":403
},
"name":"uri-blocker",
"type":0
}
},
"size":{
"height":96,
"width":201
},
"type":"uri-blocker"
},
"988ef5c2-c896-4606-a666-3d4cbe24a731":{
"id":"988ef5c2-c896-4606-a666-3d4cbe24a731",
"orientation":0,
"ports":{
"port1":{
"id":"port1",
"position":{
"x":106,
"y":0
},
"properties":{
"custom":"property"
},
"type":"input"
},
"port2":{
"id":"port2",
"position":{
"x":106,
"y":96
},
"properties":{
"custom":"property"
},
"type":"output"
}
},
"position":{
"x":607.9687500000001,
"y":471.17788461538447
},
"properties":{
"customData":{
"data":{
"batch_max_size":1000,
"broker_list":{
},
"buffer_duration":60,
"inactive_timeout":5,
"include_req_body":false,
"kafka_topic":"1",
"key":"2",
"max_retry_count":0,
"name":"kafka logger",
"retry_delay":1,
"timeout":3
},
"name":"kafka-logger",
"type":0
}
},
"size":{
"height":96,
"width":212
},
"type":"kafka-logger"
},
"b93d622c-92ef-48b4-b6bb-57e1ce893ee3":{
"id":"b93d622c-92ef-48b4-b6bb-57e1ce893ee3",
"orientation":0,
"ports":{
"port1":{
"id":"port1",
"position":{
"x":110,
"y":0
},
"properties":{
"custom":"property"
},
"type":"input"
},
"port2":{
"id":"port2",
"position":{
"x":110,
"y":96
},
"properties":{
"custom":"property"
},
"type":"output"
}
},
"position":{
"x":988.9074986362261,
"y":478.62041800736495
},
"properties":{
"customData":{
"data":{
"abort":{
"body":"200",
"http_status":300
},
"delay":{
"duration":500
}
},
"name":"fault-injection",
"type":0
}
},
"size":{
"height":96,
"width":219
},
"type":"fault-injection"
}
},
"offset":{
"x":-376.83,
"y":87.98
},
"scale":0.832,
"selected":{
"id":"b93d622c-92ef-48b4-b6bb-57e1ce893ee3",
"type":"node"
}
}
}`
func TestRoute_Get(t *testing.T) {
tests := []testCase{
{
caseDesc: "route: get success",
giveInput: &GetInput{ID: "r1"},
mockInput: "r1",
mockRet: &entity.Route{
BaseInfo: entity.BaseInfo{
ID: "r1",
},
URI: "/test",
},
mockErr: nil,
wantRet: &entity.Route{
BaseInfo: entity.BaseInfo{
ID: "r1",
},
Script: "script",
URI: "/test",
},
wantErr: nil,
called: true,
scriptRet: &entity.Script{ID: "r1", Script: "script"},
scriptErr: nil,
},
{
caseDesc: "route: store get failed",
giveInput: &GetInput{ID: "failed_key"},
mockInput: "failed_key",
mockRet: nil,
mockErr: fmt.Errorf("get failed"),
wantRet: &data.SpecCodeResponse{
StatusCode: http.StatusNotFound,
},
wantErr: fmt.Errorf("get failed"),
called: true,
},
{
caseDesc: "script: store get failed",
giveInput: &GetInput{ID: "failed_key"},
mockInput: "failed_key",
mockRet: nil,
mockErr: fmt.Errorf("get failed"),
wantRet: &data.SpecCodeResponse{
StatusCode: http.StatusNotFound,
},
wantErr: fmt.Errorf("get failed"),
scriptErr: errors.New("get failed"),
called: true,
},
}
for _, tc := range tests {
getCalled := false
mStore := &store.MockInterface{}
mStore.On("Get", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
getCalled = true
assert.Equal(t, tc.mockInput, args.Get(0))
}).Return(tc.mockRet, tc.mockErr)
sStore := &store.MockInterface{}
sStore.On("Get", mock.Anything, mock.Anything).Return(tc.scriptRet, tc.scriptErr)
h := Handler{routeStore: mStore, scriptStore: sStore}
ctx := droplet.NewContext()
ctx.SetInput(tc.giveInput)
ret, err := h.Get(ctx)
assert.True(t, tc.called, getCalled)
assert.Equal(t, tc.wantRet, ret)
assert.Equal(t, tc.wantErr, err)
}
}
func TestRoute_List(t *testing.T) {
mockData := []*entity.Route{
{
BaseInfo: entity.BaseInfo{CreateTime: 1609742634},
Name: "r1",
URI: "/test_r1",
Labels: map[string]string{
"version": "v1",
"build": "16",
},
Upstream: &entity.UpstreamDef{
Nodes: []interface{}{
map[string]interface{}{
"host": "39.97.63.215",
"port": float64(80),
"weight": float64(1),
"priority": float64(10),
},
},
},
},
{
BaseInfo: entity.BaseInfo{CreateTime: 1609742635},
Name: "r2",
URI: "/test_r2",
Labels: map[string]string{
"version": "v1",
"build": "16",
},
},
{
BaseInfo: entity.BaseInfo{CreateTime: 1609742636},
Name: "route_test",
URI: "/test_route_test",
Labels: map[string]string{
"version": "v2",
"build": "17",
},
},
{
BaseInfo: entity.BaseInfo{CreateTime: 1609742636},
Name: "test_route",
URI: "/test_test_route",
Labels: map[string]string{
"version": "v2",
"build": "17",
"extra": "test",
},
},
}
tests := []testCase{
{
caseDesc: "list all route",
giveInput: &ListInput{
Pagination: store.Pagination{
PageSize: 10,
PageNumber: 10,
},
},
mockInput: store.ListInput{
PageSize: 10,
PageNumber: 10,
},
wantRet: &store.ListOutput{
Rows: []interface{}{
mockData[0],
mockData[1],
mockData[2],
mockData[3],
},
TotalSize: 4,
},
scriptRet: &entity.Script{ID: "r1", Script: "script"},
called: true,
},
{
caseDesc: "list routes with name",
giveInput: &ListInput{
Name: "route",
Pagination: store.Pagination{
PageSize: 10,
PageNumber: 10,
},
},
mockInput: store.ListInput{
PageSize: 10,
PageNumber: 10,
},
wantRet: &store.ListOutput{
Rows: []interface{}{
mockData[2],
mockData[3],
},
TotalSize: 2,
},
scriptRet: &entity.Script{ID: "r1", Script: "script"},
called: true,
},
{
caseDesc: "list routes with uri",
giveInput: &ListInput{
URI: "test_r2",
Pagination: store.Pagination{
PageSize: 10,
PageNumber: 10,
},
},
mockInput: store.ListInput{
PageSize: 10,
PageNumber: 10,
},
wantRet: &store.ListOutput{
Rows: []interface{}{
mockData[1],
},
TotalSize: 1,
},
scriptRet: &entity.Script{ID: "r1", Script: "script"},
called: true,
},
{
caseDesc: "list routes with label",
giveInput: &ListInput{
Label: "version:v1",
Pagination: store.Pagination{
PageSize: 10,
PageNumber: 10,
},
},
mockInput: store.ListInput{
PageSize: 10,
PageNumber: 10,
},
wantRet: &store.ListOutput{
Rows: []interface{}{
mockData[0],
mockData[1],
},
TotalSize: 2,
},
scriptRet: &entity.Script{ID: "s1", Script: "script"},
called: true,
},
{
caseDesc: "list routes with label",
giveInput: &ListInput{
Label: "extra",
Pagination: store.Pagination{
PageSize: 10,
PageNumber: 10,
},
},
mockInput: store.ListInput{
PageSize: 10,
PageNumber: 10,
},
wantRet: &store.ListOutput{
Rows: []interface{}{
mockData[3],
},
TotalSize: 1,
},
scriptRet: &entity.Script{ID: "s1", Script: "script"},
called: true,
},
{
caseDesc: "list routes and test format",
giveInput: &ListInput{
Name: "r1",
Pagination: store.Pagination{
PageSize: 10,
PageNumber: 10,
},
},
mockInput: store.ListInput{
PageSize: 10,
PageNumber: 10,
},
wantRet: &store.ListOutput{
Rows: []interface{}{
&entity.Route{
BaseInfo: entity.BaseInfo{CreateTime: 1609742634},
Name: "r1",
URI: "/test_r1",
Labels: map[string]string{
"version": "v1",
"build": "16",
},
Script: "script",
Upstream: &entity.UpstreamDef{
Nodes: []*entity.Node{
{
Host: "39.97.63.215",
Port: 80,
Weight: 1,
Priority: 10,
},
},
},
},
},
TotalSize: 1,
},
called: true,
},
}
for _, tc := range tests {
t.Run(tc.caseDesc, func(t *testing.T) {
getCalled := false
mStore := &store.MockInterface{}
mStore.On("List", mock.Anything).Run(func(args mock.Arguments) {
getCalled = true
input := args.Get(0).(store.ListInput)
mockInput := tc.mockInput.(store.ListInput)
assert.Equal(t, mockInput.PageSize, input.PageSize)
assert.Equal(t, mockInput.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))
}
}
return &store.ListOutput{
Rows: returnData,
TotalSize: len(returnData),
}
}, tc.mockErr)
sStore := &store.MockInterface{}
sStore.On("Get", mock.Anything, mock.Anything).Return(tc.scriptRet, tc.scriptErr)
h := Handler{routeStore: mStore, scriptStore: sStore}
ctx := droplet.NewContext()
ctx.SetInput(tc.giveInput)
ret, err := h.List(ctx)
assert.Equal(t, tc.called, getCalled)
assert.Equal(t, tc.wantRet, ret)
assert.Equal(t, tc.wantErr, err)
})
}
}
func TestRoute_Create(t *testing.T) {
scriptMap := make(map[string]interface{})
err := json.Unmarshal([]byte(DagScript), &scriptMap)
assert.Nil(t, err)
luaCode, err := generateLuaCode(scriptMap)
assert.Nil(t, err)
tests := []testCase{
{
caseDesc: "create route success",
giveInput: &entity.Route{
BaseInfo: entity.BaseInfo{
ID: "s1",
CreateTime: 1609746531,
},
Name: "s1",
Desc: "test_route",
UpstreamID: "u1",
ServiceID: "s1",
Script: "",
Labels: map[string]string{
"version": "v1",
},
},
mockInput: &entity.Route{
BaseInfo: entity.BaseInfo{
ID: "s1",
CreateTime: 1609746531,
},
Name: "s1",
Desc: "test_route",
UpstreamID: "u1",
ServiceID: "s1",
ScriptID: "s1",
Script: "",
Labels: map[string]string{
"version": "v1",
},
},
mockRet: &entity.Route{
BaseInfo: entity.BaseInfo{
ID: "s1",
CreateTime: 1609746531,
},
Name: "s1",
Desc: "test_route",
UpstreamID: "u1",
ServiceID: "s1",
ScriptID: "s1",
Script: "",
Labels: map[string]string{
"version": "v1",
},
},
serviceRet: "service",
serviceErr: nil,
upstreamRet: "upstream",
upstreamErr: nil,
scriptRet: "script",
scriptErr: nil,
wantRet: &entity.Route{
BaseInfo: entity.BaseInfo{
ID: "s1",
CreateTime: 1609746531,
},
Name: "s1",
Desc: "test_route",
UpstreamID: "u1",
ServiceID: "s1",
ScriptID: "s1",
Script: "",
Labels: map[string]string{
"version": "v1",
},
},
wantErr: nil,
called: true,
},
{
caseDesc: "create route failed, service not found",
giveInput: &entity.Route{
BaseInfo: entity.BaseInfo{
ID: "s2",
CreateTime: 1609746531,
},
Name: "s1",
Desc: "test_route",
UpstreamID: "u1",
ServiceID: "not_found",
Script: "",
Labels: map[string]string{
"version": "v1",
},
},
wantRet: &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
wantErr: fmt.Errorf("service id: not_found not found"),
serviceRet: nil,
serviceErr: data.ErrNotFound,
},
{
caseDesc: "create route failed, service store get error",
giveInput: &entity.Route{
BaseInfo: entity.BaseInfo{
ID: "r1",
CreateTime: 1609746531,
},
Name: "r1",
Desc: "test route",
UpstreamID: "r1",
// mock store will return err if service is s3
ServiceID: "error",
Script: "",
Labels: map[string]string{
"version": "v1",
},
},
wantRet: &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
wantErr: errors.New("service error"),
serviceRet: nil,
serviceErr: errors.New("service error"),
called: false,
},
{
caseDesc: "create route failed, upstream not found",
giveInput: &entity.Route{
BaseInfo: entity.BaseInfo{
ID: "s2",
CreateTime: 1609746531,
},
Name: "s1",
Desc: "test_route",
UpstreamID: "not_found",
ServiceID: "s2",
Script: "",
Labels: map[string]string{
"version": "v1",
},
},
wantRet: &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
wantErr: fmt.Errorf("upstream id: not_found not found"),
upstreamErr: data.ErrNotFound,
called: false,
},
{
caseDesc: "create route failed, upstream store get error",
giveInput: &entity.Route{
BaseInfo: entity.BaseInfo{
ID: "s2",
CreateTime: 1609746531,
},
Name: "s1",
Desc: "test_route",
UpstreamID: "error",
ServiceID: "s2",
Script: "",
Labels: map[string]string{
"version": "v1",
},
},
wantRet: &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
wantErr: errors.New("upstream error"),
upstreamErr: errors.New("upstream error"),
called: false,
},
{
caseDesc: "create route failed, script create error",
giveInput: &entity.Route{
BaseInfo: entity.BaseInfo{
ID: "s2",
CreateTime: 1609746531,
},
Name: "s1",
Desc: "test_route",
UpstreamID: "u1",
ServiceID: "s2",
Script: "",
Labels: map[string]string{
"version": "v1",
},
},
wantRet: &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
wantErr: fmt.Errorf("upstream error"),
serviceErr: fmt.Errorf("upstream error"),
called: false,
},
{
caseDesc: "create route success with script",
giveInput: &entity.Route{
BaseInfo: entity.BaseInfo{
ID: "s1",
CreateTime: 1609746531,
},
Name: "s1",
Desc: "test_route",
UpstreamID: "u1",
ServiceID: "s1",
Script: scriptMap,
Labels: map[string]string{
"version": "v1",
},
},
mockInput: &entity.Route{
BaseInfo: entity.BaseInfo{
ID: "s1",
CreateTime: 1609746531,
},
Name: "s1",
Desc: "test_route",
UpstreamID: "u1",
ServiceID: "s1",
Script: luaCode,
ScriptID: "s1",
Labels: map[string]string{
"version": "v1",
},
},
mockRet: &entity.Route{
BaseInfo: entity.BaseInfo{
ID: "s1",
CreateTime: 1609746531,
},
Name: "s1",
Desc: "test_route",
UpstreamID: "u1",
ServiceID: "s1",
Script: luaCode,
ScriptID: "s1",
Labels: map[string]string{
"version": "v1",
},
},
serviceRet: "service",
serviceErr: nil,
upstreamRet: "upstream",
upstreamErr: nil,
scriptRet: "script",
scriptErr: nil,
wantRet: &entity.Route{
BaseInfo: entity.BaseInfo{
ID: "s1",
CreateTime: 1609746531,
},
Name: "s1",
Desc: "test_route",
UpstreamID: "u1",
ServiceID: "s1",
Script: luaCode,
ScriptID: "s1",
Labels: map[string]string{
"version": "v1",
},
},
wantErr: nil,
called: true,
},
}
for _, tc := range tests {
t.Run(tc.caseDesc, func(t *testing.T) {
getCalled := false
mStore := &store.MockInterface{}
mStore.On("Create", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
getCalled = true
route := args.Get(1).(*entity.Route)
assert.Equal(t, tc.mockInput, route)
}).Return(tc.mockRet, tc.mockErr)
mStore.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)
svcStore := &store.MockInterface{}
svcStore.On("Get", mock.Anything, mock.Anything).Return(tc.serviceRet, tc.serviceErr)
uStore := &store.MockInterface{}
uStore.On("Get", mock.Anything, mock.Anything).Return(tc.upstreamRet, tc.upstreamErr)
scriptStore := &store.MockInterface{}
scriptStore.On("Create", mock.Anything, mock.Anything).Return(tc.scriptRet, tc.serviceErr)
h := Handler{routeStore: mStore, svcStore: svcStore, upstreamStore: uStore, scriptStore: scriptStore}
ctx := droplet.NewContext()
ctx.SetInput(tc.giveInput)
ret, err := h.Create(ctx)
assert.Equal(t, tc.called, getCalled)
assert.Equal(t, tc.wantRet, ret)
assert.Equal(t, tc.wantErr, err)
})
}
}
func TestRoute_Update(t *testing.T) {
luaScript := "local _M = {} \n function _M.access(api_ctx) \n ngx.log(ngx.WARN,\"hit access phase\") \n end \nreturn _M"
scriptMap := make(map[string]interface{})
err := json.Unmarshal([]byte(DagScript), &scriptMap)
assert.Nil(t, err)
luaCode, err := generateLuaCode(scriptMap)
assert.Nil(t, err)
tests := []testCase{
{
caseDesc: "update success with script",
giveInput: &UpdateInput{
ID: "r1",
Route: entity.Route{
Name: "r1",
Desc: "updated route",
UpstreamID: "u2",
Script: luaScript,
ServiceID: "s1",
Labels: map[string]string{
"version": "v2",
},
},
},
mockInput: &entity.Route{
BaseInfo: entity.BaseInfo{
ID: "r1",
},
Name: "r1",
Desc: "updated route",
UpstreamID: "u2",
Script: luaScript,
ScriptID: "r1",
ServiceID: "s1",
Labels: map[string]string{
"version": "v2",
},
},
mockRet: &entity.Route{
BaseInfo: entity.BaseInfo{
ID: "r1",
},
Name: "r1",
Desc: "updated route",
UpstreamID: "u2",
Script: luaScript,
ScriptID: "r1",
ServiceID: "s1",
Labels: map[string]string{
"version": "v2",
},
},
wantRet: &entity.Route{
BaseInfo: entity.BaseInfo{
ID: "r1",
},
Name: "r1",
Desc: "updated route",
UpstreamID: "u2",
Script: luaScript,
ScriptID: "r1",
ServiceID: "s1",
Labels: map[string]string{
"version": "v2",
},
},
mockErr: nil,
serviceInput: "s2",
called: true,
},
{
caseDesc: "update success with script map",
giveInput: &UpdateInput{
ID: "r1",
Route: entity.Route{
Name: "r1",
Desc: "updated route",
UpstreamID: "u2",
Script: scriptMap,
ServiceID: "s1",
Labels: map[string]string{
"version": "v2",
},
},
},
mockInput: &entity.Route{
BaseInfo: entity.BaseInfo{
ID: "r1",
},
Name: "r1",
Desc: "updated route",
UpstreamID: "u2",
Script: luaCode,
ScriptID: "r1",
ServiceID: "s1",
Labels: map[string]string{
"version": "v2",
},
},
mockRet: &entity.Route{
BaseInfo: entity.BaseInfo{
ID: "r1",
},
Name: "r1",
Desc: "updated route",
UpstreamID: "u2",
Script: luaCode,
ScriptID: "r1",
ServiceID: "s1",
Labels: map[string]string{
"version": "v2",
},
},
mockErr: nil,
wantRet: &entity.Route{
BaseInfo: entity.BaseInfo{
ID: "r1",
},
Name: "r1",
Desc: "updated route",
UpstreamID: "u2",
Script: luaCode,
ScriptID: "r1",
ServiceID: "s1",
Labels: map[string]string{
"version": "v2",
},
},
serviceInput: "s2",
called: true,
},
{
caseDesc: "update failed, different id",
giveInput: &UpdateInput{
ID: "r1",
Route: entity.Route{
BaseInfo: entity.BaseInfo{
ID: "r2",
},
Name: "test_route",
UpstreamID: "u1",
Desc: "test service",
},
},
wantRet: &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
wantErr: fmt.Errorf("ID on path (r1) doesn't match ID on body (r2)"),
called: false,
},
{
caseDesc: "update failed, service not found",
giveInput: &UpdateInput{
ID: "r1",
Route: entity.Route{
BaseInfo: entity.BaseInfo{
ID: "r1",
},
Name: "test route",
ServiceID: "not_found",
UpstreamID: "u1",
},
},
wantRet: &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
wantErr: fmt.Errorf("service id: not_found not found"),
serviceErr: data.ErrNotFound,
called: false,
},
{
caseDesc: "update failed, upstream not found",
giveInput: &UpdateInput{
ID: "r1",
Route: entity.Route{
Name: "test_route",
UpstreamID: "not_found",
ServiceID: "s1",
Desc: "test route",
},
},
wantRet: &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
wantErr: fmt.Errorf("upstream id: not_found not found"),
serviceRet: "service",
upstreamErr: data.ErrNotFound,
called: false,
},
{
caseDesc: "update failed, route return error",
giveInput: &UpdateInput{
ID: "r1",
Route: entity.Route{
Name: "r1",
Desc: "test route",
},
},
mockInput: &entity.Route{
BaseInfo: entity.BaseInfo{
ID: "r1",
},
Name: "r1",
Desc: "test route",
},
mockErr: fmt.Errorf("route update error"),
wantErr: fmt.Errorf("route update error"),
wantRet: &data.SpecCodeResponse{StatusCode: http.StatusInternalServerError},
serviceRet: "service",
upstreamRet: "upstream",
serviceInput: "s1",
called: true,
},
}
for _, tc := range tests {
t.Run(tc.caseDesc, func(t *testing.T) {
getCalled := false
routeStore := &store.MockInterface{}
routeStore.On("Update", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
getCalled = true
input := args.Get(1).(*entity.Route)
createIfNotExist := args.Get(2).(bool)
assert.Equal(t, tc.mockInput, input)
assert.True(t, createIfNotExist)
}).Return(tc.mockRet, tc.mockErr)
routeStore.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)
serviceStore := &store.MockInterface{}
serviceStore.On("Get", mock.Anything, mock.Anything).Return(tc.serviceRet, tc.serviceErr)
upstreamStore := &store.MockInterface{}
upstreamStore.On("Get", mock.Anything, mock.Anything).Return(tc.upstreamRet, tc.upstreamErr)
scriptStore := &store.MockInterface{}
scriptStore.On("Get", mock.Anything, mock.Anything).Return(tc.scriptRet, tc.scriptErr)
scriptStore.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(luaScript, nil)
h := Handler{svcStore: serviceStore, upstreamStore: upstreamStore, scriptStore: scriptStore,
routeStore: routeStore}
ctx := droplet.NewContext()
ctx.SetInput(tc.giveInput)
ret, err := h.Update(ctx)
assert.Equal(t, tc.called, getCalled)
assert.Equal(t, tc.wantRet, ret)
assert.Equal(t, tc.wantErr, err)
})
}
}
func TestRoute_Patch(t *testing.T) {
existRoute := &entity.Route{
BaseInfo: entity.BaseInfo{
ID: "r1",
CreateTime: 1609340491,
UpdateTime: 1609340491,
},
Name: "exist_service",
UpstreamID: "u1",
Upstream: &entity.UpstreamDef{
Key: "key",
},
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",
},
},
Status: 1,
}
tests := []testCase{
{
caseDesc: "patch success",
giveInput: &PatchInput{
ID: "r1",
SubPath: "",
Body: []byte("{\"status\":0}"),
},
mockInput: &entity.Route{
BaseInfo: entity.BaseInfo{
ID: "r1",
CreateTime: 1609340491,
UpdateTime: 1609340491,
},
Name: "exist_service",
UpstreamID: "u1",
Upstream: &entity.UpstreamDef{
Key: "key",
},
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",
},
},
Status: 0,
},
mockRet: &entity.Route{
BaseInfo: entity.BaseInfo{
ID: "r1",
CreateTime: 1609340491,
UpdateTime: 1609340491,
},
Name: "exist_service",
UpstreamID: "u1",
Upstream: &entity.UpstreamDef{
Key: "key",
},
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",
},
},
Status: 0,
},
wantRet: &entity.Route{
BaseInfo: entity.BaseInfo{
ID: "r1",
CreateTime: 1609340491,
UpdateTime: 1609340491,
},
Name: "exist_service",
UpstreamID: "u1",
Upstream: &entity.UpstreamDef{
Key: "key",
},
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",
},
},
Status: 0,
},
called: true,
},
{
caseDesc: "patch success by path",
giveInput: &PatchInput{
ID: "r1",
SubPath: "/status",
Body: []byte("0"),
},
mockInput: &entity.Route{
BaseInfo: entity.BaseInfo{
ID: "r1",
CreateTime: 1609340491,
UpdateTime: 1609340491,
},
Name: "exist_service",
UpstreamID: "u1",
Upstream: &entity.UpstreamDef{
Key: "key",
},
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",
},
},
Status: 0,
},
mockRet: &entity.Route{
BaseInfo: entity.BaseInfo{
ID: "r1",
CreateTime: 1609340491,
UpdateTime: 1609340491,
},
Name: "exist_service",
UpstreamID: "u1",
Upstream: &entity.UpstreamDef{
Key: "key",
},
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",
},
},
Status: 0,
},
wantRet: &entity.Route{
BaseInfo: entity.BaseInfo{
ID: "r1",
CreateTime: 1609340491,
UpdateTime: 1609340491,
},
Name: "exist_service",
UpstreamID: "u1",
Upstream: &entity.UpstreamDef{
Key: "key",
},
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",
},
},
Status: 0,
},
called: true,
},
{
caseDesc: "patch failed, path error",
giveInput: &PatchInput{
ID: "r1",
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
routeStore := &store.MockInterface{}
routeStore.On("Get", mock.Anything, mock.Anything).Return(existRoute, nil)
routeStore.On("Update", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
getCalled = true
input := args.Get(1).(*entity.Route)
createIfNotExist := args.Get(2).(bool)
assert.Equal(t, tc.mockInput, input)
assert.False(t, createIfNotExist)
}).Return(tc.mockRet, tc.mockErr)
h := Handler{routeStore: routeStore}
ctx := droplet.NewContext()
ctx.SetInput(tc.giveInput)
ret, err := h.Patch(ctx)
assert.Equal(t, tc.called, 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)
}
})
}
}
func TestRoute_Delete(t *testing.T) {
tests := []testCase{
{
caseDesc: "delete success",
giveInput: &BatchDelete{
IDs: "r1",
},
mockInput: []string{"r1"},
called: true,
},
{
caseDesc: "batch delete success",
giveInput: &BatchDelete{
IDs: "r1,r2",
},
mockInput: []string{"r1", "r2"},
called: true,
},
{
caseDesc: "delete failed, route delete error",
giveInput: &BatchDelete{
IDs: "r1",
},
mockInput: []string{"r1"},
mockErr: fmt.Errorf("delete error"),
wantRet: handler.SpecCodeResponse(fmt.Errorf("delete error")),
wantErr: fmt.Errorf("delete error"),
called: true,
},
{
caseDesc: "delete failed, script delete error",
giveInput: &BatchDelete{
IDs: "r1",
},
mockInput: []string{"r1"},
scriptErr: fmt.Errorf("delete error"),
wantRet: nil,
wantErr: nil,
called: true,
},
}
for _, tc := range tests {
t.Run(tc.caseDesc, func(t *testing.T) {
getCalled := false
routeStore := &store.MockInterface{}
routeStore.On("BatchDelete", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
getCalled = true
input := args.Get(1).([]string)
assert.Equal(t, tc.mockInput, input)
}).Return(tc.mockErr)
scriptStore := &store.MockInterface{}
scriptStore.On("BatchDelete", mock.Anything, mock.Anything).Return(tc.serviceRet, tc.scriptErr)
h := Handler{routeStore: routeStore, scriptStore: scriptStore}
ctx := droplet.NewContext()
ctx.SetInput(tc.giveInput)
ret, err := h.BatchDelete(ctx)
assert.True(t, 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)
}
})
}
}
func TestRoute_Exist(t *testing.T) {
mockData := []*entity.Route{
{
BaseInfo: entity.BaseInfo{ID: "001", CreateTime: 1609742634},
Name: "r1",
URI: "/test_r1",
Labels: map[string]string{
"version": "v1",
"build": "16",
},
Upstream: &entity.UpstreamDef{
Nodes: []interface{}{
map[string]interface{}{
"host": "39.97.63.215",
"port": float64(80),
"weight": float64(1),
},
},
},
},
{
BaseInfo: entity.BaseInfo{ID: "002", CreateTime: 1609742635},
Name: "r2",
URI: "/test_r2",
Labels: map[string]string{
"version": "v1",
"build": "16",
},
},
{
BaseInfo: entity.BaseInfo{ID: "003", CreateTime: 1609742636},
Name: "route_test",
URI: "/test_route_test",
Labels: map[string]string{
"version": "v2",
"build": "17",
},
},
}
tests := []testCase{
{
caseDesc: "check route exist, excluded",
giveInput: &ExistCheckInput{
Name: "r1",
Exclude: "001",
},
wantRet: nil,
called: true,
},
{
caseDesc: "check route exist, not excluded",
giveInput: &ExistCheckInput{
Name: "r1",
Exclude: "002",
},
wantRet: &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
wantErr: consts.InvalidParam("Route name is reduplicate"),
called: true,
},
{
caseDesc: "check route exist, not existed",
giveInput: &ExistCheckInput{
Name: "r3",
Exclude: "001",
},
wantRet: nil,
wantErr: nil,
called: true,
},
}
for _, tc := range tests {
t.Run(tc.caseDesc, func(t *testing.T) {
getCalled := false
routeStore := &store.MockInterface{}
routeStore.On("List", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
getCalled = true
}).Return(func(input store.ListInput) *store.ListOutput {
var res []interface{}
for _, c := range mockData {
if input.Predicate(c) {
if input.Format != nil {
res = append(res, input.Format(c))
} else {
res = append(res, c)
}
}
}
return &store.ListOutput{
Rows: res,
TotalSize: len(res),
}
}, nil)
h := Handler{routeStore: routeStore}
ctx := droplet.NewContext()
ctx.SetInput(tc.giveInput)
ret, err := h.Exist(ctx)
assert.True(t, getCalled)
assert.Equal(t, tc.wantRet, ret)
assert.Equal(t, tc.wantErr, err)
})
}
}