| # |
| # 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. |
| # |
| use t::APISIX 'no_plan'; |
| |
| repeat_each(1); |
| no_long_string(); |
| no_root_location(); |
| no_shuffle(); |
| log_level("warn"); |
| |
| add_block_preprocessor(sub { |
| my ($block) = @_; |
| |
| if (!$block->request) { |
| $block->set_value("request", "GET /t"); |
| } |
| }); |
| |
| run_tests; |
| |
| __DATA__ |
| |
| === TEST 1: validate configs - success with valid route |
| --- config |
| location /t { |
| content_by_lua_block { |
| local t = require("lib.test_admin").test |
| local code, body = t('/apisix/admin/configs/validate', |
| ngx.HTTP_POST, |
| [[{ |
| "routes": [ |
| { |
| "id": "r1", |
| "uri": "/test", |
| "upstream": { |
| "nodes": {"127.0.0.1:1980": 1}, |
| "type": "roundrobin" |
| } |
| } |
| ] |
| }]] |
| ) |
| |
| ngx.status = code |
| } |
| } |
| --- error_code: 200 |
| |
| |
| |
| === TEST 2: validate configs - success with multiple resource types |
| --- config |
| location /t { |
| content_by_lua_block { |
| local t = require("lib.test_admin").test |
| local code, body = t('/apisix/admin/configs/validate', |
| ngx.HTTP_POST, |
| [[{ |
| "routes": [ |
| { |
| "id": "r1", |
| "uri": "/test", |
| "upstream": { |
| "nodes": {"127.0.0.1:1980": 1}, |
| "type": "roundrobin" |
| } |
| } |
| ], |
| "services": [ |
| { |
| "id": "svc-1", |
| "upstream": { |
| "nodes": {"127.0.0.1:1980": 1}, |
| "type": "roundrobin" |
| } |
| } |
| ], |
| "upstreams": [ |
| { |
| "id": "ups-1", |
| "nodes": {"127.0.0.1:1980": 1}, |
| "type": "roundrobin" |
| } |
| ] |
| }]] |
| ) |
| |
| ngx.status = code |
| } |
| } |
| --- error_code: 200 |
| |
| |
| |
| === TEST 3: validate configs - empty body |
| --- config |
| location /t { |
| content_by_lua_block { |
| local t = require("lib.test_admin").test |
| local json = require("cjson") |
| local code, body = t('/apisix/admin/configs/validate', |
| ngx.HTTP_POST, |
| "" |
| ) |
| |
| ngx.status = code |
| local data = json.decode(body) |
| assert(data.error_msg == "invalid request body: empty request body", |
| "expected empty body error, got: " .. data.error_msg) |
| ngx.say("passed") |
| } |
| } |
| --- error_code: 400 |
| --- response_body |
| passed |
| |
| |
| |
| === TEST 4: validate configs - invalid JSON body |
| --- config |
| location /t { |
| content_by_lua_block { |
| local t = require("lib.test_admin").test |
| local json = require("cjson") |
| local code, body = t('/apisix/admin/configs/validate', |
| ngx.HTTP_POST, |
| "not json" |
| ) |
| |
| ngx.status = code |
| local data = json.decode(body) |
| assert(data.error_msg and data.error_msg:find("invalid request body", 1, true), |
| "expected 'invalid request body' error, got: " .. tostring(data.error_msg)) |
| ngx.say("passed") |
| } |
| } |
| --- error_code: 400 |
| --- response_body |
| passed |
| |
| |
| |
| === TEST 5: validate configs - invalid route (uri must be string) |
| --- config |
| location /t { |
| content_by_lua_block { |
| local t = require("lib.test_admin").test |
| local json = require("cjson") |
| local code, body = t('/apisix/admin/configs/validate', |
| ngx.HTTP_POST, |
| [[{ |
| "routes": [ |
| { |
| "id": "r1", |
| "uri": 123 |
| } |
| ] |
| }]] |
| ) |
| |
| ngx.status = code |
| local data = json.decode(body) |
| assert(data.error_msg == "Configuration validation failed", |
| "expected validation failed msg, got: " .. tostring(data.error_msg)) |
| assert(data.errors and #data.errors == 1, |
| "expected 1 error, got: " .. tostring(data.errors and #data.errors)) |
| local err = data.errors[1] |
| assert(err.resource_type == "routes", "expected resource_type=routes, got: " .. tostring(err.resource_type)) |
| assert(err.index == 0, "expected index=0, got: " .. tostring(err.index)) |
| assert(err.resource_id == "r1", "expected resource_id=r1, got: " .. tostring(err.resource_id)) |
| assert(err.error and type(err.error) == "string" and #err.error > 0, |
| "expected non-empty error string, got: " .. tostring(err.error)) |
| ngx.say("passed") |
| } |
| } |
| --- error_code: 400 |
| --- response_body |
| passed |
| |
| |
| |
| === TEST 6: validate configs - duplicate route IDs |
| --- config |
| location /t { |
| content_by_lua_block { |
| local t = require("lib.test_admin").test |
| local json = require("cjson") |
| local code, body = t('/apisix/admin/configs/validate', |
| ngx.HTTP_POST, |
| [[{ |
| "routes": [ |
| { |
| "id": "r1", |
| "uri": "/test1", |
| "upstream": { |
| "nodes": {"127.0.0.1:1980": 1}, |
| "type": "roundrobin" |
| } |
| }, |
| { |
| "id": "r1", |
| "uri": "/test2", |
| "upstream": { |
| "nodes": {"127.0.0.1:1980": 1}, |
| "type": "roundrobin" |
| } |
| } |
| ] |
| }]] |
| ) |
| |
| ngx.status = code |
| local data = json.decode(body) |
| assert(data.error_msg == "Configuration validation failed", |
| "expected validation failed, got: " .. tostring(data.error_msg)) |
| -- find the duplicate error |
| local found = false |
| for _, err in ipairs(data.errors) do |
| if err.error and err.error:find("found duplicate id r1 in routes", 1, true) then |
| found = true |
| assert(err.resource_type == "routes", "expected resource_type=routes") |
| assert(err.resource_id == "r1", "expected resource_id=r1, got: " .. tostring(err.resource_id)) |
| assert(err.index == 1, "expected index=1 for the duplicate, got: " .. tostring(err.index)) |
| break |
| end |
| end |
| assert(found, "expected 'found duplicate id r1 in routes' error in: " .. body) |
| ngx.say("passed") |
| } |
| } |
| --- error_code: 400 |
| --- response_body |
| passed |
| |
| |
| |
| === TEST 7: validate configs - plugin check_schema advanced validation (cors) |
| --- config |
| location /t { |
| content_by_lua_block { |
| local t = require("lib.test_admin").test |
| local json = require("cjson") |
| local code, body = t('/apisix/admin/configs/validate', |
| ngx.HTTP_POST, |
| [[{ |
| "routes": [ |
| { |
| "id": "r1", |
| "uri": "/test", |
| "upstream": { |
| "nodes": {"127.0.0.1:1980": 1}, |
| "type": "roundrobin" |
| }, |
| "plugins": { |
| "cors": { |
| "allow_credential": true, |
| "allow_origins": "*" |
| } |
| } |
| } |
| ] |
| }]] |
| ) |
| |
| ngx.status = code |
| local data = json.decode(body) |
| assert(data.error_msg == "Configuration validation failed", |
| "expected validation failed, got: " .. tostring(data.error_msg)) |
| assert(data.errors and #data.errors >= 1, "expected at least 1 error") |
| local err = data.errors[1] |
| assert(err.resource_type == "routes", "expected resource_type=routes") |
| assert(err.resource_id == "r1", "expected resource_id=r1, got: " .. tostring(err.resource_id)) |
| -- cors check_schema rejects allow_credential=true with allow_origins="*" |
| assert(err.error and err.error:find("allow_credential", 1, true), |
| "expected cors allow_credential error, got: " .. tostring(err.error)) |
| ngx.say("passed") |
| } |
| } |
| --- error_code: 400 |
| --- response_body |
| passed |
| |
| |
| |
| === TEST 8: validate configs - collects all errors |
| --- config |
| location /t { |
| content_by_lua_block { |
| local t = require("lib.test_admin").test |
| local json = require("cjson") |
| local code, body = t('/apisix/admin/configs/validate', |
| ngx.HTTP_POST, |
| [[{ |
| "routes": [ |
| { |
| "id": "r1", |
| "uri": 123 |
| }, |
| { |
| "id": "r2", |
| "uri": 456 |
| } |
| ] |
| }]] |
| ) |
| |
| ngx.status = code |
| local data = json.decode(body) |
| assert(data.errors and #data.errors == 2, |
| "expected 2 errors, got: " .. tostring(data.errors and #data.errors)) |
| -- verify each error has correct index |
| local indices = {} |
| for _, err in ipairs(data.errors) do |
| indices[err.index] = true |
| assert(err.resource_type == "routes", "expected resource_type=routes") |
| assert(type(err.resource_id) == "string", "expected resource_id to be string") |
| assert(err.error and type(err.error) == "string" and #err.error > 0, |
| "expected non-empty error string, got: " .. tostring(err.error)) |
| end |
| assert(indices[0] and indices[1], "expected errors at index 0 and 1") |
| ngx.say("passed") |
| } |
| } |
| --- error_code: 400 |
| --- response_body |
| passed |
| |
| |
| |
| === TEST 9: validate configs - success with empty config (no resources) |
| --- config |
| location /t { |
| content_by_lua_block { |
| local t = require("lib.test_admin").test |
| local code, body = t('/apisix/admin/configs/validate', |
| ngx.HTTP_POST, |
| [[{}]] |
| ) |
| |
| ngx.status = code |
| } |
| } |
| --- error_code: 200 |
| |
| |
| |
| === TEST 10: validate configs - does not persist changes |
| --- config |
| location /t { |
| content_by_lua_block { |
| local t = require("lib.test_admin").test |
| |
| -- first validate a config |
| local code = t('/apisix/admin/configs/validate', |
| ngx.HTTP_POST, |
| [[{ |
| "routes": [ |
| { |
| "id": "validate-test-r1", |
| "uri": "/validate-test", |
| "upstream": { |
| "nodes": {"127.0.0.1:1980": 1}, |
| "type": "roundrobin" |
| } |
| } |
| ] |
| }]] |
| ) |
| assert(code == 200, "validate should succeed") |
| |
| -- then try to get the route - it should not exist |
| local code, body = t('/apisix/admin/routes/validate-test-r1', ngx.HTTP_GET) |
| ngx.status = code |
| } |
| } |
| --- error_code: 404 |
| |
| |
| |
| === TEST 11: validate configs - invalid plugin configuration (limit-count negative count) |
| --- config |
| location /t { |
| content_by_lua_block { |
| local t = require("lib.test_admin").test |
| local json = require("cjson") |
| local code, body = t('/apisix/admin/configs/validate', |
| ngx.HTTP_POST, |
| [[{ |
| "routes": [ |
| { |
| "id": "r1", |
| "uri": "/test", |
| "upstream": { |
| "nodes": {"127.0.0.1:1980": 1}, |
| "type": "roundrobin" |
| }, |
| "plugins": { |
| "limit-count": { |
| "count": -1, |
| "time_window": 60, |
| "rejected_code": 503, |
| "key": "remote_addr" |
| } |
| } |
| } |
| ] |
| }]] |
| ) |
| |
| ngx.status = code |
| local data = json.decode(body) |
| assert(data.error_msg == "Configuration validation failed", |
| "expected validation failed, got: " .. tostring(data.error_msg)) |
| assert(data.errors and #data.errors >= 1, "expected at least 1 error") |
| local err = data.errors[1] |
| assert(err.resource_type == "routes", "expected resource_type=routes") |
| assert(err.resource_id == "r1", "expected resource_id=r1, got: " .. tostring(err.resource_id)) |
| assert(err.error and err.error:find("limit%-count", 1, false), |
| "expected limit-count in error, got: " .. tostring(err.error)) |
| ngx.say("passed") |
| } |
| } |
| --- error_code: 400 |
| --- response_body |
| passed |
| |
| |
| |
| === TEST 12: validate configs - invalid upstream configuration (chash missing key) |
| --- config |
| location /t { |
| content_by_lua_block { |
| local t = require("lib.test_admin").test |
| local json = require("cjson") |
| local code, body = t('/apisix/admin/configs/validate', |
| ngx.HTTP_POST, |
| [[{ |
| "upstreams": [ |
| { |
| "id": "ups-1", |
| "type": "chash", |
| "hash_on": "vars", |
| "nodes": {"127.0.0.1:1980": 1} |
| } |
| ] |
| }]] |
| ) |
| |
| ngx.status = code |
| local data = json.decode(body) |
| assert(data.error_msg == "Configuration validation failed", |
| "expected validation failed, got: " .. tostring(data.error_msg)) |
| assert(data.errors and #data.errors >= 1, "expected at least 1 error") |
| local err = data.errors[1] |
| assert(err.resource_type == "upstreams", "expected resource_type=upstreams, got: " .. tostring(err.resource_type)) |
| assert(err.resource_id == "ups-1", "expected resource_id=ups-1, got: " .. tostring(err.resource_id)) |
| assert(err.index == 0, "expected index=0, got: " .. tostring(err.index)) |
| assert(err.error and err.error:find("missing key", 1, true), |
| "expected 'missing key' in error, got: " .. tostring(err.error)) |
| ngx.say("passed") |
| } |
| } |
| --- error_code: 400 |
| --- response_body |
| passed |
| |
| |
| |
| === TEST 13: validate configs - consumer with valid plugin |
| --- config |
| location /t { |
| content_by_lua_block { |
| local t = require("lib.test_admin").test |
| local code, body = t('/apisix/admin/configs/validate', |
| ngx.HTTP_POST, |
| [[{ |
| "consumers": [ |
| { |
| "username": "jack", |
| "plugins": { |
| "key-auth": { |
| "key": "auth-one" |
| } |
| } |
| } |
| ] |
| }]] |
| ) |
| |
| ngx.status = code |
| } |
| } |
| --- error_code: 200 |
| |
| |
| |
| === TEST 14: validate configs - routes without id (no crash on nil identifier) |
| --- config |
| location /t { |
| content_by_lua_block { |
| local t = require("lib.test_admin").test |
| local json = require("cjson") |
| local code, _, body = t('/apisix/admin/configs/validate', |
| ngx.HTTP_POST, |
| [[{ |
| "routes": [ |
| { |
| "uri": "/foo", |
| "upstream": { |
| "nodes": {"127.0.0.1:1980": 1}, |
| "type": "roundrobin" |
| } |
| }, |
| { |
| "uri": "/bar", |
| "upstream": { |
| "nodes": {"127.0.0.1:1980": 1}, |
| "type": "roundrobin" |
| } |
| } |
| ], |
| "services": [ |
| { |
| "upstream": { |
| "nodes": {"127.0.0.1:1980": 1}, |
| "type": "roundrobin" |
| } |
| } |
| ] |
| }]] |
| ) |
| |
| assert(code == 200, "expected 200, got: " .. tostring(code)) |
| ngx.say("passed") |
| } |
| } |
| --- response_body |
| passed |
| |
| |
| |
| === TEST 15: validate configs - duplicate consumer usernames detected |
| --- config |
| location /t { |
| content_by_lua_block { |
| local t = require("lib.test_admin").test |
| local json = require("cjson") |
| local code, body = t('/apisix/admin/configs/validate', |
| ngx.HTTP_POST, |
| [[{ |
| "consumers": [ |
| { |
| "username": "jack", |
| "plugins": { |
| "key-auth": {"key": "auth-one"} |
| } |
| }, |
| { |
| "username": "jack", |
| "plugins": { |
| "key-auth": {"key": "auth-two"} |
| } |
| } |
| ] |
| }]] |
| ) |
| |
| ngx.status = code |
| local data = json.decode(body) |
| assert(data.error_msg == "Configuration validation failed", |
| "expected validation failed, got: " .. tostring(data.error_msg)) |
| local found = false |
| for _, err in ipairs(data.errors) do |
| if err.error and err.error:find("found duplicate username jack in consumers", 1, true) then |
| found = true |
| assert(err.resource_type == "consumers", "expected resource_type=consumers") |
| assert(err.resource_id == "jack", "expected resource_id=jack, got: " .. tostring(err.resource_id)) |
| break |
| end |
| end |
| assert(found, "expected 'found duplicate username jack in consumers' error in: " .. body) |
| ngx.say("passed") |
| } |
| } |
| --- error_code: 400 |
| --- response_body |
| passed |
| |
| |
| |
| === TEST 16: validate configs - mixed valid and invalid resources across types |
| --- config |
| location /t { |
| content_by_lua_block { |
| local t = require("lib.test_admin").test |
| local json = require("cjson") |
| local code, body = t('/apisix/admin/configs/validate', |
| ngx.HTTP_POST, |
| [[{ |
| "routes": [ |
| { |
| "id": "r1", |
| "uri": "/ok", |
| "upstream": { |
| "nodes": {"127.0.0.1:1980": 1}, |
| "type": "roundrobin" |
| } |
| } |
| ], |
| "upstreams": [ |
| { |
| "id": "ups-bad", |
| "type": "chash", |
| "hash_on": "vars", |
| "nodes": {"127.0.0.1:1980": 1} |
| } |
| ] |
| }]] |
| ) |
| |
| ngx.status = code |
| local data = json.decode(body) |
| assert(data.error_msg == "Configuration validation failed", |
| "expected validation failed, got: " .. tostring(data.error_msg)) |
| -- should have errors from upstreams but not routes |
| local has_upstream_err = false |
| local has_route_err = false |
| for _, err in ipairs(data.errors) do |
| if err.resource_type == "upstreams" then |
| has_upstream_err = true |
| assert(err.resource_id == "ups-bad", |
| "expected resource_id=ups-bad, got: " .. tostring(err.resource_id)) |
| elseif err.resource_type == "routes" then |
| has_route_err = true |
| end |
| end |
| assert(has_upstream_err, "expected upstream validation error, errors: " .. body) |
| assert(not has_route_err, "route should be valid, no route errors expected") |
| ngx.say("passed") |
| } |
| } |
| --- error_code: 400 |
| --- response_body |
| passed |