From d10408cd70b817cbcee0ef7e4953a8b5a8444a11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Nowak?= Date: Thu, 29 Aug 2024 16:56:59 +0200 Subject: [PATCH] fix(*): reject config if both deprecated and new field defined and their values mismatch (#13565) * fix(*): reject config if both deprecated and new field defined and their values mismatch This commit adds a validation to deprecated fields that checks if in case both new field and old field were defined - their values must match. Note that if one of the fields is null then the validation passes even if the other is not null. KAG-5262 * fixup! fix(*): reject config if both deprecated and new field defined and their values mismatch PR fixes * fixup! fix(*): reject config if both deprecated and new field defined and their values mismatch PR fixes 2 --- ...t-config-on-deprecated-fields-mismatch.yml | 5 + kong/db/schema/init.lua | 77 +++-- kong/db/schema/metaschema.lua | 9 + kong/plugins/acme/schema.lua | 4 + kong/plugins/rate-limiting/schema.lua | 9 + kong/plugins/response-ratelimiting/schema.lua | 9 + .../01-db/01-schema/01-schema_spec.lua | 320 ++++++++++++++++-- 7 files changed, 386 insertions(+), 47 deletions(-) create mode 100644 changelog/unreleased/kong/reject-config-on-deprecated-fields-mismatch.yml diff --git a/changelog/unreleased/kong/reject-config-on-deprecated-fields-mismatch.yml b/changelog/unreleased/kong/reject-config-on-deprecated-fields-mismatch.yml new file mode 100644 index 000000000000..c402a30f95e7 --- /dev/null +++ b/changelog/unreleased/kong/reject-config-on-deprecated-fields-mismatch.yml @@ -0,0 +1,5 @@ +message: | + Changed the behaviour of shorthand fields that are used to describe deprecated fields. If + both fields are sent in the request and their values mismatch - the request will be rejected. +type: bugfix +scope: Core diff --git a/kong/db/schema/init.lua b/kong/db/schema/init.lua index 93fc6af258cf..1d213a3a4ffe 100644 --- a/kong/db/schema/init.lua +++ b/kong/db/schema/init.lua @@ -17,6 +17,7 @@ local table_merge = require "kong.tools.table".table_merge local null_aware_table_merge = require "kong.tools.table".null_aware_table_merge local table_path = require "kong.tools.table".table_path local is_array = require "kong.tools.table".is_array +local join_string = require "kong.tools.string".join local setmetatable = setmetatable @@ -1704,6 +1705,35 @@ local function collect_field_reference(refs, key, reference) end +local function validate_deprecation_exclusiveness(data, shorthand_value, shorthand_name, shorthand_definition) + if shorthand_value == nil or + shorthand_value == ngx.null or + shorthand_definition.deprecation == nil or + shorthand_definition.deprecation.replaced_with == nil then + return true + end + + for _, replaced_with_element in ipairs(shorthand_definition.deprecation.replaced_with) do + local new_field_value = replaced_with_element.reverse_mapping_function and replaced_with_element.reverse_mapping_function(data) + or table_path(data, replaced_with_element.path) + + if new_field_value and + new_field_value ~= ngx.null and + not deepcompare(new_field_value, shorthand_value) then + local new_field_name = join_string(".", replaced_with_element.path) + + return nil, string.format( + "both deprecated and new field are used but their values mismatch: %s = %s vs %s = %s", + shorthand_name, tostring(shorthand_value), + new_field_name, tostring(new_field_value) + ) + end + end + + return true +end + + --- Given a table, update its fields whose schema -- definition declares them as `auto = true`, -- based on its CRUD operation context, and set @@ -1741,27 +1771,34 @@ function Schema:process_auto_fields(data, context, nulls, opts) errs[sname] = err has_errs = true else - data[sname] = nil - local new_values = sdata.func(value) - if new_values then - -- a shorthand field may have a deprecation property, that is used - -- to determine whether the shorthand's return value takes precedence - -- over the similarly named actual schema fields' value when both - -- are present. On deprecated shorthand fields the actual schema - -- field value takes the precedence, otherwise the shorthand's - -- return value takes the precedence. - local deprecation = sdata.deprecation - for k, v in pairs(new_values) do - if type(v) == "table" then - local source = {} - if data[k] and data[k] ~= null then - source = data[k] - end - data[k] = deprecation and null_aware_table_merge(v, source) - or table_merge(source, v) + local _, deprecation_error = validate_deprecation_exclusiveness(data, value, sname, sdata) - elseif not deprecation or (data[k] == nil or data[k] == null) then - data[k] = v + if deprecation_error then + errs[sname] = deprecation_error + has_errs = true + else + data[sname] = nil + local new_values = sdata.func(value) + if new_values then + -- a shorthand field may have a deprecation property, that is used + -- to determine whether the shorthand's return value takes precedence + -- over the similarly named actual schema fields' value when both + -- are present. On deprecated shorthand fields the actual schema + -- field value takes the precedence, otherwise the shorthand's + -- return value takes the precedence. + local deprecation = sdata.deprecation + for k, v in pairs(new_values) do + if type(v) == "table" then + local source = {} + if data[k] and data[k] ~= null then + source = data[k] + end + data[k] = deprecation and null_aware_table_merge(v, source) + or table_merge(source, v) + + elseif not deprecation or (data[k] == nil or data[k] == null) then + data[k] = v + end end end end diff --git a/kong/db/schema/metaschema.lua b/kong/db/schema/metaschema.lua index 504893d567e7..554e59eaddcd 100644 --- a/kong/db/schema/metaschema.lua +++ b/kong/db/schema/metaschema.lua @@ -206,6 +206,15 @@ local field_schema = { { message = { type = "string", required = true } }, { removal_in_version = { type = "string", required = true } }, { old_default = { type = "any", required = false } }, + { replaced_with = { type = "array", required = false, + elements = { type = "record", + required = false, + fields = { + { path = { type = "array", len_min = 1, required = true, elements = { type = "string"}} }, + { reverse_mapping_function = { type = "function", required = false }} + }, + } + } }, }, } }, } diff --git a/kong/plugins/acme/schema.lua b/kong/plugins/acme/schema.lua index 5ccc3ffdf4a2..5b349ac11dbd 100644 --- a/kong/plugins/acme/schema.lua +++ b/kong/plugins/acme/schema.lua @@ -43,6 +43,7 @@ local LEGACY_SCHEMA_TRANSLATIONS = { len_min = 0, translate_backwards = {'password'}, deprecation = { + replaced_with = { { path = { 'password' } } }, message = "acme: config.storage_config.redis.auth is deprecated, please use config.storage_config.redis.password instead", removal_in_version = "4.0", }, func = function(value) @@ -53,6 +54,7 @@ local LEGACY_SCHEMA_TRANSLATIONS = { type = "string", translate_backwards = {'server_name'}, deprecation = { + replaced_with = { { path = { 'server_name' } } }, message = "acme: config.storage_config.redis.ssl_server_name is deprecated, please use config.storage_config.redis.server_name instead", removal_in_version = "4.0", }, func = function(value) @@ -64,6 +66,7 @@ local LEGACY_SCHEMA_TRANSLATIONS = { len_min = 0, translate_backwards = {'extra_options', 'namespace'}, deprecation = { + replaced_with = { { path = { 'extra_options', 'namespace' } } }, message = "acme: config.storage_config.redis.namespace is deprecated, please use config.storage_config.redis.extra_options.namespace instead", removal_in_version = "4.0", }, func = function(value) @@ -74,6 +77,7 @@ local LEGACY_SCHEMA_TRANSLATIONS = { type = "integer", translate_backwards = {'extra_options', 'scan_count'}, deprecation = { + replaced_with = { { path = { 'extra_options', 'scan_count' } } }, message = "acme: config.storage_config.redis.scan_count is deprecated, please use config.storage_config.redis.extra_options.scan_count instead", removal_in_version = "4.0", }, func = function(value) diff --git a/kong/plugins/rate-limiting/schema.lua b/kong/plugins/rate-limiting/schema.lua index 8928fb87fcd5..32a346c58ed7 100644 --- a/kong/plugins/rate-limiting/schema.lua +++ b/kong/plugins/rate-limiting/schema.lua @@ -104,6 +104,7 @@ return { type = "string", translate_backwards = {'redis', 'host'}, deprecation = { + replaced_with = { { path = { 'redis', 'host' } } }, message = "rate-limiting: config.redis_host is deprecated, please use config.redis.host instead", removal_in_version = "4.0", }, func = function(value) @@ -114,6 +115,7 @@ return { type = "integer", translate_backwards = {'redis', 'port'}, deprecation = { + replaced_with = { { path = { 'redis', 'port' } } }, message = "rate-limiting: config.redis_port is deprecated, please use config.redis.port instead", removal_in_version = "4.0", }, func = function(value) @@ -125,6 +127,7 @@ return { len_min = 0, translate_backwards = {'redis', 'password'}, deprecation = { + replaced_with = { { path = { 'redis', 'password' } } }, message = "rate-limiting: config.redis_password is deprecated, please use config.redis.password instead", removal_in_version = "4.0", }, func = function(value) @@ -135,6 +138,7 @@ return { type = "string", translate_backwards = {'redis', 'username'}, deprecation = { + replaced_with = { { path = { 'redis', 'username' } } }, message = "rate-limiting: config.redis_username is deprecated, please use config.redis.username instead", removal_in_version = "4.0", }, func = function(value) @@ -145,6 +149,7 @@ return { type = "boolean", translate_backwards = {'redis', 'ssl'}, deprecation = { + replaced_with = { { path = { 'redis', 'ssl' } } }, message = "rate-limiting: config.redis_ssl is deprecated, please use config.redis.ssl instead", removal_in_version = "4.0", }, func = function(value) @@ -155,6 +160,7 @@ return { type = "boolean", translate_backwards = {'redis', 'ssl_verify'}, deprecation = { + replaced_with = { { path = { 'redis', 'ssl_verify' } } }, message = "rate-limiting: config.redis_ssl_verify is deprecated, please use config.redis.ssl_verify instead", removal_in_version = "4.0", }, func = function(value) @@ -165,6 +171,7 @@ return { type = "string", translate_backwards = {'redis', 'server_name'}, deprecation = { + replaced_with = { { path = { 'redis', 'server_name' } } }, message = "rate-limiting: config.redis_server_name is deprecated, please use config.redis.server_name instead", removal_in_version = "4.0", }, func = function(value) @@ -175,6 +182,7 @@ return { type = "integer", translate_backwards = {'redis', 'timeout'}, deprecation = { + replaced_with = { { path = { 'redis', 'timeout' } } }, message = "rate-limiting: config.redis_timeout is deprecated, please use config.redis.timeout instead", removal_in_version = "4.0", }, func = function(value) @@ -185,6 +193,7 @@ return { type = "integer", translate_backwards = {'redis', 'database'}, deprecation = { + replaced_with = { { path = { 'redis', 'database' } } }, message = "rate-limiting: config.redis_database is deprecated, please use config.redis.database instead", removal_in_version = "4.0", }, func = function(value) diff --git a/kong/plugins/response-ratelimiting/schema.lua b/kong/plugins/response-ratelimiting/schema.lua index d919ced5a8ee..81b4a926474b 100644 --- a/kong/plugins/response-ratelimiting/schema.lua +++ b/kong/plugins/response-ratelimiting/schema.lua @@ -143,6 +143,7 @@ return { type = "string", translate_backwards = {'redis', 'host'}, deprecation = { + replaced_with = { { path = { 'redis', 'host' } } }, message = "response-ratelimiting: config.redis_host is deprecated, please use config.redis.host instead", removal_in_version = "4.0", }, func = function(value) @@ -153,6 +154,7 @@ return { type = "integer", translate_backwards = {'redis', 'port'}, deprecation = { + replaced_with = { { path = {'redis', 'port'} } }, message = "response-ratelimiting: config.redis_port is deprecated, please use config.redis.port instead", removal_in_version = "4.0", }, func = function(value) @@ -164,6 +166,7 @@ return { len_min = 0, translate_backwards = {'redis', 'password'}, deprecation = { + replaced_with = { { path = {'redis', 'password'} } }, message = "response-ratelimiting: config.redis_password is deprecated, please use config.redis.password instead", removal_in_version = "4.0", }, func = function(value) @@ -174,6 +177,7 @@ return { type = "string", translate_backwards = {'redis', 'username'}, deprecation = { + replaced_with = { { path = {'redis', 'username'} } }, message = "response-ratelimiting: config.redis_username is deprecated, please use config.redis.username instead", removal_in_version = "4.0", }, func = function(value) @@ -184,6 +188,7 @@ return { type = "boolean", translate_backwards = {'redis', 'ssl'}, deprecation = { + replaced_with = { { path = {'redis', 'ssl'} } }, message = "response-ratelimiting: config.redis_ssl is deprecated, please use config.redis.ssl instead", removal_in_version = "4.0", }, func = function(value) @@ -194,6 +199,7 @@ return { type = "boolean", translate_backwards = {'redis', 'ssl_verify'}, deprecation = { + replaced_with = { { path = {'redis', 'ssl_verify'} } }, message = "response-ratelimiting: config.redis_ssl_verify is deprecated, please use config.redis.ssl_verify instead", removal_in_version = "4.0", }, func = function(value) @@ -204,6 +210,7 @@ return { type = "string", translate_backwards = {'redis', 'server_name'}, deprecation = { + replaced_with = { { path = {'redis', 'server_name'} } }, message = "response-ratelimiting: config.redis_server_name is deprecated, please use config.redis.server_name instead", removal_in_version = "4.0", }, func = function(value) @@ -214,6 +221,7 @@ return { type = "integer", translate_backwards = {'redis', 'timeout'}, deprecation = { + replaced_with = { { path = {'redis', 'timeout'} } }, message = "response-ratelimiting: config.redis_timeout is deprecated, please use config.redis.timeout instead", removal_in_version = "4.0", }, func = function(value) @@ -224,6 +232,7 @@ return { type = "integer", translate_backwards = {'redis', 'database'}, deprecation = { + replaced_with = { { path = {'redis', 'database'} } }, message = "response-ratelimiting: config.redis_database is deprecated, please use config.redis.database instead", removal_in_version = "4.0", }, func = function(value) diff --git a/spec/01-unit/01-db/01-schema/01-schema_spec.lua b/spec/01-unit/01-db/01-schema/01-schema_spec.lua index a6c03d3d8455..c2581ec5a2c8 100644 --- a/spec/01-unit/01-db/01-schema/01-schema_spec.lua +++ b/spec/01-unit/01-db/01-schema/01-schema_spec.lua @@ -1,6 +1,7 @@ local Schema = require "kong.db.schema" local cjson = require "cjson" local helpers = require "spec.helpers" +local table_copy = require "kong.tools.table".deep_copy local SchemaKind = { @@ -4114,8 +4115,8 @@ describe("schema", function() local TestSchema = Schema.new({ name = "test", fields = { - { name = { type = "string" } }, - { record = { + { field_A = { type = "string" } }, + { field_B = { type = "record", fields = { { x = { type = "string" } } @@ -4124,21 +4125,21 @@ describe("schema", function() }, shorthand_fields = { { - username = { + shorthand_A = { type = "string", func = function(value) return { - name = value + field_A = value } end, }, }, { - y = { + shorthand_B = { type = "string", func = function(value) return { - record = { + field_B = { x = value, }, } @@ -4148,70 +4149,335 @@ describe("schema", function() }, }) - local input = { username = "test1", name = "ignored", record = { x = "ignored" }, y = "test1" } + local input = { shorthand_A = "test1", field_A = "ignored", + shorthand_B = "test2", field_B = { x = "ignored" } } local output, _ = TestSchema:process_auto_fields(input) - assert.same({ name = "test1", record = { x = "test1" } }, output) + assert.same({ field_A = "test1", field_B = { x = "test2" } }, output) - -- deprecated fields does take precedence if the new fields are null - local input = { username = "overwritten-1", name = ngx.null, record = { x = ngx.null }, y = "overwritten-2" } + -- shorthand value takes precedence if the destination field is null + local input = { shorthand_A = "overwritten-1", field_A = ngx.null, + shorthand_B = "overwritten-2", field_B = { x = ngx.null }} local output, _ = TestSchema:process_auto_fields(input) - assert.same({ name = "overwritten-1", record = { x = "overwritten-2" } }, output) + assert.same({ field_A = "overwritten-1", field_B = { x = "overwritten-2" } }, output) end) - it("does not take precedence if deprecated", function() + describe("with simple 'table_path' reverse mapping", function() local TestSchema = Schema.new({ name = "test", fields = { - { name = { type = "string" } }, - { record = { + { new_A = { type = "string" } }, + { new_B = { type = "record", fields = { { x = { type = "string" } } }, }}, + { new_C = { type = "string", default = "abc", required = true }}, + { new_D_1 = { type = "string" }}, + { new_D_2 = { type = "string" }}, }, shorthand_fields = { { - username = { + old_A = { type = "string", func = function(value) return { - name = value + new_A = value } end, deprecation = { - message = "username is deprecated, please use name instead", + replaced_with = { { path = { "new_A" } } }, + message = "old_A is deprecated, please use new_A instead", removal_in_version = "4.0", }, }, }, { - y = { + old_B = { type = "string", func = function(value) return { - record = { + new_B = { x = value, }, } end, deprecation = { - message = "y is deprecated, please use record.x instead", + replaced_with = { { path = { "new_B", "x" } } }, + message = "old_B is deprecated, please use new_B.x instead", removal_in_version = "4.0", }, }, }, + { + old_C = { + type = "string", + func = function(value) + return { + new_C = value + } + end, + deprecation = { + replaced_with = { { path = { "new_C" } } }, + message = "old_C is deprecated, please use new_C instead", + removal_in_version = "4.0", + } + } + }, + { + old_D = { + type = "string", + func = function(value) + return { new_D_1 = value, new_D_2 = value } + end, + deprecation = { + replaced_with = { { path = { "new_D_1" } }, { path = { "new_D_2" } } }, + message = "old_D is deprecated, please use new_D_1 and new_D_2 instead", + removal_in_version = "4.0", + } + } + } }, }) - local input = { username = "ignored", name = "test1", record = { x = "test1" }, y = "ignored" } - local output, _ = TestSchema:process_auto_fields(input) - assert.same({ name = "test1", record = { x = "test1" } }, output) + it("notifes of error if values mismatch with replaced field", function() + local input = { old_A = "not-test-1", new_A = "test-1", + old_B = "not-test-2", new_B = { x = "test-2" }, + old_C = "abc", new_C = "not-abc", -- "abc" is the default value + old_D = "test-4", new_D_1 = "test-4", new_D_2 = "not-test-4", } + local output, err = TestSchema:process_auto_fields(input) + assert.same({ + old_A = 'both deprecated and new field are used but their values mismatch: old_A = not-test-1 vs new_A = test-1', + old_B = 'both deprecated and new field are used but their values mismatch: old_B = not-test-2 vs new_B.x = test-2' , + old_C = 'both deprecated and new field are used but their values mismatch: old_C = abc vs new_C = not-abc', + old_D = 'both deprecated and new field are used but their values mismatch: old_D = test-4 vs new_D_2 = not-test-4' }, + err + ) + assert.falsy(output) + end) - -- deprecated fields does take precedence if the new fields are null - local input = { username = "overwritten-1", name = ngx.null, record = { x = ngx.null }, y = "overwritten-2" } - local output, _ = TestSchema:process_auto_fields(input) - assert.same({ name = "overwritten-1", record = { x = "overwritten-2" } }, output) + it("accepts config if both new field and deprecated field defined and their values match", function() + local input = { old_A = "test-1", new_A = "test-1", + old_B = "test-2", new_B = { x = "test-2" }, + -- "C" field is using default + old_D = "test-4", new_D_1 = "test-4", new_D_2 = "test-4", } + local output, err = TestSchema:process_auto_fields(input) + assert.is_nil(err) + assert.same({ new_A = "test-1", new_B = { x = "test-2" }, new_C = "abc", new_D_1 = "test-4", new_D_2 = "test-4" }, output) + + + local input = { old_A = "test-1", new_A = "test-1", + old_B = "test-2", new_B = { x = "test-2" }, + old_C = "test-3", -- no new field C specified but it has a default which should be ignored + new_D_1 = "test-4-1", new_D_2 = "test-4-2", } + local output, err = TestSchema:process_auto_fields(input) + assert.is_nil(err) + assert.same({ new_A = "test-1", new_B = { x = "test-2" }, new_C = "test-3", new_D_1 = "test-4-1", new_D_2 = "test-4-2" }, output) + + -- when new values are null it's still accepted + local input = { old_A = "test-1", new_A = ngx.null, + old_B = "test-2", new_B = { x = ngx.null }, + old_C = "test-3", new_C = ngx.null, + old_D = "test-4", new_D_1 = ngx.null, new_D_2 = ngx.null, } + local output, err = TestSchema:process_auto_fields(input) + assert.is_nil(err) + assert.same({new_A = "test-1", new_B = { x = "test-2" }, new_C = "test-3", new_D_1 = "test-4", new_D_2 = "test-4" }, output) + + -- when old values are null it's still accepted + local input = { old_A = ngx.null, new_A = "test-1", + old_B = ngx.null, new_B = { x = "test-2" }, + old_C = ngx.null, new_C = "test-3", + old_D = ngx.null, new_D_1 = "test-4-1", new_D_2 = "test-4-2", } + local output, err = TestSchema:process_auto_fields(input) + assert.is_nil(err) + assert.same({ new_A = "test-1", new_B = { x = "test-2" }, new_C = "test-3", new_D_1 = "test-4-1", new_D_2 = "test-4-2" }, output) + end) + + it("allows to set explicit nulls when only one set of fields was passed", function() + -- when new values are null it's still accepted + local input = { new_A = ngx.null, + new_B = { x = ngx.null }, + new_C = ngx.null, + new_D_1 = ngx.null, new_D_2 = ngx.null } + local output, err = TestSchema:process_auto_fields(input) + assert.is_nil(err) + assert.same({new_A = ngx.null, new_B = { x = ngx.null }, new_C = ngx.null, new_D_1 = ngx.null, new_D_2 = ngx.null}, output) + + -- when old values are null it's still accepted + local input = { old_A = ngx.null, + old_B = ngx.null, + old_C = ngx.null, + old_D = ngx.null } + local output, err = TestSchema:process_auto_fields(input) + assert.is_nil(err) + assert.same({new_A = ngx.null, new_B = { x = ngx.null }, new_C = ngx.null, new_D_1 = ngx.null, new_D_2 = ngx.null}, output) + end) + end) + + describe("with complex field reverse_mapping_function", function() + local TestSchema = Schema.new({ + name = "test", + fields = { + { new_A = { type = "string" } }, + { new_B = { + type = "record", + fields = { + { x = { type = "string" } } + }, + }}, + { new_C = { + type = "array", + elements = { + type = "number" + } + }} + }, + shorthand_fields = { + { + old_A = { + type = "string", + func = function(value) + if value == ngx.null then + return { new_A = ngx.null } + end + return { new_A = value:upper() } + end, + deprecation = { + replaced_with = { + { path = { "new_A" }, + reverse_mapping_function = function(data) + if data.new_A and data.new_A ~= ngx.null then + return data.new_A:lower() + end + + return data.new_A + end } + }, + message = "old_A is deprecated, please use new_A instead", + removal_in_version = "4.0", + }, + }, + }, + { + old_B = { + type = "string", + func = function(value) + if value == ngx.null then + return { + new_B = { + x = ngx.null, + }, + } + end + + return { + new_B = { + x = value:upper(), + }, + } + end, + deprecation = { + replaced_with = { + { path = { "new_B", "x" }, + reverse_mapping_function = function (data) + if data.new_B and data.new_B.x ~= ngx.null then + return data.new_B.x:lower() + end + return ngx.null + end + } }, + message = "old_B is deprecated, please use new_B.x instead", + removal_in_version = "4.0", + }, + }, + }, + { + old_C = { + type = "array", + elements = { + type = "number" + }, + func = function(value) + if value == ngx.null then + return { new_C = ngx.null } + end + local copy = table_copy(value) + table.sort(copy, function(a,b) return a > b end ) + return { new_C = copy } -- new field is reversed + end, + deprecation = { + replaced_with = { + { path = { "new_C" }, + reverse_mapping_function = function (data) + if data.new_C == ngx.null then + return ngx.null + end + + local copy = table_copy(data.new_C) + table.sort(copy, function(a,b) return a < b end) + return copy + end + }, + } + } + } + } + }, + }) + + it("notifes of error if values mismatch with replaced field", function() + local input = { old_A = "not-test-1", new_A = "TEST1", + old_B = "not-test-2", new_B = { x = "TEST2" }, + old_C = { 1, 2, 4 }, new_C = { 3, 2, 1 } } + local output, err = TestSchema:process_auto_fields(input) + assert.same('both deprecated and new field are used but their values mismatch: old_A = not-test-1 vs new_A = test1', err.old_A) + assert.same('both deprecated and new field are used but their values mismatch: old_B = not-test-2 vs new_B.x = test2', err.old_B) + assert.matches('both deprecated and new field are used but their values mismatch: old_C = .+ vs new_C = .+', err.old_C) + assert.falsy(output) + end) + + it("accepts config if both new field and deprecated field defined and their values match", function() + local input = { old_A = "test-1", new_A = "TEST-1", + old_B = "test-2", new_B = { x = "TEST-2" }, + old_C = { 1, 2, 3 }, new_C = { 3, 2, 1 } } + local output, err = TestSchema:process_auto_fields(input) + assert.is_nil(err) + assert.same({ new_A = "TEST-1", new_B = { x = "TEST-2" }, new_C = { 3, 2, 1 }}, output) + + -- when new values are null it's still accepted + local input = { old_A = "test-1", new_A = ngx.null, + old_B = "test-2", new_B = { x = ngx.null }, + old_C = { 1, 2, 3 }, new_C = ngx.null } + local output, err = TestSchema:process_auto_fields(input) + assert.is_nil(err) + assert.same({ new_A = "TEST-1", new_B = { x = "TEST-2" }, new_C = { 3, 2, 1 }}, output) + + -- when old values are null it's still accepted + local input = { old_A = ngx.null, new_A = "TEST-1", + old_B = ngx.null, new_B = { x = "TEST-2" }, + old_C = ngx.null, new_C = { 3, 2, 1 } } + local output, err = TestSchema:process_auto_fields(input) + assert.is_nil(err) + assert.same({ new_A = "TEST-1", new_B = { x = "TEST-2" }, new_C = { 3, 2, 1 }}, output) + end) + + it("allows to set explicit nulls when only one set of fields was passed", function() + -- when new values are null it's still accepted + local input = { new_A = ngx.null, + new_B = { x = ngx.null }, + new_C = ngx.null } + local output, err = TestSchema:process_auto_fields(input) + assert.is_nil(err) + assert.same({new_A = ngx.null, new_B = { x = ngx.null }, new_C = ngx.null}, output) + + -- when old values are null it's still accepted + local input = { old_A = ngx.null, + old_B = ngx.null, + old_C = ngx.null } + local output, err = TestSchema:process_auto_fields(input) + assert.is_nil(err) + assert.same({new_A = ngx.null, new_B = { x = ngx.null }, new_C = ngx.null}, output) + end) end) it("can produce multiple fields", function()