| /* |
| * 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 store |
| |
| import ( |
| "encoding/json" |
| "fmt" |
| "testing" |
| |
| "github.com/stretchr/testify/assert" |
| |
| "github.com/apisix/manager-api/internal/core/entity" |
| ) |
| |
| type TestObj struct { |
| Name string `json:"name"` |
| Email string `json:"email"` |
| Age int `json:"age"` |
| } |
| |
| func TestJsonSchemaValidator_Validate(t *testing.T) { |
| tests := []struct { |
| givePath string |
| giveObj interface{} |
| wantNewErr error |
| wantValidateErr []error |
| }{ |
| { |
| givePath: "./test_case.json", |
| giveObj: TestObj{ |
| Name: "lessName", |
| Email: "too long name greater than 10", |
| Age: 12, |
| }, |
| wantValidateErr: []error{ |
| fmt.Errorf("name: String length must be greater than or equal to 10\nemail: String length must be less than or equal to 10"), |
| fmt.Errorf("email: String length must be less than or equal to 10\nname: String length must be greater than or equal to 10"), |
| }, |
| }, |
| } |
| |
| for _, tc := range tests { |
| v, err := NewJsonSchemaValidator(tc.givePath) |
| if err != nil { |
| assert.Equal(t, tc.wantNewErr, err) |
| continue |
| } |
| err = v.Validate(tc.giveObj) |
| ret := false |
| for _, wantErr := range tc.wantValidateErr { |
| if wantErr.Error() == err.Error() { |
| ret = true |
| } |
| } |
| assert.True(t, ret) |
| } |
| } |
| |
| func TestAPISIXJsonSchemaValidator_Validate(t *testing.T) { |
| validator, err := NewAPISIXJsonSchemaValidator("main.consumer") |
| assert.Nil(t, err) |
| |
| consumer := &entity.Consumer{} |
| reqBody := `{ |
| "username": "jack", |
| "plugins": { |
| "limit-count": { |
| "count": 2, |
| "time_window": 60, |
| "rejected_code": 503, |
| "key": "remote_addr", |
| "policy": "local" |
| } |
| }, |
| "desc": "test description" |
| }` |
| err = json.Unmarshal([]byte(reqBody), consumer) |
| assert.Nil(t, err) |
| err = validator.Validate(consumer) |
| assert.Nil(t, err) |
| |
| //check nil obj |
| err = validator.Validate(nil) |
| assert.NotNil(t, err) |
| assert.EqualError(t, err, "schema validate failed: (root): Invalid type. Expected: object, given: null") |
| |
| //plugin schema fail |
| consumer3 := &entity.Consumer{} |
| reqBody = `{ |
| "username": "jack", |
| "plugins": { |
| "limit-count": { |
| "time_window": 60, |
| "rejected_code": 503, |
| "key": "remote_addr", |
| "policy": "local" |
| } |
| }, |
| "desc": "test description" |
| }` |
| err = json.Unmarshal([]byte(reqBody), consumer3) |
| assert.Nil(t, err) |
| err = validator.Validate(consumer3) |
| assert.NotNil(t, err) |
| assert.EqualError(t, err, "schema validate failed: (root): count is required") |
| } |
| |
| func TestAPISIXJsonSchemaValidator_checkUpstream(t *testing.T) { |
| validator, err := NewAPISIXJsonSchemaValidator("main.route") |
| assert.Nil(t, err) |
| |
| // type:chash, hash_on: consumer, missing key, ok |
| route := &entity.Route{} |
| reqBody := `{ |
| "id": "1", |
| "name": "route1", |
| "methods": ["GET"], |
| "upstream": { |
| "nodes": { |
| "127.0.0.1:8080": 1 |
| }, |
| "type": "chash", |
| "hash_on":"consumer" |
| }, |
| "desc": "new route", |
| "uri": "/index.html" |
| }` |
| err = json.Unmarshal([]byte(reqBody), route) |
| assert.Nil(t, err) |
| err = validator.Validate(route) |
| assert.Nil(t, err) |
| |
| // type:chash, hash_on: default(vars), missing key |
| route2 := &entity.Route{} |
| reqBody = `{ |
| "id": "1", |
| "name": "route1", |
| "methods": ["GET"], |
| "upstream": { |
| "nodes": { |
| "127.0.0.1:8080": 1 |
| }, |
| "type": "chash" |
| }, |
| "desc": "new route", |
| "uri": "/index.html" |
| }` |
| err = json.Unmarshal([]byte(reqBody), route2) |
| assert.Nil(t, err) |
| err = validator.Validate(route2) |
| assert.NotNil(t, err) |
| assert.EqualError(t, err, "missing key") |
| |
| //type:chash, hash_on: header, missing key |
| route3 := &entity.Route{} |
| reqBody = `{ |
| "id": "1", |
| "name": "route1", |
| "methods": ["GET"], |
| "upstream": { |
| "nodes": { |
| "127.0.0.1:8080": 1 |
| }, |
| "type": "chash", |
| "hash_on":"header" |
| }, |
| "desc": "new route", |
| "uri": "/index.html" |
| }` |
| err = json.Unmarshal([]byte(reqBody), route3) |
| assert.Nil(t, err) |
| err = validator.Validate(route3) |
| assert.NotNil(t, err) |
| assert.EqualError(t, err, "missing key") |
| |
| //type:chash, hash_on: cookie, missing key |
| route4 := &entity.Route{} |
| reqBody = `{ |
| "id": "1", |
| "name": "route1", |
| "methods": ["GET"], |
| "upstream": { |
| "nodes": { |
| "127.0.0.1:8080": 1 |
| }, |
| "type": "chash", |
| "hash_on":"cookie" |
| }, |
| "desc": "new route", |
| "uri": "/index.html" |
| }` |
| err = json.Unmarshal([]byte(reqBody), route4) |
| assert.Nil(t, err) |
| err = validator.Validate(route4) |
| assert.NotNil(t, err) |
| assert.EqualError(t, err, "missing key") |
| |
| //type:chash, hash_on: vars, wrong key |
| route5 := &entity.Route{} |
| reqBody = `{ |
| "id": "1", |
| "name": "route1", |
| "methods": ["GET"], |
| "upstream": { |
| "nodes": { |
| "127.0.0.1:8080": 1 |
| }, |
| "type": "chash", |
| "hash_on":"vars", |
| "key": "not_support" |
| }, |
| "desc": "new route", |
| "uri": "/index.html" |
| }` |
| err = json.Unmarshal([]byte(reqBody), route5) |
| assert.Nil(t, err) |
| err = validator.Validate(route5) |
| assert.NotNil(t, err) |
| assert.EqualError(t, err, "schema validate failed: (root): Does not match pattern '^((uri|server_name|server_addr|request_uri|remote_port|remote_addr|query_string|host|hostname)|arg_[0-9a-zA-z_-]+)$'") |
| } |
| |
| func TestAPISIXJsonSchemaValidator_Plugin(t *testing.T) { |
| validator, err := NewAPISIXJsonSchemaValidator("main.route") |
| assert.Nil(t, err) |
| |
| // validate plugin's schema which has no `properties` or empty `properties` |
| route := &entity.Route{} |
| reqBody := `{ |
| "id": "1", |
| "name": "route1", |
| "uri": "/hello", |
| "plugins": { |
| "prometheus": { |
| "disable": false |
| }, |
| "key-auth": { |
| "disable": true |
| } |
| } |
| }` |
| err = json.Unmarshal([]byte(reqBody), route) |
| assert.Nil(t, err) |
| err = validator.Validate(route) |
| assert.Nil(t, err) |
| |
| // validate plugin's schema which use `oneOf` |
| reqBody = `{ |
| "id": "1", |
| "uri": "/hello", |
| "plugins": { |
| "ip-restriction": { |
| "blacklist": [ |
| "127.0.0.0/24" |
| ], |
| "disable": true |
| } |
| } |
| }` |
| err = json.Unmarshal([]byte(reqBody), route) |
| assert.Nil(t, err) |
| err = validator.Validate(route) |
| assert.Nil(t, err) |
| |
| // validate plugin's schema with invalid type for `disable` |
| reqBody = `{ |
| "id": "1", |
| "uri": "/hello", |
| "plugins": { |
| "ip-restriction": { |
| "blacklist": [ |
| "127.0.0.0/24" |
| ], |
| "_meta": { |
| "disable": 1 |
| } |
| } |
| } |
| }` |
| err = json.Unmarshal([]byte(reqBody), route) |
| assert.Nil(t, err) |
| err = validator.Validate(route) |
| assert.Equal(t, fmt.Errorf("schema validate failed: _meta.disable: Invalid type. Expected: boolean, given: integer"), err) |
| } |
| |
| func TestAPISIXJsonSchemaValidator_Route_checkRemoteAddr(t *testing.T) { |
| tests := []struct { |
| caseDesc string |
| giveContent string |
| wantNewErr error |
| wantValidateErr error |
| }{ |
| { |
| caseDesc: "correct remote_addr", |
| giveContent: `{ |
| "id": "1", |
| "name": "route1", |
| "uri": "/*", |
| "upstream": { |
| "nodes": [{ |
| "host": "127.0.0.1", |
| "port": 8080, |
| "weight": 1 |
| }], |
| "type": "roundrobin" |
| }, |
| "remote_addr": "127.0.0.1" |
| }`, |
| }, |
| { |
| caseDesc: "correct remote_addr (CIDR)", |
| giveContent: `{ |
| "id": "1", |
| "name": "route1", |
| "uri": "/*", |
| "upstream": { |
| "nodes": [{ |
| "host": "127.0.0.1", |
| "port": 8080, |
| "weight": 1 |
| }], |
| "type": "roundrobin" |
| }, |
| "remote_addr": "192.168.1.0/24" |
| }`, |
| }, |
| { |
| caseDesc: "invalid remote_addr", |
| giveContent: `{ |
| "id": "1", |
| "name": "route1", |
| "uri": "/*", |
| "upstream": { |
| "nodes": [{ |
| "host": "127.0.0.1", |
| "port": 8080, |
| "weight": 1 |
| }], |
| "type": "roundrobin" |
| }, |
| "remote_addr": "127.0.0." |
| }`, |
| wantValidateErr: fmt.Errorf("schema validate failed: remote_addr: Must validate at least one schema (anyOf)\nremote_addr: Does not match format 'ipv4'"), |
| }, |
| { |
| caseDesc: "correct remote_addrs", |
| giveContent: `{ |
| "id": "1", |
| "name": "route1", |
| "uri": "/*", |
| "upstream": { |
| "nodes": [{ |
| "host": "127.0.0.1", |
| "port": 8080, |
| "weight": 1 |
| }], |
| "type": "roundrobin" |
| }, |
| "remote_addrs": ["127.0.0.1", "192.0.0.0/8", "::1", "fe80::1/64"] |
| }`, |
| }, |
| { |
| caseDesc: "invalid remote_addrs", |
| giveContent: `{ |
| "id": "1", |
| "name": "route1", |
| "uri": "/*", |
| "upstream": { |
| "nodes": [{ |
| "host": "127.0.0.1", |
| "port": 8080, |
| "weight": 1 |
| }], |
| "type": "roundrobin" |
| }, |
| "remote_addrs": ["127.0.0.", "192.0.0.0/128", "::1"] |
| }`, |
| wantValidateErr: fmt.Errorf("schema validate failed: remote_addrs.0: Must validate at least one schema (anyOf)\nremote_addrs.0: Does not match format 'ipv4'\nremote_addrs.1: Must validate at least one schema (anyOf)\nremote_addrs.1: Does not match format 'ipv4'"), |
| }, |
| { |
| caseDesc: "invalid remote_addrs (an empty string item)", |
| giveContent: `{ |
| "id": "1", |
| "name": "route1", |
| "uri": "/*", |
| "upstream": { |
| "nodes": [{ |
| "host": "127.0.0.1", |
| "port": 8080, |
| "weight": 1 |
| }], |
| "type": "roundrobin" |
| }, |
| "remote_addrs": [""] |
| }`, |
| wantValidateErr: fmt.Errorf("schema validate failed: remote_addrs.0: Must validate at least one schema (anyOf)\nremote_addrs.0: Does not match format 'ipv4'"), |
| }, |
| } |
| |
| // todo: add a test case for "remote_addr": "" |
| |
| for _, tc := range tests { |
| validator, err := NewAPISIXJsonSchemaValidator("main.route") |
| if err != nil { |
| assert.Equal(t, tc.wantNewErr, err, tc.caseDesc) |
| continue |
| } |
| route := &entity.Route{} |
| err = json.Unmarshal([]byte(tc.giveContent), route) |
| assert.Nil(t, err, tc.caseDesc) |
| |
| err = validator.Validate(route) |
| if tc.wantValidateErr == nil { |
| assert.Equal(t, nil, err, tc.caseDesc) |
| continue |
| } |
| |
| assert.Equal(t, tc.wantValidateErr, err, tc.caseDesc) |
| } |
| } |
| |
| func TestAPISIXSchemaValidator_SystemConfig(t *testing.T) { |
| tests := []struct { |
| name string |
| givePath string |
| giveObj interface{} |
| wantNewErr bool |
| wantValidateErr bool |
| wantErrMessage string |
| }{ |
| { |
| name: "new json schema validator failed", |
| givePath: "main.xxx", |
| wantNewErr: true, |
| wantErrMessage: "schema validate failed: schema not found, path: main.xxx", |
| }, |
| { |
| name: "invalid configName (configName is empty)", |
| givePath: "main.system_config", |
| giveObj: &entity.SystemConfig{ |
| Payload: map[string]interface{}{"a": 1}, |
| }, |
| wantValidateErr: true, |
| wantErrMessage: "schema validate failed: config_name: String length must be greater than or equal to 1\nconfig_name: Does not match pattern '^[a-zA-Z0-9_]+$'", |
| }, |
| { |
| name: "invalid configName (configName do not match regex)", |
| givePath: "main.system_config", |
| giveObj: &entity.SystemConfig{ |
| ConfigName: "1@2", |
| Payload: map[string]interface{}{"a": 1}, |
| }, |
| wantValidateErr: true, |
| wantErrMessage: "schema validate failed: config_name: Does not match pattern '^[a-zA-Z0-9_]+$'", |
| }, |
| { |
| name: "invalid payload", |
| givePath: "main.system_config", |
| giveObj: &entity.SystemConfig{ |
| ConfigName: "cc", |
| }, |
| wantValidateErr: true, |
| wantErrMessage: "schema validate failed: (root): payload is required", |
| }, |
| { |
| name: "validate should succeed", |
| givePath: "main.system_config", |
| giveObj: &entity.SystemConfig{ |
| ConfigName: "aaa", |
| Payload: map[string]interface{}{"a": 1}, |
| }, |
| }, |
| } |
| |
| for _, tc := range tests { |
| validator, err := NewAPISIXSchemaValidator(tc.givePath) |
| if tc.wantNewErr { |
| assert.Error(t, err) |
| assert.Equal(t, tc.wantErrMessage, err.Error()) |
| continue |
| } |
| |
| assert.NoError(t, err) |
| assert.NotNil(t, validator) |
| |
| req, err := json.Marshal(tc.giveObj) |
| assert.NoError(t, err) |
| err = validator.Validate(req) |
| if tc.wantValidateErr { |
| assert.Error(t, err) |
| assert.Equal(t, tc.wantErrMessage, err.Error()) |
| continue |
| } |
| assert.NoError(t, err) |
| } |
| } |
| |
| func TestAPISIXSchemaValidator_Validate(t *testing.T) { |
| validator, err := NewAPISIXSchemaValidator("main.consumer") |
| assert.Nil(t, err) |
| |
| // normal config, should pass |
| reqBody := `{ |
| "username": "jack", |
| "plugins": { |
| "limit-count": { |
| "count": 2, |
| "time_window": 60, |
| "rejected_code": 503, |
| "key": "remote_addr" |
| } |
| }, |
| "desc": "test description" |
| }` |
| err = validator.Validate([]byte(reqBody)) |
| assert.Nil(t, err) |
| } |