From 63d90f886bc82d6d67416306039665b03abe9292 Mon Sep 17 00:00:00 2001 From: Sam Lee-Lindsay Date: Wed, 31 Jul 2024 13:39:00 +1000 Subject: [PATCH 1/2] Fix(SCIMMY.Types.SchemaDefinition): properly handle schema extensions when filtering coerced values --- src/lib/types/definition.js | 88 +++++++++++++++++++-------------- test/lib/types/definition.js | 96 +++++++++++++++++++++++------------- 2 files changed, 113 insertions(+), 71 deletions(-) diff --git a/src/lib/types/definition.js b/src/lib/types/definition.js index bad6eb6..242e21f 100644 --- a/src/lib/types/definition.js +++ b/src/lib/types/definition.js @@ -1,6 +1,9 @@ import {Attribute} from "./attribute.js"; import {Filter} from "./filter.js"; +// Deeply inspect a filter object to see if it represents attributes to be excluded from a coerced value +const isExcludedAttributesFilter = (v) => Array.isArray(v) ? v[0] === "np" : Object.values(v).every(isExcludedAttributesFilter); + /** * SCIM Schema Definition Type * @alias SCIMMY.Types.SchemaDefinition @@ -317,79 +320,90 @@ export class SchemaDefinition { } } - return SchemaDefinition.#filter(target, filter && {...filter}, this.attributes); + return SchemaDefinition.#filter(this, filter && {...filter}, target); } /** * Filter out desired or undesired attributes from a coerced schema value - * @param {Object|Object[]} [data] - the data to filter attributes from + * @param {SCIMMY.Types.SchemaDefinition} definition - the schema definition requesting the filtering * @param {Object} [filter] - the filter to apply to the coerced value - * @param {SCIMMY.Types.Attribute[]} [attributes] - set of attributes to match against + * @param {Object|Object[]} [data={}] - the data to filter attributes from + * @param {String} [prefix=""] - prefix to use when filtering on complex value subAttributes * @returns {Object} the coerced value with desired or undesired attributes filtered out * @private */ - static #filter(data = {}, filter, attributes) { + static #filter(definition, filter, data = {}, prefix = "") { // If there's no filter, just return the data - if (filter === undefined) return data; + if (filter === undefined || !Object.keys(filter).length) + return data; // If the data is a set, only get values that match the filter else if (Array.isArray(data)) - return data.map(data => SchemaDefinition.#filter(data, {...filter}, attributes)).filter(v => Object.keys(v).length); + return data.map(data => SchemaDefinition.#filter(definition, {...filter}, data, prefix)).filter(v => Object.keys(v).length); // Otherwise, filter the data! else { // Prepare resultant value storage const target = {}; - const filterable = {...filter}; - const inclusions = attributes.map(({name}) => name); + const inclusions = []; + const exclusions = []; - // Check for any negative filters - for (let key in {...filterable}) { - // Find the attribute by lower case name - const {name, config: {returned} = {}} = attributes.find(a => a.name.toLowerCase() === key.toLowerCase()) ?? {}; + for (let key in filter) try { + // Find the attribute or extension definition using the filter key + const attribute = definition.attribute(prefix ? `${prefix}.${key}` : key); - // Mark the property as omitted from the result, and remove the spent filter - if (returned !== "always" && Array.isArray(filterable[key]) && filterable[key][0] === "np") { - inclusions.splice(inclusions.indexOf(name), 1); - delete filterable[key]; + // Only be concerned with filter expressions for attributes or extensions directly for now + if (Array.isArray(filter[key]) && (attribute instanceof SchemaDefinition || !key.startsWith("urn:"))) { + const name = (attribute instanceof SchemaDefinition ? attribute.id : attribute.name); + + // Mark the positively filtered property as included in the result + if (filter[key][0] === "pr") + inclusions.push(name); + // Mark the negatively filtered property as excluded from the result + else if (filter[key][0] === "np") + exclusions.push(name); } + } catch { + // If we've reached here, the filter refers to an unknown attribute and should be ignored } - // Check for remaining positive filters - if (Object.keys(filterable).length) { - // If there was a positive filter, ignore the negative filters - inclusions.splice(0, inclusions.length); - - // Mark the positively filtered property as included in the result, and remove the spent filter - for (let key in {...filterable}) if (Array.isArray(filterable[key]) && filterable[key][0] === "pr") { - inclusions.push(key); - delete filterable[key]; + // If there were no explicit inclusions, and all filter expressions were negative... + if (!inclusions.length && isExcludedAttributesFilter(filter)) { + // ...go through all subAttributes, or extension attributes... + for (let attribute of (prefix ? definition.attribute(prefix).subAttributes : definition.attributes)) { + // ...and assume they should be included, if they weren't explicitly excluded + if (attribute instanceof Attribute && !exclusions.includes(attribute.name)) inclusions.push(attribute.name); } } - // Go through every value in the data and filter attributes + // Go through every value in the data and filter it for (let key in data) { - if (key.toLowerCase().startsWith("urn:")) { - // If there is data in a namespaced key, and a filter for it, include it - if (Object.keys(data[key]).length && inclusions.some(k => k.toLowerCase().startsWith(`${key.toLowerCase()}:`))) + // Get the matching attribute or extension definition for the key + const attribute = definition.attribute(prefix ? `${prefix}.${key}` : key) ?? {}; + + if (attribute instanceof SchemaDefinition) { + // If there is data in a namespaced key and no namespace filter, or there's an explicit inclusion filter... + if ((Object.keys(data[key]).length && !Array.isArray(filter[key])) || (key in filter && !exclusions.includes(key))) + // ...include the extension data target[key] = data[key]; } else { - // Get the matching attribute definition and some relevant config values - const attribute = attributes.find(a => a.name === key) ?? {}; - const {type, config: {returned, multiValued} = {}, subAttributes} = attribute; + // Get some relevant config values from the attribute + const {name, type, config: {returned, multiValued} = {}} = attribute; // If the attribute is always returned, add it to the result if (returned === "always") target[key] = data[key]; // Otherwise, if the attribute was requested and ~can~ be returned, process it else if (![false, "never"].includes(returned)) { - // If there was a simple presence filter for the attribute, assign it - if (inclusions.includes(key) && data[key] !== undefined) target[key] = data[key]; - // Otherwise, if there's an unhandled filter for a complex attribute, evaluate it - else if (key in filter && type === "complex") { - const value = SchemaDefinition.#filter(data[key], filter[key], subAttributes); + // If there's a filter for a complex attribute, evaluate it + if (key in filter && !Array.isArray(filter[key]) && type === "complex") { + const value = SchemaDefinition.#filter(definition, filter[key], data[key], key); // Only set the value if it isn't empty if ((!multiValued && value !== undefined) || (Array.isArray(value) && value.length)) target[key] = value; } + // Otherwise, if there was a simple presence filter for the attribute, assign it + else if (inclusions.includes(name) && data[key] !== undefined) { + target[key] = data[key]; + } } } } diff --git a/test/lib/types/definition.js b/test/lib/types/definition.js index e838a0e..644dd8b 100644 --- a/test/lib/types/definition.js +++ b/test/lib/types/definition.js @@ -14,6 +14,7 @@ const basepath = path.relative(process.cwd(), path.dirname(url.fileURLToPath(imp const fixtures = fs.readFile(path.join(basepath, "./definition.json"), "utf8").then((f) => JSON.parse(f)); // Default parameter values to use in tests const params = {name: "Test", id: "urn:ietf:params:scim:schemas:Test"}; +const extensionId = params.id.replace("Test", "Extension"); describe("SCIMMY.Types.SchemaDefinition", () => { describe("@constructor", () => { @@ -474,7 +475,6 @@ describe("SCIMMY.Types.SchemaDefinition", () => { it("should expect coerce to be called on included schema extensions", () => { const stub = sinon.stub(); const get = (t, p) => (p === "coerce" ? (...args) => (stub(...args) ?? t.coerce(...args)) : t[p]); - const extensionId = params.id.replace("Test", "Extension"); const attributes = [new Attribute("string", "employeeNumber")]; const extension = new SchemaDefinition("Extension", extensionId, "An Extension", attributes).truncate(["schemas", "meta"]); const definition = new SchemaDefinition(...Object.values(params)).extend(new Proxy(extension, {get}), true); @@ -483,46 +483,46 @@ describe("SCIMMY.Types.SchemaDefinition", () => { assert.ok(stub.calledWithMatch({employeenumber: "1234"}), "Instance method 'coerce' did not call coerce method on included schema extensions"); - assert.deepStrictEqual(JSON.parse(JSON.stringify(actual)), expected, + assert.deepStrictEqual({...actual}, expected, "Instance method 'coerce' did not correctly coerce included schema extension value"); }); it("should expect required schema extensions to be defined", () => { - const extension = new SchemaDefinition("Extension", params.id.replace("Test", "Extension"), "An Extension", []); - const definition = new SchemaDefinition(...Object.values(params)).extend(extension, true); + const definition = new SchemaDefinition(...Object.values(params)) + .extend(new SchemaDefinition("Extension", extensionId, "An Extension", []), true); assert.throws(() => definition.coerce({}), - {name: "TypeError", message: `Missing values for required schema extension '${extension.id}'`}, + {name: "TypeError", message: `Missing values for required schema extension '${extensionId}'`}, "Instance method 'coerce' did not attempt to coerce required schema extension"); - assert.throws(() => definition.coerce({[extension.id]: {}}), - {name: "TypeError", message: `Missing values for required schema extension '${extension.id}'`}, + assert.throws(() => definition.coerce({[extensionId]: {}}), + {name: "TypeError", message: `Missing values for required schema extension '${extensionId}'`}, "Instance method 'coerce' did not attempt to coerce required schema extension"); }); it("should expect namespaced attributes or extensions to be coerced", () => { const attribute = new Attribute("string", "employeeNumber"); - const extension = new SchemaDefinition("Extension", params.id.replace("Test", "Extension"), "An Extension", [attribute]); - const definition = new SchemaDefinition(...Object.values(params)).extend(extension, true); - const metadata = {schemas: [definition.id, extension.id], meta: {resourceType: definition.name}}; + const extension = new SchemaDefinition("Extension", extensionId, "An Extension", [attribute]); + const definition = new SchemaDefinition(...Object.values(params)).truncate(["meta"]).extend(extension, true); + const metadata = {schemas: [definition.id, extension.id]}; const expected = {[extension.id]: {employeeNumber: "1234"}}; - assert.deepStrictEqual(JSON.parse(JSON.stringify(definition.coerce(expected))), {...metadata, ...expected}, + assert.deepStrictEqual({...definition.coerce(expected)}, {...metadata, ...expected}, "Instance method 'coerce' failed to coerce required schema extension value"); - assert.deepStrictEqual(JSON.parse(JSON.stringify(definition.coerce({[`${extension.id}:employeeNumber`]: "1234"}))), {...metadata, ...expected}, + assert.deepStrictEqual({...definition.coerce({[`${extensionId}:employeeNumber`]: "1234"})}, {...metadata, ...expected}, "Instance method 'coerce' failed to coerce required schema extension value"); - assert.throws(() => definition.coerce({[`${extension.id}:employeeNumber`]: false}), + assert.throws(() => definition.coerce({[`${extensionId}:employeeNumber`]: false}), {name: "TypeError", message: `Attribute 'employeeNumber' expected value type 'string' but found type 'boolean' in schema extension '${extension.id}'`}, "Instance method 'coerce' did not attempt to coerce required schema extension's invalid value"); }); it("should expect deeply nested namespaced and extension attributes to be merged and coerced", () => { const attributes = [new Attribute("complex", "test", {}, [new Attribute("string", "name"), new Attribute("string", "value")])] - const extension = new SchemaDefinition("Extension", params.id.replace("Test", "Extension"), "An Extension", attributes); + const extension = new SchemaDefinition("Extension", extensionId, "An Extension", attributes); const definition = new SchemaDefinition(...Object.values(params)).extend(extension, true); const {[extension.id]: actual} = definition.coerce({[`${extension.id}:test.value`]: "Test", [extension.id]: {test: {name: "Test"}}}); const expected = {test: {name: "Test", value: "Test"}}; - assert.deepStrictEqual(JSON.parse(JSON.stringify(actual)), expected, + assert.deepStrictEqual({...actual}, expected, "Instance method 'coerce' did not expect deeply nested namespaced and extension attributes to be merged and coerced"); }); @@ -539,49 +539,77 @@ describe("SCIMMY.Types.SchemaDefinition", () => { it("should expect negative filters to be applied to coerced results", () => { const attributes = [new Attribute("complex", "test", {}, [new Attribute("string", "name"), new Attribute("string", "value")])]; - const definition = new SchemaDefinition(...Object.values(params), "Test Schema", attributes); + const definition = new SchemaDefinition(...Object.values(params), "Test Schema", attributes).truncate(["schemas", "meta"]); const actual = definition.coerce({test: {name: "Test", value: "False"}}, undefined, undefined, new Filter("test.value np")); const expected = {test: {name: "Test"}} - assert.deepStrictEqual(JSON.parse(JSON.stringify(actual)), expected, + assert.deepStrictEqual({...actual}, expected, "Instance method 'coerce' did not expect negative filters to be applied to coerced results"); }); + it("should expect missing attributes in a filter to be ignored", () => { + const attributes = [new Attribute("string", "employeeNumber"), new Attribute("string", "costCenter")]; + const definition = new SchemaDefinition(...Object.values(params), "Test Schema", attributes).truncate(["schemas", "meta"]); + const source = {employeeNumber: "Test", costCenter: "Test"}; + const actual = definition.coerce(source, undefined, undefined, new Filter("userName np")); + const expected = {employeeNumber: "Test", costCenter: "Test"}; + + assert.deepStrictEqual({...actual}, expected, + "Instance method 'coerce' did not ignore missing attributes specified in filter"); + }); + it("should expect complex multi-valued attributes to be filtered positively", () => { const attributes = [new Attribute("complex", "test", {multiValued: true}, [new Attribute("string", "name"), new Attribute("string", "value")])]; - const definition = new SchemaDefinition(...Object.values(params), "Test Schema", attributes); + const definition = new SchemaDefinition(...Object.values(params), "Test Schema", attributes).truncate(["schemas", "meta"]); const source = {test: [{name: "Test", value: "Test"}, {value: "False"}]}; - const actual = definition.coerce(source, undefined, undefined, new Filter("test[name pr]")); + const actual = definition.coerce(source, undefined, undefined, new Filter("test.name pr")); const expected = {test: [{name: "Test"}]}; - assert.deepStrictEqual(JSON.parse(JSON.stringify(actual)), expected, + assert.deepStrictEqual({...actual}, expected, "Instance method 'coerce' did not positively filter complex multi-valued attributes"); }); it("should expect complex multi-valued attributes to be filtered negatively", () => { const attributes = [new Attribute("complex", "test", {multiValued: true}, [new Attribute("string", "name"), new Attribute("string", "value")])]; - const definition = new SchemaDefinition(...Object.values(params), "Test Schema", attributes); + const definition = new SchemaDefinition(...Object.values(params), "Test Schema", attributes).truncate(["schemas", "meta"]); const source = {test: [{name: "Test", value: "Test"}, {value: "False"}]}; - const actual = definition.coerce(source, undefined, undefined, new Filter("test[name np]")); + const actual = definition.coerce(source, undefined, undefined, new Filter("test.name np")); const expected = {test: [{value: "Test"}, {value: "False"}]}; - assert.deepStrictEqual(JSON.parse(JSON.stringify(actual)), expected, + assert.deepStrictEqual({...actual}, expected, "Instance method 'coerce' did not negatively filter complex multi-valued attributes"); }); - it("should expect namespaced attributes in the supplied filter to be applied to coerced result", () => { - const attribute = new Attribute("string", "employeeNumber"); + for (let [target, outcome, filter, unexpected, expected] of [ + ["namespaced attributes", "present in coerced result", + "employeeNumber np", "unexpectedly excluded", + {costCenter: "Test", [extensionId]: {employeeNumber: "1234", costCenter: "Test"}}], + ["namespaced attributes", "filtered positively", + `${extensionId}:employeeNumber pr`, "unexpectedly included", + {[extensionId]: {employeeNumber: "1234"}}], + ["namespaced attributes", "filtered negatively", + `${extensionId}:employeeNumber np`, "unexpectedly excluded", + {employeeNumber: "Test", costCenter: "Test", [extensionId]: {costCenter: "Test"}}], + ["extension namespaces", "filtered positively", + `${extensionId} pr`, "unexpectedly included", + {[extensionId]: {employeeNumber: "1234", costCenter: "Test"}}], + ["extension namespaces", "filtered negatively", + `${extensionId} np`, "unexpectedly excluded", + {employeeNumber: "Test", costCenter: "Test"}], + ["direct and namespaced attributes", "filtered positively", + `costCenter pr and ${extensionId}:employeeNumber pr`, "unexpectedly included", + {costCenter: "Test", [extensionId]: {employeeNumber: "1234"}}], + ["direct and namespaced attributes", "filtered negatively", + `costCenter np and ${extensionId}:employeeNumber np`, "unexpectedly excluded", + {employeeNumber: "Test", [extensionId]: {costCenter: "Test"}}] + ]) it(`should expect ${target} to be ${outcome}`, () => { + const source = {employeeNumber: "Test", costCenter: "Test", [`${extensionId}:employeeNumber`]: "1234", [`${extensionId}:costCenter`]: "Test"}; const attributes = [new Attribute("string", "employeeNumber"), new Attribute("string", "costCenter")]; - const extension = new SchemaDefinition("Extension", params.id.replace("Test", "Extension"), "An Extension", attributes); - const definition = new SchemaDefinition(...Object.values(params), "Test Schema", [attribute]).extend(extension); - const source = {employeeNumber: "Test", [`${extension.id}:employeeNumber`]: "1234", [`${extension.id}:costCenter`]: "Test"}; - const actual = definition.coerce(source, undefined, undefined, new Filter(`${extension.id}:employeeNumber pr`)); - const expected = {[extension.id]: {employeeNumber: "1234"}}; + const definition = new SchemaDefinition(...Object.values(params), "Test Schema", attributes).truncate(["schemas", "meta"]) + .extend(new SchemaDefinition("Extension", extensionId, "An Extension", attributes)); - assert.strictEqual(actual[extension.id].employeeNumber, "1234", - "Instance method 'coerce' did not include namespaced attributes for filter"); - assert.deepStrictEqual(JSON.parse(JSON.stringify(actual)), expected, - "Instance method 'coerce' included namespaced attributes not specified for filter"); + assert.deepStrictEqual({...definition.coerce(source, undefined, undefined, new Filter(filter))}, expected, + `Instance method 'coerce' ${unexpected} ${target} not specified for filter`); }); }); }); \ No newline at end of file From 8df3846a618bfce63f44ccb05b7ea47f0374acfb Mon Sep 17 00:00:00 2001 From: Sam Lee-Lindsay Date: Thu, 1 Aug 2024 17:01:41 +1000 Subject: [PATCH 2/2] Fix(SCIMMY.Types.Filter): make sure positive filters are preferred over negative filters in coercion --- src/lib/types/definition.js | 8 ++-- test/lib/types/definition.js | 74 +++++++++++++----------------------- 2 files changed, 31 insertions(+), 51 deletions(-) diff --git a/src/lib/types/definition.js b/src/lib/types/definition.js index 242e21f..38bdd21 100644 --- a/src/lib/types/definition.js +++ b/src/lib/types/definition.js @@ -352,13 +352,15 @@ export class SchemaDefinition { // Only be concerned with filter expressions for attributes or extensions directly for now if (Array.isArray(filter[key]) && (attribute instanceof SchemaDefinition || !key.startsWith("urn:"))) { + // Get real name and handle potentially overlapping filter conditions const name = (attribute instanceof SchemaDefinition ? attribute.id : attribute.name); + const condition = filter[key].map(c => Array.isArray(c) ? c[0] : c); // Mark the positively filtered property as included in the result - if (filter[key][0] === "pr") + if (condition.includes("pr")) inclusions.push(name); // Mark the negatively filtered property as excluded from the result - else if (filter[key][0] === "np") + else if (condition.includes("np")) exclusions.push(name); } } catch { @@ -381,7 +383,7 @@ export class SchemaDefinition { if (attribute instanceof SchemaDefinition) { // If there is data in a namespaced key and no namespace filter, or there's an explicit inclusion filter... - if ((Object.keys(data[key]).length && !Array.isArray(filter[key])) || (key in filter && !exclusions.includes(key))) + if ((Object.keys(data[key]).length && !Array.isArray(filter[key])) || (key in filter && inclusions.includes(key))) // ...include the extension data target[key] = data[key]; } else { diff --git a/test/lib/types/definition.js b/test/lib/types/definition.js index 644dd8b..5a11676 100644 --- a/test/lib/types/definition.js +++ b/test/lib/types/definition.js @@ -537,71 +537,49 @@ describe("SCIMMY.Types.SchemaDefinition", () => { "Instance method 'coerce' included attributes not specified for filter 'testName pr'"); }); - it("should expect negative filters to be applied to coerced results", () => { - const attributes = [new Attribute("complex", "test", {}, [new Attribute("string", "name"), new Attribute("string", "value")])]; - const definition = new SchemaDefinition(...Object.values(params), "Test Schema", attributes).truncate(["schemas", "meta"]); - const actual = definition.coerce({test: {name: "Test", value: "False"}}, undefined, undefined, new Filter("test.value np")); - const expected = {test: {name: "Test"}} + it("should expect positive filters to be preferred over negative filters", () => { + const source = {testName: "a string", testValue: "another string", employeeNumber: "Test", costCenter: "Test"}; + const attributes = [new Attribute("string", "testName"), new Attribute("string", "testValue"), new Attribute("string", "employeeNumber"), new Attribute("string", "costCenter")]; + const definition = new SchemaDefinition(...Object.values(params), "Test Schema", attributes); + const actual = definition.coerce(source, undefined, undefined, new Filter("employeeNumber np and testName np and testName pr")); + const expected = {testName: "a string"}; assert.deepStrictEqual({...actual}, expected, - "Instance method 'coerce' did not expect negative filters to be applied to coerced results"); + "Instance method 'coerce' did not prefer positive filters over negative filters"); }); it("should expect missing attributes in a filter to be ignored", () => { const attributes = [new Attribute("string", "employeeNumber"), new Attribute("string", "costCenter")]; const definition = new SchemaDefinition(...Object.values(params), "Test Schema", attributes).truncate(["schemas", "meta"]); - const source = {employeeNumber: "Test", costCenter: "Test"}; - const actual = definition.coerce(source, undefined, undefined, new Filter("userName np")); const expected = {employeeNumber: "Test", costCenter: "Test"}; + const actual = definition.coerce(expected, undefined, undefined, new Filter("userName np")); assert.deepStrictEqual({...actual}, expected, "Instance method 'coerce' did not ignore missing attributes specified in filter"); }); - it("should expect complex multi-valued attributes to be filtered positively", () => { - const attributes = [new Attribute("complex", "test", {multiValued: true}, [new Attribute("string", "name"), new Attribute("string", "value")])]; + for (let [target, outcome, unexpected, expected, filter, multiValued] of [ + ["complex attributes", "filtered positively", "unexpectedly included", {test: {value: "False"}}, "test.value pr"], + ["complex attributes", "filtered negatively", "unexpectedly excluded", {test: {name: "Test"}}, "test.value np"], + ["complex multi-value attributes", "filtered positively", "unexpectedly included", {test: [{name: "Test"}]}, "test.name pr", true], + ["complex multi-value attributes", "filtered negatively", "unexpectedly excluded", {test: [{value: "Test"}, {value: "False"}]}, "test.name np", true] + ]) it(`should expect ${target} to be ${outcome}`, () => { + const source = {test: multiValued ? [{name: "Test", value: "Test"}, {value: "False"}] : {name: "Test", value: "False"}}; + const attributes = [new Attribute("complex", "test", {multiValued}, [new Attribute("string", "name"), new Attribute("string", "value")])]; const definition = new SchemaDefinition(...Object.values(params), "Test Schema", attributes).truncate(["schemas", "meta"]); - const source = {test: [{name: "Test", value: "Test"}, {value: "False"}]}; - const actual = definition.coerce(source, undefined, undefined, new Filter("test.name pr")); - const expected = {test: [{name: "Test"}]}; - assert.deepStrictEqual({...actual}, expected, - "Instance method 'coerce' did not positively filter complex multi-valued attributes"); + assert.deepStrictEqual({...definition.coerce(source, undefined, undefined, new Filter(filter))}, expected, + `Instance method 'coerce' ${unexpected} ${target} not specified for filter`); }); - it("should expect complex multi-valued attributes to be filtered negatively", () => { - const attributes = [new Attribute("complex", "test", {multiValued: true}, [new Attribute("string", "name"), new Attribute("string", "value")])]; - const definition = new SchemaDefinition(...Object.values(params), "Test Schema", attributes).truncate(["schemas", "meta"]); - const source = {test: [{name: "Test", value: "Test"}, {value: "False"}]}; - const actual = definition.coerce(source, undefined, undefined, new Filter("test.name np")); - const expected = {test: [{value: "Test"}, {value: "False"}]}; - - assert.deepStrictEqual({...actual}, expected, - "Instance method 'coerce' did not negatively filter complex multi-valued attributes"); - }); - - for (let [target, outcome, filter, unexpected, expected] of [ - ["namespaced attributes", "present in coerced result", - "employeeNumber np", "unexpectedly excluded", - {costCenter: "Test", [extensionId]: {employeeNumber: "1234", costCenter: "Test"}}], - ["namespaced attributes", "filtered positively", - `${extensionId}:employeeNumber pr`, "unexpectedly included", - {[extensionId]: {employeeNumber: "1234"}}], - ["namespaced attributes", "filtered negatively", - `${extensionId}:employeeNumber np`, "unexpectedly excluded", - {employeeNumber: "Test", costCenter: "Test", [extensionId]: {costCenter: "Test"}}], - ["extension namespaces", "filtered positively", - `${extensionId} pr`, "unexpectedly included", - {[extensionId]: {employeeNumber: "1234", costCenter: "Test"}}], - ["extension namespaces", "filtered negatively", - `${extensionId} np`, "unexpectedly excluded", - {employeeNumber: "Test", costCenter: "Test"}], - ["direct and namespaced attributes", "filtered positively", - `costCenter pr and ${extensionId}:employeeNumber pr`, "unexpectedly included", - {costCenter: "Test", [extensionId]: {employeeNumber: "1234"}}], - ["direct and namespaced attributes", "filtered negatively", - `costCenter np and ${extensionId}:employeeNumber np`, "unexpectedly excluded", - {employeeNumber: "Test", [extensionId]: {costCenter: "Test"}}] + for (let [target, outcome, unexpected, expected, filter] of [ + ["namespaced attributes", "present in coerced result", "unexpectedly excluded", {costCenter: "Test", [extensionId]: {employeeNumber: "1234", costCenter: "Test"}}, "employeeNumber np"], + ["namespaced attributes", "filtered positively", "unexpectedly included", {[extensionId]: {employeeNumber: "1234"}}, `${extensionId}:employeeNumber pr`], + ["namespaced attributes", "filtered negatively", "unexpectedly excluded", {employeeNumber: "Test", costCenter: "Test", [extensionId]: {costCenter: "Test"}}, `${extensionId}:employeeNumber np`], + ["extension namespaces", "filtered positively", "unexpectedly included", {[extensionId]: {employeeNumber: "1234", costCenter: "Test"}}, `${extensionId} pr`], + ["extension namespaces", "filtered negatively", "unexpectedly excluded", {employeeNumber: "Test", costCenter: "Test"}, `${extensionId} np`], + ["direct and namespaced attributes", "filtered positively", "unexpectedly included", {costCenter: "Test", [extensionId]: {employeeNumber: "1234"}}, `costCenter pr and ${extensionId}:employeeNumber pr`], + ["direct and namespaced attributes", "filtered negatively", "unexpectedly excluded", {employeeNumber: "Test", [extensionId]: {costCenter: "Test"}}, `costCenter np and ${extensionId}:employeeNumber np`] ]) it(`should expect ${target} to be ${outcome}`, () => { const source = {employeeNumber: "Test", costCenter: "Test", [`${extensionId}:employeeNumber`]: "1234", [`${extensionId}:costCenter`]: "Test"}; const attributes = [new Attribute("string", "employeeNumber"), new Attribute("string", "costCenter")];