From 8b827468cbe5a1248f67142d071fe1c7738c800b Mon Sep 17 00:00:00 2001 From: Avery Anderson Date: Mon, 4 Aug 2025 16:45:41 -0400 Subject: [PATCH 1/7] test: add comprehensive JsonNodeMapper tests and fix fqdn creation for top-level arrays --- .../frameworks/mapper/JsonNodeMapper.kt | 56 +++- .../frameworks/mapper/JsonNodeMapperTest.kt | 308 ++++++++++++++++++ 2 files changed, 354 insertions(+), 10 deletions(-) create mode 100644 client/src/test/kotlin/com/lowes/auditor/client/infrastructure/frameworks/mapper/JsonNodeMapperTest.kt diff --git a/client/src/main/kotlin/com/lowes/auditor/client/infrastructure/frameworks/mapper/JsonNodeMapper.kt b/client/src/main/kotlin/com/lowes/auditor/client/infrastructure/frameworks/mapper/JsonNodeMapper.kt index d41416c..af90631 100644 --- a/client/src/main/kotlin/com/lowes/auditor/client/infrastructure/frameworks/mapper/JsonNodeMapper.kt +++ b/client/src/main/kotlin/com/lowes/auditor/client/infrastructure/frameworks/mapper/JsonNodeMapper.kt @@ -26,7 +26,42 @@ object JsonNodeMapper { fqcn: String, ): Flux { val hasFields = node.fields().hasNext() - return Flux.fromIterable(getIterables(hasFields, node)) + + // Special handling for top-level arrays + if (!hasFields && node.isArray) { + return Flux.fromIterable(node) + .index() + .flatMap { tuple -> + val index = tuple.t1 + val arrayItem = tuple.t2 + val elementFqcn = "$fqcn.$index" + + if (arrayItem.isValueNode) { + val updatedValue = if (eventType == EventType.CREATED) getValue(NodeType.TEXT, arrayItem) else null + val previousValue = if (eventType == EventType.DELETED) getValue(NodeType.TEXT, arrayItem) else null + + Flux.just( + Element( + name = index.toString(), + updatedValue = updatedValue, + previousValue = previousValue, + metadata = ElementMetadata(fqdn = elementFqcn), + ), + ) + } else { + toElement(arrayItem, eventType, elementFqcn) + } + } + } + + val iterables = + if (hasFields) { + node.fields().asSequence().toList() + } else { + node.toList().flatMap { it.fields().asSequence().toList() } + } + + return Flux.fromIterable(iterables) .index() .flatMap { indexEntryPair -> val index = indexEntryPair.t1 @@ -34,20 +69,24 @@ object JsonNodeMapper { findType(entry.value) ?.let { if (it == NodeType.ARRAY) { + val parentFqcn = getFqcnValue(hasFields, index, fqcn, entry) Flux.fromIterable(entry.value) .index() .flatMap { t -> - val fqcnValue = getFqcnValue(hasFields, t.t1, fqcn, entry).plus(".").plus(t.t1) - if (t.t2.isValueNode) { + val arrayIdx = t.t1 + val arrayItem = t.t2 + val elementFqcn = "$parentFqcn.$arrayIdx" + + if (arrayItem.isValueNode) { val updatedValue = if (eventType == EventType.CREATED) { - findType(t.t2)?.let { it1 -> getValue(it1, t.t2) } + findType(arrayItem)?.let { it1 -> getValue(it1, arrayItem) } } else { null } val previousValue = if (eventType == EventType.DELETED) { - findType(t.t2)?.let { it1 -> getValue(it1, t.t2) } + findType(arrayItem)?.let { it1 -> getValue(it1, arrayItem) } } else { null } @@ -56,14 +95,11 @@ object JsonNodeMapper { name = entry.key, updatedValue = updatedValue, previousValue = previousValue, - metadata = - ElementMetadata( - fqdn = fqcnValue, - ), + metadata = ElementMetadata(fqdn = elementFqcn), ), ) } else { - toElement(t.t2, eventType, fqcnValue) + toElement(arrayItem, eventType, elementFqcn) } } } else if (it == NodeType.OBJECT) { diff --git a/client/src/test/kotlin/com/lowes/auditor/client/infrastructure/frameworks/mapper/JsonNodeMapperTest.kt b/client/src/test/kotlin/com/lowes/auditor/client/infrastructure/frameworks/mapper/JsonNodeMapperTest.kt new file mode 100644 index 0000000..451eaad --- /dev/null +++ b/client/src/test/kotlin/com/lowes/auditor/client/infrastructure/frameworks/mapper/JsonNodeMapperTest.kt @@ -0,0 +1,308 @@ +package com.lowes.auditor.client.infrastructure.frameworks.mapper + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.node.ArrayNode +import com.fasterxml.jackson.databind.node.JsonNodeFactory +import com.fasterxml.jackson.databind.node.ObjectNode +import com.lowes.auditor.core.entities.domain.Element +import com.lowes.auditor.core.entities.domain.ElementMetadata +import com.lowes.auditor.core.entities.domain.EventType +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.matchers.string.shouldStartWith + +/** + * Unit Tests for [JsonNodeMapper] + */ +class JsonNodeMapperTest : BehaviorSpec({ + val objectMapper = ObjectMapper() + val jsonNodeFactory = JsonNodeFactory.instance + + Given("A JsonNodeMapper and a simple JSON object") { + val simpleJson = + """ + { + "id": 1, + "name": "Test", + "active": true + } + """.trimIndent() + + val jsonNode = objectMapper.readTree(simpleJson) + val fqcn = "com.example.Test" + + When("converting to elements with CREATED event type") { + val elements = + JsonNodeMapper.toElement(jsonNode, EventType.CREATED, fqcn) + .collectList() + .block() + + Then("should create elements with correct FQDNs and values") { + elements?.size shouldBe 3 + + val expectedElements = + listOf( + Element( + name = "id", + updatedValue = "1", + previousValue = null, + metadata = ElementMetadata(fqdn = "$fqcn.id"), + ), + Element( + name = "name", + updatedValue = "Test", + previousValue = null, + metadata = ElementMetadata(fqdn = "$fqcn.name"), + ), + Element( + name = "active", + updatedValue = "true", + previousValue = null, + metadata = ElementMetadata(fqdn = "$fqcn.active"), + ), + ) + + elements?.forEach { element -> + expectedElements.first { it.name == element.name }.let { expected -> + element.updatedValue shouldBe expected.updatedValue + element.metadata?.fqdn shouldBe expected.metadata?.fqdn + } + } + } + } + + When("converting to elements with DELETED event type") { + val elements = + JsonNodeMapper.toElement(jsonNode, EventType.DELETED, fqcn) + .collectList() + .block() + + Then("should set previousValue instead of updatedValue") { + elements?.forEach { element -> + element.updatedValue shouldBe null + element.previousValue shouldNotBe null + } + } + } + } + + Given("A JsonNodeMapper and a nested JSON object") { + val nestedJson = + """ + { + "id": 1, + "name": "Test", + "address": { + "street": "123 Main St", + "city": "Anytown", + "zip": "12345" + } + } + """.trimIndent() + + val jsonNode = objectMapper.readTree(nestedJson) + val fqcn = "com.example.User" + + When("converting to elements") { + val elements = + JsonNodeMapper.toElement(jsonNode, EventType.CREATED, fqcn) + .collectList() + .block() + + Then("should handle nested objects correctly") { + elements?.size shouldBe 5 // id, name, address.street, address.city, address.zip + + val addressElements = elements?.filter { it.name == "street" || it.name == "city" || it.name == "zip" } + addressElements?.size shouldBe 3 + + addressElements?.forEach { element -> + element.metadata?.fqdn shouldStartWith "$fqcn.address" + } + } + } + } + + Given("A JsonNodeMapper and a JSON array") { + val arrayJson = + """ + [ + {"id": 1, "name": "Item 1"}, + {"id": 2, "name": "Item 2"} + ] + """.trimIndent() + + val jsonNode = objectMapper.readTree(arrayJson) + val fqcn = "com.example.Items" + + When("converting array to elements") { + val elements = + JsonNodeMapper.toElement(jsonNode, EventType.CREATED, fqcn) + .collectList() + .block() + + Then("should create elements with array indices in FQDN") { + elements?.size shouldBe 4 // 2 items * 2 fields each + println(elements?.toString()) + elements?.find { it.name == "id" && it.metadata?.fqdn == "$fqcn.0.id" }?.updatedValue shouldBe "1" + elements?.find { it.name == "name" && it.metadata?.fqdn == "$fqcn.0.name" }?.updatedValue shouldBe "Item 1" + elements?.find { it.name == "id" && it.metadata?.fqdn == "$fqcn.1.id" }?.updatedValue shouldBe "2" + elements?.find { it.name == "name" && it.metadata?.fqdn == "$fqcn.1.name" }?.updatedValue shouldBe "Item 2" + } + } + } + + Given("A JsonNodeMapper and a complex nested JSON object") { + val complexJson = + """ + { + "id": 1, + "name": "Test User", + "addresses": [ + { + "type": "home", + "street": "123 Main St", + "city": "Anytown" + }, + { + "type": "work", + "street": "456 Business Ave", + "city": "Businesstown" + } + ], + "preferences": { + "notifications": true, + "theme": "dark", + "favoriteCategories": ["tech", "books", "music"] + } + } + """.trimIndent() + + val jsonNode = objectMapper.readTree(complexJson) + val fqcn = "com.example.UserProfile" + + When("converting complex object to elements") { + val elements = + JsonNodeMapper.toElement(jsonNode, EventType.CREATED, fqcn) + .collectList() + .block() + + Then("should handle all nested structures correctly") { + // Top level fields + elements?.find { it.metadata?.fqdn == "$fqcn.id" }?.updatedValue shouldBe "1" + elements?.find { it.metadata?.fqdn == "$fqcn.name" }?.updatedValue shouldBe "Test User" + + // Nested array of objects (addresses) + elements?.find { it.metadata?.fqdn == "$fqcn.addresses.0.type" }?.updatedValue shouldBe "home" + elements?.find { it.metadata?.fqdn == "$fqcn.addresses.0.street" }?.updatedValue shouldBe "123 Main St" + elements?.find { it.metadata?.fqdn == "$fqcn.addresses.1.type" }?.updatedValue shouldBe "work" + elements?.find { it.metadata?.fqdn == "$fqcn.addresses.1.city" }?.updatedValue shouldBe "Businesstown" + + // Nested object with array (preferences.favoriteCategories) + elements?.find { it.metadata?.fqdn == "$fqcn.preferences.notifications" }?.updatedValue shouldBe "true" + elements?.find { it.metadata?.fqdn == "$fqcn.preferences.theme" }?.updatedValue shouldBe "dark" + + // Array handling within nested object + val categoryElements = elements?.filter { it.name == "favoriteCategories" } + categoryElements?.size shouldBe 3 + categoryElements?.map { it.updatedValue }?.toSet() shouldBe setOf("tech", "books", "music") + } + } + } + + Given("A JsonNodeMapper and edge case JSON values") { + val edgeCaseJson = + """ + { + "nullValue": null, + "emptyString": "", + "zero": 0, + "falseValue": false, + "emptyObject": {}, + "emptyArray": [] + } + """.trimIndent() + + val jsonNode = objectMapper.readTree(edgeCaseJson) + val fqcn = "com.example.EdgeCases" + + When("converting edge case values to elements") { + val elements = + JsonNodeMapper.toElement(jsonNode, EventType.CREATED, fqcn) + .collectList() + .block() + + Then("should handle all edge cases correctly") { + elements?.find { it.name == "nullValue" }?.updatedValue shouldBe "null" + elements?.find { it.name == "emptyString" }?.updatedValue shouldBe "" + elements?.find { it.name == "zero" }?.updatedValue shouldBe "0" + elements?.find { it.name == "falseValue" }?.updatedValue shouldBe "false" + elements?.find { it.name == "emptyObject" } shouldBe null // Empty objects should be filtered out + elements?.find { it.name == "emptyArray" } shouldBe null // Empty arrays should be filtered out + } + } + } + + Given("A JsonNodeMapper and a programmatically created complex object") { + val rootNode: ObjectNode = jsonNodeFactory.objectNode() + val addressArray: ArrayNode = jsonNodeFactory.arrayNode() + + // Create address 1 + val address1 = jsonNodeFactory.objectNode() + address1.put("type", "home") + address1.put("street", "123 Main St") + address1.put("city", "Anytown") + + // Create address 2 + val address2 = jsonNodeFactory.objectNode() + address2.put("type", "work") + address2.put("street", "456 Business Ave") + address2.put("city", "Businesstown") + + // Add addresses to array + addressArray.add(address1) + addressArray.add(address2) + + // Create preferences + val preferences = jsonNodeFactory.objectNode() + preferences.put("notifications", true) + preferences.put("theme", "dark") + + val favoriteCategories = jsonNodeFactory.arrayNode() + favoriteCategories.add("tech") + favoriteCategories.add("books") + favoriteCategories.add("music") + preferences.set("favoriteCategories", favoriteCategories) + + // Build root object + rootNode.put("id", 1) + rootNode.put("name", "Test User") + rootNode.set("addresses", addressArray) + rootNode.set("preferences", preferences) + + val fqcn = "com.example.ProgrammaticUser" + + When("converting programmatically created complex object to elements") { + val elements = + JsonNodeMapper.toElement(rootNode, EventType.CREATED, fqcn) + .collectList() + .block() + + Then("should handle all programmatic structures correctly") { + // Basic fields + elements?.find { it.metadata?.fqdn == "$fqcn.id" }?.updatedValue shouldBe "1" + elements?.find { it.metadata?.fqdn == "$fqcn.name" }?.updatedValue shouldBe "Test User" + + // Nested arrays and objects + elements?.find { it.metadata?.fqdn == "$fqcn.addresses.0.type" }?.updatedValue shouldBe "home" + elements?.find { it.metadata?.fqdn == "$fqcn.addresses.1.street" }?.updatedValue shouldBe "456 Business Ave" + elements?.find { it.metadata?.fqdn == "$fqcn.preferences.theme" }?.updatedValue shouldBe "dark" + + // Array within nested object + val categoryElements = elements?.filter { it.name == "favoriteCategories" }?.map { it.updatedValue } + categoryElements shouldContainExactlyInAnyOrder listOf("tech", "books", "music") + } + } + } +}) From 9ac6b337d825eb74dcb5ebf637b08d4b32e6667b Mon Sep 17 00:00:00 2001 From: Avery Anderson Date: Tue, 5 Aug 2025 14:40:55 -0400 Subject: [PATCH 2/7] refactor: simplify JsonNodeMapper with helper methods and improve test organization --- .../frameworks/mapper/JsonNodeMapper.kt | 130 ++++--------- .../frameworks/mapper/JsonNodeMapperTest.kt | 176 +++++++----------- 2 files changed, 111 insertions(+), 195 deletions(-) diff --git a/client/src/main/kotlin/com/lowes/auditor/client/infrastructure/frameworks/mapper/JsonNodeMapper.kt b/client/src/main/kotlin/com/lowes/auditor/client/infrastructure/frameworks/mapper/JsonNodeMapper.kt index af90631..7cdf9cb 100644 --- a/client/src/main/kotlin/com/lowes/auditor/client/infrastructure/frameworks/mapper/JsonNodeMapper.kt +++ b/client/src/main/kotlin/com/lowes/auditor/client/infrastructure/frameworks/mapper/JsonNodeMapper.kt @@ -35,102 +35,54 @@ object JsonNodeMapper { val index = tuple.t1 val arrayItem = tuple.t2 val elementFqcn = "$fqcn.$index" + createElementForNode(arrayItem, index.toString(), eventType, elementFqcn) + } + } - if (arrayItem.isValueNode) { - val updatedValue = if (eventType == EventType.CREATED) getValue(NodeType.TEXT, arrayItem) else null - val previousValue = if (eventType == EventType.DELETED) getValue(NodeType.TEXT, arrayItem) else null + return Flux.fromIterable(getIterables(hasFields, node)) + .index() + .flatMap { tuple -> + val index = tuple.t1 + val entry = tuple.t2 + val elementFqcn = getFqcnValue(hasFields, index, fqcn, entry) + val nodeType = findType(entry.value) ?: return@flatMap Flux.empty() - Flux.just( - Element( - name = index.toString(), - updatedValue = updatedValue, - previousValue = previousValue, - metadata = ElementMetadata(fqdn = elementFqcn), - ), - ) - } else { - toElement(arrayItem, eventType, elementFqcn) + when (nodeType) { + NodeType.ARRAY -> { + Flux.fromIterable(entry.value) + .index() + .flatMap { arrayTuple -> + val arrayIdx = arrayTuple.t1 + val arrayItem = arrayTuple.t2 + val arrayElementFqcn = "$elementFqcn.$arrayIdx" + createElementForNode(arrayItem, entry.key, eventType, arrayElementFqcn) + } } + NodeType.OBJECT -> toElement(entry.value, eventType, elementFqcn) + else -> createElementForNode(entry.value, entry.key, eventType, elementFqcn, nodeType) } - } - - val iterables = - if (hasFields) { - node.fields().asSequence().toList() - } else { - node.toList().flatMap { it.fields().asSequence().toList() } } + } - return Flux.fromIterable(iterables) - .index() - .flatMap { indexEntryPair -> - val index = indexEntryPair.t1 - val entry = indexEntryPair.t2 - findType(entry.value) - ?.let { - if (it == NodeType.ARRAY) { - val parentFqcn = getFqcnValue(hasFields, index, fqcn, entry) - Flux.fromIterable(entry.value) - .index() - .flatMap { t -> - val arrayIdx = t.t1 - val arrayItem = t.t2 - val elementFqcn = "$parentFqcn.$arrayIdx" + /** + * Creates an Element for a node with the given parameters. + */ + private fun createElementForNode( + node: JsonNode, + name: String, + eventType: EventType, + fqdn: String, + nodeType: NodeType? = findType(node), + ): Flux { + if (nodeType == null) return Flux.empty() + val updatedValue = if (eventType == EventType.CREATED) getValue(nodeType, node) else null + val previousValue = if (eventType == EventType.DELETED) getValue(nodeType, node) else null - if (arrayItem.isValueNode) { - val updatedValue = - if (eventType == EventType.CREATED) { - findType(arrayItem)?.let { it1 -> getValue(it1, arrayItem) } - } else { - null - } - val previousValue = - if (eventType == EventType.DELETED) { - findType(arrayItem)?.let { it1 -> getValue(it1, arrayItem) } - } else { - null - } - Flux.just( - Element( - name = entry.key, - updatedValue = updatedValue, - previousValue = previousValue, - metadata = ElementMetadata(fqdn = elementFqcn), - ), - ) - } else { - toElement(arrayItem, eventType, elementFqcn) - } - } - } else if (it == NodeType.OBJECT) { - toElement(entry.value, eventType, getFqcnValue(hasFields, index, fqcn, entry)) - } else { - val updatedValue = - if (eventType == EventType.CREATED) { - getValue(it, entry.value) - } else { - null - } - val previousValue = - if (eventType == EventType.DELETED) { - getValue(it, entry.value) - } else { - null - } - Flux.just( - Element( - name = entry.key, - updatedValue = updatedValue, - previousValue = previousValue, - metadata = - ElementMetadata( - fqdn = getFqcnValue(hasFields, index, fqcn, entry), - ), - ), - ) - } - } ?: Flux.empty() - } + return if (nodeType == NodeType.ARRAY || nodeType == NodeType.OBJECT) { + toElement(node, eventType, fqdn) + } else { + Flux.just(Element(name, updatedValue, previousValue, ElementMetadata(fqdn = fqdn))) + } } /** diff --git a/client/src/test/kotlin/com/lowes/auditor/client/infrastructure/frameworks/mapper/JsonNodeMapperTest.kt b/client/src/test/kotlin/com/lowes/auditor/client/infrastructure/frameworks/mapper/JsonNodeMapperTest.kt index 451eaad..e444551 100644 --- a/client/src/test/kotlin/com/lowes/auditor/client/infrastructure/frameworks/mapper/JsonNodeMapperTest.kt +++ b/client/src/test/kotlin/com/lowes/auditor/client/infrastructure/frameworks/mapper/JsonNodeMapperTest.kt @@ -1,14 +1,11 @@ package com.lowes.auditor.client.infrastructure.frameworks.mapper import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.node.ArrayNode -import com.fasterxml.jackson.databind.node.JsonNodeFactory -import com.fasterxml.jackson.databind.node.ObjectNode import com.lowes.auditor.core.entities.domain.Element import com.lowes.auditor.core.entities.domain.ElementMetadata import com.lowes.auditor.core.entities.domain.EventType import io.kotest.core.spec.style.BehaviorSpec -import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder +import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe import io.kotest.matchers.string.shouldStartWith @@ -18,9 +15,8 @@ import io.kotest.matchers.string.shouldStartWith */ class JsonNodeMapperTest : BehaviorSpec({ val objectMapper = ObjectMapper() - val jsonNodeFactory = JsonNodeFactory.instance - Given("A JsonNodeMapper and a simple JSON object") { + Given("a simple JSON object with primitive values") { val simpleJson = """ { @@ -88,7 +84,7 @@ class JsonNodeMapperTest : BehaviorSpec({ } } - Given("A JsonNodeMapper and a nested JSON object") { + Given("a JSON object with nested objects") { val nestedJson = """ { @@ -124,36 +120,7 @@ class JsonNodeMapperTest : BehaviorSpec({ } } - Given("A JsonNodeMapper and a JSON array") { - val arrayJson = - """ - [ - {"id": 1, "name": "Item 1"}, - {"id": 2, "name": "Item 2"} - ] - """.trimIndent() - - val jsonNode = objectMapper.readTree(arrayJson) - val fqcn = "com.example.Items" - - When("converting array to elements") { - val elements = - JsonNodeMapper.toElement(jsonNode, EventType.CREATED, fqcn) - .collectList() - .block() - - Then("should create elements with array indices in FQDN") { - elements?.size shouldBe 4 // 2 items * 2 fields each - println(elements?.toString()) - elements?.find { it.name == "id" && it.metadata?.fqdn == "$fqcn.0.id" }?.updatedValue shouldBe "1" - elements?.find { it.name == "name" && it.metadata?.fqdn == "$fqcn.0.name" }?.updatedValue shouldBe "Item 1" - elements?.find { it.name == "id" && it.metadata?.fqdn == "$fqcn.1.id" }?.updatedValue shouldBe "2" - elements?.find { it.name == "name" && it.metadata?.fqdn == "$fqcn.1.name" }?.updatedValue shouldBe "Item 2" - } - } - } - - Given("A JsonNodeMapper and a complex nested JSON object") { + Given("a complex JSON object with arrays and nested objects") { val complexJson = """ { @@ -189,21 +156,21 @@ class JsonNodeMapperTest : BehaviorSpec({ .block() Then("should handle all nested structures correctly") { - // Top level fields + // Verify top-level primitive fields are correctly mapped elements?.find { it.metadata?.fqdn == "$fqcn.id" }?.updatedValue shouldBe "1" elements?.find { it.metadata?.fqdn == "$fqcn.name" }?.updatedValue shouldBe "Test User" - // Nested array of objects (addresses) + // Verify nested array of objects with proper FQDN construction elements?.find { it.metadata?.fqdn == "$fqcn.addresses.0.type" }?.updatedValue shouldBe "home" elements?.find { it.metadata?.fqdn == "$fqcn.addresses.0.street" }?.updatedValue shouldBe "123 Main St" elements?.find { it.metadata?.fqdn == "$fqcn.addresses.1.type" }?.updatedValue shouldBe "work" elements?.find { it.metadata?.fqdn == "$fqcn.addresses.1.city" }?.updatedValue shouldBe "Businesstown" - // Nested object with array (preferences.favoriteCategories) + // Verify nested object with array field elements?.find { it.metadata?.fqdn == "$fqcn.preferences.notifications" }?.updatedValue shouldBe "true" elements?.find { it.metadata?.fqdn == "$fqcn.preferences.theme" }?.updatedValue shouldBe "dark" - // Array handling within nested object + // Verify array values within nested object are correctly flattened val categoryElements = elements?.filter { it.name == "favoriteCategories" } categoryElements?.size shouldBe 3 categoryElements?.map { it.updatedValue }?.toSet() shouldBe setOf("tech", "books", "music") @@ -211,7 +178,65 @@ class JsonNodeMapperTest : BehaviorSpec({ } } - Given("A JsonNodeMapper and edge case JSON values") { + Given("a JSON object with various array structures") { + val nestedArrayJson = + """ + { + "matrix": [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9] + ], + "nested": { + "deepArray": [ + [ + {"id": 1, "values": ["a", "b"]}, + {"id": 2, "values": ["c", "d"]} + ], + [ + {"id": 3, "values": ["e", "f"]} + ] + ] + }, + "simpleArray": [ + {"id": 1, "name": "Item 1"}, + {"id": 2, "name": "Item 2"} + ] + } + """.trimIndent() + + val jsonNode = objectMapper.readTree(nestedArrayJson) + val fqcn = "com.example.NestedArrays" + + When("converting nested arrays to elements") { + val elements = + JsonNodeMapper.toElement(jsonNode, EventType.CREATED, fqcn) + .collectList() + .block() + + Then("should handle all levels of nested arrays correctly") { + elements.shouldNotBeNull() + + // Verify 2D array access with proper indices + elements.find { it.metadata?.fqdn == "$fqcn.matrix.0.0" }?.updatedValue shouldBe "1" + elements.find { it.metadata?.fqdn == "$fqcn.matrix.1.2" }?.updatedValue shouldBe "6" + elements.find { it.metadata?.fqdn == "$fqcn.matrix.2.1" }?.updatedValue shouldBe "8" + + // Verify deeply nested arrays with objects and proper FQDN construction + elements.find { it.metadata?.fqdn == "$fqcn.nested.deepArray.0.0.id" }?.updatedValue shouldBe "1" + elements.find { it.metadata?.fqdn == "$fqcn.nested.deepArray.0.1.values.1" }?.updatedValue shouldBe "d" + elements.find { it.metadata?.fqdn == "$fqcn.nested.deepArray.1.0.values.0" }?.updatedValue shouldBe "e" + + // Verify simple array of objects with proper FQDN construction + elements.find { it.metadata?.fqdn == "$fqcn.simpleArray.0.id" }?.updatedValue shouldBe "1" + elements.find { it.metadata?.fqdn == "$fqcn.simpleArray.0.name" }?.updatedValue shouldBe "Item 1" + elements.find { it.metadata?.fqdn == "$fqcn.simpleArray.1.id" }?.updatedValue shouldBe "2" + elements.find { it.metadata?.fqdn == "$fqcn.simpleArray.1.name" }?.updatedValue shouldBe "Item 2" + } + } + } + + Given("a JSON object with edge case values") { val edgeCaseJson = """ { @@ -238,70 +263,9 @@ class JsonNodeMapperTest : BehaviorSpec({ elements?.find { it.name == "emptyString" }?.updatedValue shouldBe "" elements?.find { it.name == "zero" }?.updatedValue shouldBe "0" elements?.find { it.name == "falseValue" }?.updatedValue shouldBe "false" - elements?.find { it.name == "emptyObject" } shouldBe null // Empty objects should be filtered out - elements?.find { it.name == "emptyArray" } shouldBe null // Empty arrays should be filtered out - } - } - } - - Given("A JsonNodeMapper and a programmatically created complex object") { - val rootNode: ObjectNode = jsonNodeFactory.objectNode() - val addressArray: ArrayNode = jsonNodeFactory.arrayNode() - - // Create address 1 - val address1 = jsonNodeFactory.objectNode() - address1.put("type", "home") - address1.put("street", "123 Main St") - address1.put("city", "Anytown") - - // Create address 2 - val address2 = jsonNodeFactory.objectNode() - address2.put("type", "work") - address2.put("street", "456 Business Ave") - address2.put("city", "Businesstown") - - // Add addresses to array - addressArray.add(address1) - addressArray.add(address2) - - // Create preferences - val preferences = jsonNodeFactory.objectNode() - preferences.put("notifications", true) - preferences.put("theme", "dark") - - val favoriteCategories = jsonNodeFactory.arrayNode() - favoriteCategories.add("tech") - favoriteCategories.add("books") - favoriteCategories.add("music") - preferences.set("favoriteCategories", favoriteCategories) - - // Build root object - rootNode.put("id", 1) - rootNode.put("name", "Test User") - rootNode.set("addresses", addressArray) - rootNode.set("preferences", preferences) - - val fqcn = "com.example.ProgrammaticUser" - - When("converting programmatically created complex object to elements") { - val elements = - JsonNodeMapper.toElement(rootNode, EventType.CREATED, fqcn) - .collectList() - .block() - - Then("should handle all programmatic structures correctly") { - // Basic fields - elements?.find { it.metadata?.fqdn == "$fqcn.id" }?.updatedValue shouldBe "1" - elements?.find { it.metadata?.fqdn == "$fqcn.name" }?.updatedValue shouldBe "Test User" - - // Nested arrays and objects - elements?.find { it.metadata?.fqdn == "$fqcn.addresses.0.type" }?.updatedValue shouldBe "home" - elements?.find { it.metadata?.fqdn == "$fqcn.addresses.1.street" }?.updatedValue shouldBe "456 Business Ave" - elements?.find { it.metadata?.fqdn == "$fqcn.preferences.theme" }?.updatedValue shouldBe "dark" - - // Array within nested object - val categoryElements = elements?.filter { it.name == "favoriteCategories" }?.map { it.updatedValue } - categoryElements shouldContainExactlyInAnyOrder listOf("tech", "books", "music") + // Verify empty objects and arrays are filtered out + elements?.find { it.name == "emptyObject" } shouldBe null + elements?.find { it.name == "emptyArray" } shouldBe null } } } From 4df3e51ad0604fbc6fcdb80d1afd69064203dc54 Mon Sep 17 00:00:00 2001 From: Avery Anderson Date: Thu, 21 Aug 2025 15:13:59 -0400 Subject: [PATCH 3/7] refactor: improve JsonNodeMapper test readability with external test data files, fix inconsistent casing and typos in some existing resource file names --- .../frameworks/mapper/JsonNodeMapper.kt | 30 +-- .../frameworks/mapper/JsonNodeMapperTest.kt | 167 ++++++++-------- .../service/ObjectDiffCheckerServiceTest.kt | 12 +- client/src/test/resources/complexCreated.json | 106 +++++++++++ .../src/test/resources/edgeCaseCreated.json | 34 ++++ ...pInnerpUpdate.json => mapInnerUpdate.json} | 0 .../test/resources/nestedArrayCreated.json | 178 ++++++++++++++++++ client/src/test/resources/nestedCreated.json | 42 +++++ client/src/test/resources/simpleCreated.json | 26 +++ client/src/test/resources/simpleDeleted.json | 26 +++ .../resources/{udpate.json => update.json} | 0 11 files changed, 513 insertions(+), 108 deletions(-) create mode 100644 client/src/test/resources/complexCreated.json create mode 100644 client/src/test/resources/edgeCaseCreated.json rename client/src/test/resources/{mapInnerpUpdate.json => mapInnerUpdate.json} (100%) create mode 100644 client/src/test/resources/nestedArrayCreated.json create mode 100644 client/src/test/resources/nestedCreated.json create mode 100644 client/src/test/resources/simpleCreated.json create mode 100644 client/src/test/resources/simpleDeleted.json rename client/src/test/resources/{udpate.json => update.json} (100%) diff --git a/client/src/main/kotlin/com/lowes/auditor/client/infrastructure/frameworks/mapper/JsonNodeMapper.kt b/client/src/main/kotlin/com/lowes/auditor/client/infrastructure/frameworks/mapper/JsonNodeMapper.kt index 7cdf9cb..c05f7ad 100644 --- a/client/src/main/kotlin/com/lowes/auditor/client/infrastructure/frameworks/mapper/JsonNodeMapper.kt +++ b/client/src/main/kotlin/com/lowes/auditor/client/infrastructure/frameworks/mapper/JsonNodeMapper.kt @@ -45,22 +45,24 @@ object JsonNodeMapper { val index = tuple.t1 val entry = tuple.t2 val elementFqcn = getFqcnValue(hasFields, index, fqcn, entry) - val nodeType = findType(entry.value) ?: return@flatMap Flux.empty() - when (nodeType) { - NodeType.ARRAY -> { - Flux.fromIterable(entry.value) - .index() - .flatMap { arrayTuple -> - val arrayIdx = arrayTuple.t1 - val arrayItem = arrayTuple.t2 - val arrayElementFqcn = "$elementFqcn.$arrayIdx" - createElementForNode(arrayItem, entry.key, eventType, arrayElementFqcn) - } + // Process only entries with valid node types + findType(entry.value)?.let { nodeType -> + when (nodeType) { + NodeType.ARRAY -> { + Flux.fromIterable(entry.value) + .index() + .flatMap { arrayTuple -> + val arrayIdx = arrayTuple.t1 + val arrayItem = arrayTuple.t2 + val arrayElementFqcn = "$elementFqcn.$arrayIdx" + createElementForNode(arrayItem, entry.key, eventType, arrayElementFqcn) + } + } + NodeType.OBJECT -> toElement(entry.value, eventType, elementFqcn) + else -> createElementForNode(entry.value, entry.key, eventType, elementFqcn, nodeType) } - NodeType.OBJECT -> toElement(entry.value, eventType, elementFqcn) - else -> createElementForNode(entry.value, entry.key, eventType, elementFqcn, nodeType) - } + } ?: Flux.empty() } } diff --git a/client/src/test/kotlin/com/lowes/auditor/client/infrastructure/frameworks/mapper/JsonNodeMapperTest.kt b/client/src/test/kotlin/com/lowes/auditor/client/infrastructure/frameworks/mapper/JsonNodeMapperTest.kt index e444551..5be8a28 100644 --- a/client/src/test/kotlin/com/lowes/auditor/client/infrastructure/frameworks/mapper/JsonNodeMapperTest.kt +++ b/client/src/test/kotlin/com/lowes/auditor/client/infrastructure/frameworks/mapper/JsonNodeMapperTest.kt @@ -2,19 +2,17 @@ package com.lowes.auditor.client.infrastructure.frameworks.mapper import com.fasterxml.jackson.databind.ObjectMapper import com.lowes.auditor.core.entities.domain.Element -import com.lowes.auditor.core.entities.domain.ElementMetadata import com.lowes.auditor.core.entities.domain.EventType import io.kotest.core.spec.style.BehaviorSpec import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe -import io.kotest.matchers.shouldNotBe -import io.kotest.matchers.string.shouldStartWith /** * Unit Tests for [JsonNodeMapper] */ class JsonNodeMapperTest : BehaviorSpec({ val objectMapper = ObjectMapper() + val fqcn = "com.example.Test" Given("a simple JSON object with primitive values") { val simpleJson = @@ -27,7 +25,6 @@ class JsonNodeMapperTest : BehaviorSpec({ """.trimIndent() val jsonNode = objectMapper.readTree(simpleJson) - val fqcn = "com.example.Test" When("converting to elements with CREATED event type") { val elements = @@ -36,36 +33,19 @@ class JsonNodeMapperTest : BehaviorSpec({ .block() Then("should create elements with correct FQDNs and values") { - elements?.size shouldBe 3 - - val expectedElements = - listOf( - Element( - name = "id", - updatedValue = "1", - previousValue = null, - metadata = ElementMetadata(fqdn = "$fqcn.id"), - ), - Element( - name = "name", - updatedValue = "Test", - previousValue = null, - metadata = ElementMetadata(fqdn = "$fqcn.name"), - ), - Element( - name = "active", - updatedValue = "true", - previousValue = null, - metadata = ElementMetadata(fqdn = "$fqcn.active"), - ), - ) - - elements?.forEach { element -> - expectedElements.first { it.name == element.name }.let { expected -> - element.updatedValue shouldBe expected.updatedValue - element.metadata?.fqdn shouldBe expected.metadata?.fqdn - } - } + elements.shouldNotBeNull() + + val expectedElements: List = + objectMapper.readValue( + javaClass.getResource("/simpleCreated.json").readBytes(), + Array::class.java, + ).toList() + + // Sort both lists by name for consistent comparison + val sortedActual = elements.sortedBy { it.name } + val sortedExpected = expectedElements.sortedBy { it.name } + + sortedActual shouldBe sortedExpected } } @@ -76,10 +56,19 @@ class JsonNodeMapperTest : BehaviorSpec({ .block() Then("should set previousValue instead of updatedValue") { - elements?.forEach { element -> - element.updatedValue shouldBe null - element.previousValue shouldNotBe null - } + elements.shouldNotBeNull() + + val expectedElements: List = + objectMapper.readValue( + javaClass.getResource("/simpleDeleted.json").readBytes(), + Array::class.java, + ).toList() + + // Sort both lists by name for consistent comparison + val sortedActual = elements.sortedBy { it.name } + val sortedExpected = expectedElements.sortedBy { it.name } + + sortedActual shouldBe sortedExpected } } } @@ -99,7 +88,6 @@ class JsonNodeMapperTest : BehaviorSpec({ """.trimIndent() val jsonNode = objectMapper.readTree(nestedJson) - val fqcn = "com.example.User" When("converting to elements") { val elements = @@ -108,14 +96,19 @@ class JsonNodeMapperTest : BehaviorSpec({ .block() Then("should handle nested objects correctly") { - elements?.size shouldBe 5 // id, name, address.street, address.city, address.zip + elements.shouldNotBeNull() - val addressElements = elements?.filter { it.name == "street" || it.name == "city" || it.name == "zip" } - addressElements?.size shouldBe 3 + val expectedElements: List = + objectMapper.readValue( + javaClass.getResource("/nestedCreated.json").readBytes(), + Array::class.java, + ).toList() - addressElements?.forEach { element -> - element.metadata?.fqdn shouldStartWith "$fqcn.address" - } + // Sort both lists by name and fqdn for consistent comparison + val sortedActual = elements.sortedWith(compareBy({ it.name }, { it.metadata?.fqdn })) + val sortedExpected = expectedElements.sortedWith(compareBy({ it.name }, { it.metadata?.fqdn })) + + sortedActual shouldBe sortedExpected } } } @@ -147,7 +140,6 @@ class JsonNodeMapperTest : BehaviorSpec({ """.trimIndent() val jsonNode = objectMapper.readTree(complexJson) - val fqcn = "com.example.UserProfile" When("converting complex object to elements") { val elements = @@ -156,24 +148,19 @@ class JsonNodeMapperTest : BehaviorSpec({ .block() Then("should handle all nested structures correctly") { - // Verify top-level primitive fields are correctly mapped - elements?.find { it.metadata?.fqdn == "$fqcn.id" }?.updatedValue shouldBe "1" - elements?.find { it.metadata?.fqdn == "$fqcn.name" }?.updatedValue shouldBe "Test User" - - // Verify nested array of objects with proper FQDN construction - elements?.find { it.metadata?.fqdn == "$fqcn.addresses.0.type" }?.updatedValue shouldBe "home" - elements?.find { it.metadata?.fqdn == "$fqcn.addresses.0.street" }?.updatedValue shouldBe "123 Main St" - elements?.find { it.metadata?.fqdn == "$fqcn.addresses.1.type" }?.updatedValue shouldBe "work" - elements?.find { it.metadata?.fqdn == "$fqcn.addresses.1.city" }?.updatedValue shouldBe "Businesstown" - - // Verify nested object with array field - elements?.find { it.metadata?.fqdn == "$fqcn.preferences.notifications" }?.updatedValue shouldBe "true" - elements?.find { it.metadata?.fqdn == "$fqcn.preferences.theme" }?.updatedValue shouldBe "dark" - - // Verify array values within nested object are correctly flattened - val categoryElements = elements?.filter { it.name == "favoriteCategories" } - categoryElements?.size shouldBe 3 - categoryElements?.map { it.updatedValue }?.toSet() shouldBe setOf("tech", "books", "music") + elements.shouldNotBeNull() + + val expectedElements: List = + objectMapper.readValue( + javaClass.getResource("/complexCreated.json").readBytes(), + Array::class.java, + ).toList() + + // Sort both lists by fqdn for consistent comparison + val sortedActual = elements.sortedBy { it.metadata?.fqdn } + val sortedExpected = expectedElements.sortedBy { it.metadata?.fqdn } + + sortedActual shouldBe sortedExpected } } } @@ -206,7 +193,6 @@ class JsonNodeMapperTest : BehaviorSpec({ """.trimIndent() val jsonNode = objectMapper.readTree(nestedArrayJson) - val fqcn = "com.example.NestedArrays" When("converting nested arrays to elements") { val elements = @@ -217,21 +203,17 @@ class JsonNodeMapperTest : BehaviorSpec({ Then("should handle all levels of nested arrays correctly") { elements.shouldNotBeNull() - // Verify 2D array access with proper indices - elements.find { it.metadata?.fqdn == "$fqcn.matrix.0.0" }?.updatedValue shouldBe "1" - elements.find { it.metadata?.fqdn == "$fqcn.matrix.1.2" }?.updatedValue shouldBe "6" - elements.find { it.metadata?.fqdn == "$fqcn.matrix.2.1" }?.updatedValue shouldBe "8" - - // Verify deeply nested arrays with objects and proper FQDN construction - elements.find { it.metadata?.fqdn == "$fqcn.nested.deepArray.0.0.id" }?.updatedValue shouldBe "1" - elements.find { it.metadata?.fqdn == "$fqcn.nested.deepArray.0.1.values.1" }?.updatedValue shouldBe "d" - elements.find { it.metadata?.fqdn == "$fqcn.nested.deepArray.1.0.values.0" }?.updatedValue shouldBe "e" - - // Verify simple array of objects with proper FQDN construction - elements.find { it.metadata?.fqdn == "$fqcn.simpleArray.0.id" }?.updatedValue shouldBe "1" - elements.find { it.metadata?.fqdn == "$fqcn.simpleArray.0.name" }?.updatedValue shouldBe "Item 1" - elements.find { it.metadata?.fqdn == "$fqcn.simpleArray.1.id" }?.updatedValue shouldBe "2" - elements.find { it.metadata?.fqdn == "$fqcn.simpleArray.1.name" }?.updatedValue shouldBe "Item 2" + val expectedElements: List = + objectMapper.readValue( + javaClass.getResource("/nestedArrayCreated.json").readBytes(), + Array::class.java, + ).toList() + + // Sort both lists by fqdn for consistent comparison + val sortedActual = elements.sortedBy { it.metadata?.fqdn } + val sortedExpected = expectedElements.sortedBy { it.metadata?.fqdn } + + sortedActual shouldBe sortedExpected } } } @@ -250,7 +232,6 @@ class JsonNodeMapperTest : BehaviorSpec({ """.trimIndent() val jsonNode = objectMapper.readTree(edgeCaseJson) - val fqcn = "com.example.EdgeCases" When("converting edge case values to elements") { val elements = @@ -259,13 +240,23 @@ class JsonNodeMapperTest : BehaviorSpec({ .block() Then("should handle all edge cases correctly") { - elements?.find { it.name == "nullValue" }?.updatedValue shouldBe "null" - elements?.find { it.name == "emptyString" }?.updatedValue shouldBe "" - elements?.find { it.name == "zero" }?.updatedValue shouldBe "0" - elements?.find { it.name == "falseValue" }?.updatedValue shouldBe "false" - // Verify empty objects and arrays are filtered out - elements?.find { it.name == "emptyObject" } shouldBe null - elements?.find { it.name == "emptyArray" } shouldBe null + elements.shouldNotBeNull() + + val expectedElements: List = + objectMapper.readValue( + javaClass.getResource("/edgeCaseCreated.json").readBytes(), + Array::class.java, + ).toList() + + // Sort both lists by name for consistent comparison + val sortedActual = elements.sortedBy { it.name } + val sortedExpected = expectedElements.sortedBy { it.name } + + sortedActual shouldBe sortedExpected + + // Explicitly verify empty objects and arrays are filtered out + elements.find { it.name == "emptyObject" } shouldBe null + elements.find { it.name == "emptyArray" } shouldBe null } } } diff --git a/client/src/test/kotlin/com/lowes/auditor/client/infrastructure/frameworks/service/ObjectDiffCheckerServiceTest.kt b/client/src/test/kotlin/com/lowes/auditor/client/infrastructure/frameworks/service/ObjectDiffCheckerServiceTest.kt index 9ce779b..0ca1443 100644 --- a/client/src/test/kotlin/com/lowes/auditor/client/infrastructure/frameworks/service/ObjectDiffCheckerServiceTest.kt +++ b/client/src/test/kotlin/com/lowes/auditor/client/infrastructure/frameworks/service/ObjectDiffCheckerServiceTest.kt @@ -62,7 +62,7 @@ class ObjectDiffCheckerServiceTest : BehaviorSpec({ When("Compare old and new simple object") { val diff = diffChecker.diff(oldItem, newItem).collectList().block() Then("Contains all update, create and delete Events - Simple object") { - diff shouldBe obj.readValue(javaClass.getResource("/udpate.json").readBytes(), Array::class.java).toList() + diff shouldBe obj.readValue(javaClass.getResource("/update.json").readBytes(), Array::class.java).toList() } } @@ -122,21 +122,21 @@ class ObjectDiffCheckerServiceTest : BehaviorSpec({ When("Only new nested list object is present - Create") { val diff = diffChecker.diff(null, oldItem).collectList().block() Then("Only updated values are populates - Nested list object") { - diff shouldBe obj.readValue(javaClass.getResource("/InnerlistCreate.json").readBytes(), Array::class.java).toList() + diff shouldBe obj.readValue(javaClass.getResource("/innerListCreate.json").readBytes(), Array::class.java).toList() } } When("Compare old and new nested list object") { val diff = diffChecker.diff(oldItem, newItem).collectList().block() Then("Conatains all update, create and delete Events - Nested list object") { - diff shouldBe obj.readValue(javaClass.getResource("/InnerlistUpdate.json").readBytes(), Array::class.java).toList() + diff shouldBe obj.readValue(javaClass.getResource("/innerListUpdate.json").readBytes(), Array::class.java).toList() } } When("Only old nested list object is present - Delete") { val diff = diffChecker.diff(newItem, null).collectList().block() Then("Only previous values are populates - Nested list object") { - diff shouldBe obj.readValue(javaClass.getResource("/InnerlistDelete.json").readBytes(), Array::class.java).toList() + diff shouldBe obj.readValue(javaClass.getResource("/innerListDelete.json").readBytes(), Array::class.java).toList() } } @@ -177,7 +177,7 @@ class ObjectDiffCheckerServiceTest : BehaviorSpec({ When("Only old collection list object is present - Delete") { val diff = diffChecker.diff(newItem, null).collectList().block() Then("Only previous values are populates - Collection list object") { - diff shouldBe obj.readValue(javaClass.getResource("/listdelete.json").readBytes(), Array::class.java).toList() + diff shouldBe obj.readValue(javaClass.getResource("/listDelete.json").readBytes(), Array::class.java).toList() } } @@ -262,7 +262,7 @@ class ObjectDiffCheckerServiceTest : BehaviorSpec({ When("Compare old and new collection mapInner objects") { val diff = diffChecker.diff(oldItem, newItem).collectList().block() Then("Contains all update, create and delete Events - Collection mapInner object") { - diff shouldBe obj.readValue(javaClass.getResource("/mapInnerpUpdate.json").readBytes(), Array::class.java).toList() + diff shouldBe obj.readValue(javaClass.getResource("/mapInnerUpdate.json").readBytes(), Array::class.java).toList() } } diff --git a/client/src/test/resources/complexCreated.json b/client/src/test/resources/complexCreated.json new file mode 100644 index 0000000..0b38b96 --- /dev/null +++ b/client/src/test/resources/complexCreated.json @@ -0,0 +1,106 @@ +[ + { + "name": "id", + "updatedValue": "1", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.id" + } + }, + { + "name": "name", + "updatedValue": "Test User", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.name" + } + }, + { + "name": "type", + "updatedValue": "home", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.addresses.0.type" + } + }, + { + "name": "street", + "updatedValue": "123 Main St", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.addresses.0.street" + } + }, + { + "name": "city", + "updatedValue": "Anytown", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.addresses.0.city" + } + }, + { + "name": "type", + "updatedValue": "work", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.addresses.1.type" + } + }, + { + "name": "street", + "updatedValue": "456 Business Ave", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.addresses.1.street" + } + }, + { + "name": "city", + "updatedValue": "Businesstown", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.addresses.1.city" + } + }, + { + "name": "notifications", + "updatedValue": "true", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.preferences.notifications" + } + }, + { + "name": "theme", + "updatedValue": "dark", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.preferences.theme" + } + }, + { + "name": "favoriteCategories", + "updatedValue": "tech", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.preferences.favoriteCategories.0" + } + }, + { + "name": "favoriteCategories", + "updatedValue": "books", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.preferences.favoriteCategories.1" + } + }, + { + "name": "favoriteCategories", + "updatedValue": "music", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.preferences.favoriteCategories.2" + } + } +] diff --git a/client/src/test/resources/edgeCaseCreated.json b/client/src/test/resources/edgeCaseCreated.json new file mode 100644 index 0000000..91696bd --- /dev/null +++ b/client/src/test/resources/edgeCaseCreated.json @@ -0,0 +1,34 @@ +[ + { + "name": "nullValue", + "updatedValue": "null", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.nullValue" + } + }, + { + "name": "emptyString", + "updatedValue": "", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.emptyString" + } + }, + { + "name": "zero", + "updatedValue": "0", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.zero" + } + }, + { + "name": "falseValue", + "updatedValue": "false", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.falseValue" + } + } +] diff --git a/client/src/test/resources/mapInnerpUpdate.json b/client/src/test/resources/mapInnerUpdate.json similarity index 100% rename from client/src/test/resources/mapInnerpUpdate.json rename to client/src/test/resources/mapInnerUpdate.json diff --git a/client/src/test/resources/nestedArrayCreated.json b/client/src/test/resources/nestedArrayCreated.json new file mode 100644 index 0000000..9cc32bc --- /dev/null +++ b/client/src/test/resources/nestedArrayCreated.json @@ -0,0 +1,178 @@ +[ + { + "name": "0", + "updatedValue": "1", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.matrix.0.0" + } + }, + { + "name": "1", + "updatedValue": "2", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.matrix.0.1" + } + }, + { + "name": "2", + "updatedValue": "3", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.matrix.0.2" + } + }, + { + "name": "0", + "updatedValue": "4", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.matrix.1.0" + } + }, + { + "name": "1", + "updatedValue": "5", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.matrix.1.1" + } + }, + { + "name": "2", + "updatedValue": "6", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.matrix.1.2" + } + }, + { + "name": "0", + "updatedValue": "7", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.matrix.2.0" + } + }, + { + "name": "1", + "updatedValue": "8", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.matrix.2.1" + } + }, + { + "name": "2", + "updatedValue": "9", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.matrix.2.2" + } + }, + { + "name": "id", + "updatedValue": "1", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.nested.deepArray.0.0.id" + } + }, + { + "name": "values", + "updatedValue": "a", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.nested.deepArray.0.0.values.0" + } + }, + { + "name": "values", + "updatedValue": "b", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.nested.deepArray.0.0.values.1" + } + }, + { + "name": "id", + "updatedValue": "2", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.nested.deepArray.0.1.id" + } + }, + { + "name": "values", + "updatedValue": "c", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.nested.deepArray.0.1.values.0" + } + }, + { + "name": "values", + "updatedValue": "d", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.nested.deepArray.0.1.values.1" + } + }, + { + "name": "id", + "updatedValue": "3", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.nested.deepArray.1.0.id" + } + }, + { + "name": "values", + "updatedValue": "e", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.nested.deepArray.1.0.values.0" + } + }, + { + "name": "values", + "updatedValue": "f", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.nested.deepArray.1.0.values.1" + } + }, + { + "name": "id", + "updatedValue": "1", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.simpleArray.0.id" + } + }, + { + "name": "name", + "updatedValue": "Item 1", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.simpleArray.0.name" + } + }, + { + "name": "id", + "updatedValue": "2", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.simpleArray.1.id" + } + }, + { + "name": "name", + "updatedValue": "Item 2", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.simpleArray.1.name" + } + } +] diff --git a/client/src/test/resources/nestedCreated.json b/client/src/test/resources/nestedCreated.json new file mode 100644 index 0000000..c16b1c4 --- /dev/null +++ b/client/src/test/resources/nestedCreated.json @@ -0,0 +1,42 @@ +[ + { + "name": "id", + "updatedValue": "1", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.id" + } + }, + { + "name": "name", + "updatedValue": "Test", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.name" + } + }, + { + "name": "street", + "updatedValue": "123 Main St", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.address.street" + } + }, + { + "name": "city", + "updatedValue": "Anytown", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.address.city" + } + }, + { + "name": "zip", + "updatedValue": "12345", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.address.zip" + } + } +] diff --git a/client/src/test/resources/simpleCreated.json b/client/src/test/resources/simpleCreated.json new file mode 100644 index 0000000..e6baebe --- /dev/null +++ b/client/src/test/resources/simpleCreated.json @@ -0,0 +1,26 @@ +[ + { + "name": "id", + "updatedValue": "1", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.id" + } + }, + { + "name": "name", + "updatedValue": "Test", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.name" + } + }, + { + "name": "active", + "updatedValue": "true", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.active" + } + } +] diff --git a/client/src/test/resources/simpleDeleted.json b/client/src/test/resources/simpleDeleted.json new file mode 100644 index 0000000..63d1811 --- /dev/null +++ b/client/src/test/resources/simpleDeleted.json @@ -0,0 +1,26 @@ +[ + { + "name": "id", + "updatedValue": null, + "previousValue": "1", + "metadata": { + "fqdn": "com.example.Test.id" + } + }, + { + "name": "name", + "updatedValue": null, + "previousValue": "Test", + "metadata": { + "fqdn": "com.example.Test.name" + } + }, + { + "name": "active", + "updatedValue": null, + "previousValue": "true", + "metadata": { + "fqdn": "com.example.Test.active" + } + } +] diff --git a/client/src/test/resources/udpate.json b/client/src/test/resources/update.json similarity index 100% rename from client/src/test/resources/udpate.json rename to client/src/test/resources/update.json From c1019786964e4f13ec05fe77715f884aa568a02c Mon Sep 17 00:00:00 2001 From: Avery Anderson Date: Thu, 21 Aug 2025 15:13:59 -0400 Subject: [PATCH 4/7] refactor: improve JsonNodeMapper test readability with external test data files --- .../frameworks/mapper/JsonNodeMapper.kt | 30 +-- .../frameworks/mapper/JsonNodeMapperTest.kt | 167 ++++++++-------- client/src/test/resources/complexCreated.json | 106 +++++++++++ .../src/test/resources/edgeCaseCreated.json | 34 ++++ .../test/resources/nestedArrayCreated.json | 178 ++++++++++++++++++ client/src/test/resources/nestedCreated.json | 42 +++++ client/src/test/resources/simpleCreated.json | 26 +++ client/src/test/resources/simpleDeleted.json | 26 +++ 8 files changed, 507 insertions(+), 102 deletions(-) create mode 100644 client/src/test/resources/complexCreated.json create mode 100644 client/src/test/resources/edgeCaseCreated.json create mode 100644 client/src/test/resources/nestedArrayCreated.json create mode 100644 client/src/test/resources/nestedCreated.json create mode 100644 client/src/test/resources/simpleCreated.json create mode 100644 client/src/test/resources/simpleDeleted.json diff --git a/client/src/main/kotlin/com/lowes/auditor/client/infrastructure/frameworks/mapper/JsonNodeMapper.kt b/client/src/main/kotlin/com/lowes/auditor/client/infrastructure/frameworks/mapper/JsonNodeMapper.kt index 7cdf9cb..c05f7ad 100644 --- a/client/src/main/kotlin/com/lowes/auditor/client/infrastructure/frameworks/mapper/JsonNodeMapper.kt +++ b/client/src/main/kotlin/com/lowes/auditor/client/infrastructure/frameworks/mapper/JsonNodeMapper.kt @@ -45,22 +45,24 @@ object JsonNodeMapper { val index = tuple.t1 val entry = tuple.t2 val elementFqcn = getFqcnValue(hasFields, index, fqcn, entry) - val nodeType = findType(entry.value) ?: return@flatMap Flux.empty() - when (nodeType) { - NodeType.ARRAY -> { - Flux.fromIterable(entry.value) - .index() - .flatMap { arrayTuple -> - val arrayIdx = arrayTuple.t1 - val arrayItem = arrayTuple.t2 - val arrayElementFqcn = "$elementFqcn.$arrayIdx" - createElementForNode(arrayItem, entry.key, eventType, arrayElementFqcn) - } + // Process only entries with valid node types + findType(entry.value)?.let { nodeType -> + when (nodeType) { + NodeType.ARRAY -> { + Flux.fromIterable(entry.value) + .index() + .flatMap { arrayTuple -> + val arrayIdx = arrayTuple.t1 + val arrayItem = arrayTuple.t2 + val arrayElementFqcn = "$elementFqcn.$arrayIdx" + createElementForNode(arrayItem, entry.key, eventType, arrayElementFqcn) + } + } + NodeType.OBJECT -> toElement(entry.value, eventType, elementFqcn) + else -> createElementForNode(entry.value, entry.key, eventType, elementFqcn, nodeType) } - NodeType.OBJECT -> toElement(entry.value, eventType, elementFqcn) - else -> createElementForNode(entry.value, entry.key, eventType, elementFqcn, nodeType) - } + } ?: Flux.empty() } } diff --git a/client/src/test/kotlin/com/lowes/auditor/client/infrastructure/frameworks/mapper/JsonNodeMapperTest.kt b/client/src/test/kotlin/com/lowes/auditor/client/infrastructure/frameworks/mapper/JsonNodeMapperTest.kt index e444551..5be8a28 100644 --- a/client/src/test/kotlin/com/lowes/auditor/client/infrastructure/frameworks/mapper/JsonNodeMapperTest.kt +++ b/client/src/test/kotlin/com/lowes/auditor/client/infrastructure/frameworks/mapper/JsonNodeMapperTest.kt @@ -2,19 +2,17 @@ package com.lowes.auditor.client.infrastructure.frameworks.mapper import com.fasterxml.jackson.databind.ObjectMapper import com.lowes.auditor.core.entities.domain.Element -import com.lowes.auditor.core.entities.domain.ElementMetadata import com.lowes.auditor.core.entities.domain.EventType import io.kotest.core.spec.style.BehaviorSpec import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe -import io.kotest.matchers.shouldNotBe -import io.kotest.matchers.string.shouldStartWith /** * Unit Tests for [JsonNodeMapper] */ class JsonNodeMapperTest : BehaviorSpec({ val objectMapper = ObjectMapper() + val fqcn = "com.example.Test" Given("a simple JSON object with primitive values") { val simpleJson = @@ -27,7 +25,6 @@ class JsonNodeMapperTest : BehaviorSpec({ """.trimIndent() val jsonNode = objectMapper.readTree(simpleJson) - val fqcn = "com.example.Test" When("converting to elements with CREATED event type") { val elements = @@ -36,36 +33,19 @@ class JsonNodeMapperTest : BehaviorSpec({ .block() Then("should create elements with correct FQDNs and values") { - elements?.size shouldBe 3 - - val expectedElements = - listOf( - Element( - name = "id", - updatedValue = "1", - previousValue = null, - metadata = ElementMetadata(fqdn = "$fqcn.id"), - ), - Element( - name = "name", - updatedValue = "Test", - previousValue = null, - metadata = ElementMetadata(fqdn = "$fqcn.name"), - ), - Element( - name = "active", - updatedValue = "true", - previousValue = null, - metadata = ElementMetadata(fqdn = "$fqcn.active"), - ), - ) - - elements?.forEach { element -> - expectedElements.first { it.name == element.name }.let { expected -> - element.updatedValue shouldBe expected.updatedValue - element.metadata?.fqdn shouldBe expected.metadata?.fqdn - } - } + elements.shouldNotBeNull() + + val expectedElements: List = + objectMapper.readValue( + javaClass.getResource("/simpleCreated.json").readBytes(), + Array::class.java, + ).toList() + + // Sort both lists by name for consistent comparison + val sortedActual = elements.sortedBy { it.name } + val sortedExpected = expectedElements.sortedBy { it.name } + + sortedActual shouldBe sortedExpected } } @@ -76,10 +56,19 @@ class JsonNodeMapperTest : BehaviorSpec({ .block() Then("should set previousValue instead of updatedValue") { - elements?.forEach { element -> - element.updatedValue shouldBe null - element.previousValue shouldNotBe null - } + elements.shouldNotBeNull() + + val expectedElements: List = + objectMapper.readValue( + javaClass.getResource("/simpleDeleted.json").readBytes(), + Array::class.java, + ).toList() + + // Sort both lists by name for consistent comparison + val sortedActual = elements.sortedBy { it.name } + val sortedExpected = expectedElements.sortedBy { it.name } + + sortedActual shouldBe sortedExpected } } } @@ -99,7 +88,6 @@ class JsonNodeMapperTest : BehaviorSpec({ """.trimIndent() val jsonNode = objectMapper.readTree(nestedJson) - val fqcn = "com.example.User" When("converting to elements") { val elements = @@ -108,14 +96,19 @@ class JsonNodeMapperTest : BehaviorSpec({ .block() Then("should handle nested objects correctly") { - elements?.size shouldBe 5 // id, name, address.street, address.city, address.zip + elements.shouldNotBeNull() - val addressElements = elements?.filter { it.name == "street" || it.name == "city" || it.name == "zip" } - addressElements?.size shouldBe 3 + val expectedElements: List = + objectMapper.readValue( + javaClass.getResource("/nestedCreated.json").readBytes(), + Array::class.java, + ).toList() - addressElements?.forEach { element -> - element.metadata?.fqdn shouldStartWith "$fqcn.address" - } + // Sort both lists by name and fqdn for consistent comparison + val sortedActual = elements.sortedWith(compareBy({ it.name }, { it.metadata?.fqdn })) + val sortedExpected = expectedElements.sortedWith(compareBy({ it.name }, { it.metadata?.fqdn })) + + sortedActual shouldBe sortedExpected } } } @@ -147,7 +140,6 @@ class JsonNodeMapperTest : BehaviorSpec({ """.trimIndent() val jsonNode = objectMapper.readTree(complexJson) - val fqcn = "com.example.UserProfile" When("converting complex object to elements") { val elements = @@ -156,24 +148,19 @@ class JsonNodeMapperTest : BehaviorSpec({ .block() Then("should handle all nested structures correctly") { - // Verify top-level primitive fields are correctly mapped - elements?.find { it.metadata?.fqdn == "$fqcn.id" }?.updatedValue shouldBe "1" - elements?.find { it.metadata?.fqdn == "$fqcn.name" }?.updatedValue shouldBe "Test User" - - // Verify nested array of objects with proper FQDN construction - elements?.find { it.metadata?.fqdn == "$fqcn.addresses.0.type" }?.updatedValue shouldBe "home" - elements?.find { it.metadata?.fqdn == "$fqcn.addresses.0.street" }?.updatedValue shouldBe "123 Main St" - elements?.find { it.metadata?.fqdn == "$fqcn.addresses.1.type" }?.updatedValue shouldBe "work" - elements?.find { it.metadata?.fqdn == "$fqcn.addresses.1.city" }?.updatedValue shouldBe "Businesstown" - - // Verify nested object with array field - elements?.find { it.metadata?.fqdn == "$fqcn.preferences.notifications" }?.updatedValue shouldBe "true" - elements?.find { it.metadata?.fqdn == "$fqcn.preferences.theme" }?.updatedValue shouldBe "dark" - - // Verify array values within nested object are correctly flattened - val categoryElements = elements?.filter { it.name == "favoriteCategories" } - categoryElements?.size shouldBe 3 - categoryElements?.map { it.updatedValue }?.toSet() shouldBe setOf("tech", "books", "music") + elements.shouldNotBeNull() + + val expectedElements: List = + objectMapper.readValue( + javaClass.getResource("/complexCreated.json").readBytes(), + Array::class.java, + ).toList() + + // Sort both lists by fqdn for consistent comparison + val sortedActual = elements.sortedBy { it.metadata?.fqdn } + val sortedExpected = expectedElements.sortedBy { it.metadata?.fqdn } + + sortedActual shouldBe sortedExpected } } } @@ -206,7 +193,6 @@ class JsonNodeMapperTest : BehaviorSpec({ """.trimIndent() val jsonNode = objectMapper.readTree(nestedArrayJson) - val fqcn = "com.example.NestedArrays" When("converting nested arrays to elements") { val elements = @@ -217,21 +203,17 @@ class JsonNodeMapperTest : BehaviorSpec({ Then("should handle all levels of nested arrays correctly") { elements.shouldNotBeNull() - // Verify 2D array access with proper indices - elements.find { it.metadata?.fqdn == "$fqcn.matrix.0.0" }?.updatedValue shouldBe "1" - elements.find { it.metadata?.fqdn == "$fqcn.matrix.1.2" }?.updatedValue shouldBe "6" - elements.find { it.metadata?.fqdn == "$fqcn.matrix.2.1" }?.updatedValue shouldBe "8" - - // Verify deeply nested arrays with objects and proper FQDN construction - elements.find { it.metadata?.fqdn == "$fqcn.nested.deepArray.0.0.id" }?.updatedValue shouldBe "1" - elements.find { it.metadata?.fqdn == "$fqcn.nested.deepArray.0.1.values.1" }?.updatedValue shouldBe "d" - elements.find { it.metadata?.fqdn == "$fqcn.nested.deepArray.1.0.values.0" }?.updatedValue shouldBe "e" - - // Verify simple array of objects with proper FQDN construction - elements.find { it.metadata?.fqdn == "$fqcn.simpleArray.0.id" }?.updatedValue shouldBe "1" - elements.find { it.metadata?.fqdn == "$fqcn.simpleArray.0.name" }?.updatedValue shouldBe "Item 1" - elements.find { it.metadata?.fqdn == "$fqcn.simpleArray.1.id" }?.updatedValue shouldBe "2" - elements.find { it.metadata?.fqdn == "$fqcn.simpleArray.1.name" }?.updatedValue shouldBe "Item 2" + val expectedElements: List = + objectMapper.readValue( + javaClass.getResource("/nestedArrayCreated.json").readBytes(), + Array::class.java, + ).toList() + + // Sort both lists by fqdn for consistent comparison + val sortedActual = elements.sortedBy { it.metadata?.fqdn } + val sortedExpected = expectedElements.sortedBy { it.metadata?.fqdn } + + sortedActual shouldBe sortedExpected } } } @@ -250,7 +232,6 @@ class JsonNodeMapperTest : BehaviorSpec({ """.trimIndent() val jsonNode = objectMapper.readTree(edgeCaseJson) - val fqcn = "com.example.EdgeCases" When("converting edge case values to elements") { val elements = @@ -259,13 +240,23 @@ class JsonNodeMapperTest : BehaviorSpec({ .block() Then("should handle all edge cases correctly") { - elements?.find { it.name == "nullValue" }?.updatedValue shouldBe "null" - elements?.find { it.name == "emptyString" }?.updatedValue shouldBe "" - elements?.find { it.name == "zero" }?.updatedValue shouldBe "0" - elements?.find { it.name == "falseValue" }?.updatedValue shouldBe "false" - // Verify empty objects and arrays are filtered out - elements?.find { it.name == "emptyObject" } shouldBe null - elements?.find { it.name == "emptyArray" } shouldBe null + elements.shouldNotBeNull() + + val expectedElements: List = + objectMapper.readValue( + javaClass.getResource("/edgeCaseCreated.json").readBytes(), + Array::class.java, + ).toList() + + // Sort both lists by name for consistent comparison + val sortedActual = elements.sortedBy { it.name } + val sortedExpected = expectedElements.sortedBy { it.name } + + sortedActual shouldBe sortedExpected + + // Explicitly verify empty objects and arrays are filtered out + elements.find { it.name == "emptyObject" } shouldBe null + elements.find { it.name == "emptyArray" } shouldBe null } } } diff --git a/client/src/test/resources/complexCreated.json b/client/src/test/resources/complexCreated.json new file mode 100644 index 0000000..0b38b96 --- /dev/null +++ b/client/src/test/resources/complexCreated.json @@ -0,0 +1,106 @@ +[ + { + "name": "id", + "updatedValue": "1", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.id" + } + }, + { + "name": "name", + "updatedValue": "Test User", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.name" + } + }, + { + "name": "type", + "updatedValue": "home", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.addresses.0.type" + } + }, + { + "name": "street", + "updatedValue": "123 Main St", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.addresses.0.street" + } + }, + { + "name": "city", + "updatedValue": "Anytown", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.addresses.0.city" + } + }, + { + "name": "type", + "updatedValue": "work", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.addresses.1.type" + } + }, + { + "name": "street", + "updatedValue": "456 Business Ave", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.addresses.1.street" + } + }, + { + "name": "city", + "updatedValue": "Businesstown", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.addresses.1.city" + } + }, + { + "name": "notifications", + "updatedValue": "true", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.preferences.notifications" + } + }, + { + "name": "theme", + "updatedValue": "dark", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.preferences.theme" + } + }, + { + "name": "favoriteCategories", + "updatedValue": "tech", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.preferences.favoriteCategories.0" + } + }, + { + "name": "favoriteCategories", + "updatedValue": "books", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.preferences.favoriteCategories.1" + } + }, + { + "name": "favoriteCategories", + "updatedValue": "music", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.preferences.favoriteCategories.2" + } + } +] diff --git a/client/src/test/resources/edgeCaseCreated.json b/client/src/test/resources/edgeCaseCreated.json new file mode 100644 index 0000000..91696bd --- /dev/null +++ b/client/src/test/resources/edgeCaseCreated.json @@ -0,0 +1,34 @@ +[ + { + "name": "nullValue", + "updatedValue": "null", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.nullValue" + } + }, + { + "name": "emptyString", + "updatedValue": "", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.emptyString" + } + }, + { + "name": "zero", + "updatedValue": "0", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.zero" + } + }, + { + "name": "falseValue", + "updatedValue": "false", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.falseValue" + } + } +] diff --git a/client/src/test/resources/nestedArrayCreated.json b/client/src/test/resources/nestedArrayCreated.json new file mode 100644 index 0000000..9cc32bc --- /dev/null +++ b/client/src/test/resources/nestedArrayCreated.json @@ -0,0 +1,178 @@ +[ + { + "name": "0", + "updatedValue": "1", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.matrix.0.0" + } + }, + { + "name": "1", + "updatedValue": "2", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.matrix.0.1" + } + }, + { + "name": "2", + "updatedValue": "3", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.matrix.0.2" + } + }, + { + "name": "0", + "updatedValue": "4", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.matrix.1.0" + } + }, + { + "name": "1", + "updatedValue": "5", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.matrix.1.1" + } + }, + { + "name": "2", + "updatedValue": "6", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.matrix.1.2" + } + }, + { + "name": "0", + "updatedValue": "7", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.matrix.2.0" + } + }, + { + "name": "1", + "updatedValue": "8", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.matrix.2.1" + } + }, + { + "name": "2", + "updatedValue": "9", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.matrix.2.2" + } + }, + { + "name": "id", + "updatedValue": "1", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.nested.deepArray.0.0.id" + } + }, + { + "name": "values", + "updatedValue": "a", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.nested.deepArray.0.0.values.0" + } + }, + { + "name": "values", + "updatedValue": "b", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.nested.deepArray.0.0.values.1" + } + }, + { + "name": "id", + "updatedValue": "2", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.nested.deepArray.0.1.id" + } + }, + { + "name": "values", + "updatedValue": "c", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.nested.deepArray.0.1.values.0" + } + }, + { + "name": "values", + "updatedValue": "d", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.nested.deepArray.0.1.values.1" + } + }, + { + "name": "id", + "updatedValue": "3", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.nested.deepArray.1.0.id" + } + }, + { + "name": "values", + "updatedValue": "e", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.nested.deepArray.1.0.values.0" + } + }, + { + "name": "values", + "updatedValue": "f", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.nested.deepArray.1.0.values.1" + } + }, + { + "name": "id", + "updatedValue": "1", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.simpleArray.0.id" + } + }, + { + "name": "name", + "updatedValue": "Item 1", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.simpleArray.0.name" + } + }, + { + "name": "id", + "updatedValue": "2", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.simpleArray.1.id" + } + }, + { + "name": "name", + "updatedValue": "Item 2", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.simpleArray.1.name" + } + } +] diff --git a/client/src/test/resources/nestedCreated.json b/client/src/test/resources/nestedCreated.json new file mode 100644 index 0000000..c16b1c4 --- /dev/null +++ b/client/src/test/resources/nestedCreated.json @@ -0,0 +1,42 @@ +[ + { + "name": "id", + "updatedValue": "1", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.id" + } + }, + { + "name": "name", + "updatedValue": "Test", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.name" + } + }, + { + "name": "street", + "updatedValue": "123 Main St", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.address.street" + } + }, + { + "name": "city", + "updatedValue": "Anytown", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.address.city" + } + }, + { + "name": "zip", + "updatedValue": "12345", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.address.zip" + } + } +] diff --git a/client/src/test/resources/simpleCreated.json b/client/src/test/resources/simpleCreated.json new file mode 100644 index 0000000..e6baebe --- /dev/null +++ b/client/src/test/resources/simpleCreated.json @@ -0,0 +1,26 @@ +[ + { + "name": "id", + "updatedValue": "1", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.id" + } + }, + { + "name": "name", + "updatedValue": "Test", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.name" + } + }, + { + "name": "active", + "updatedValue": "true", + "previousValue": null, + "metadata": { + "fqdn": "com.example.Test.active" + } + } +] diff --git a/client/src/test/resources/simpleDeleted.json b/client/src/test/resources/simpleDeleted.json new file mode 100644 index 0000000..63d1811 --- /dev/null +++ b/client/src/test/resources/simpleDeleted.json @@ -0,0 +1,26 @@ +[ + { + "name": "id", + "updatedValue": null, + "previousValue": "1", + "metadata": { + "fqdn": "com.example.Test.id" + } + }, + { + "name": "name", + "updatedValue": null, + "previousValue": "Test", + "metadata": { + "fqdn": "com.example.Test.name" + } + }, + { + "name": "active", + "updatedValue": null, + "previousValue": "true", + "metadata": { + "fqdn": "com.example.Test.active" + } + } +] From e077d10ff1a6fe14aedc8a182f98412b4d951458 Mon Sep 17 00:00:00 2001 From: Avery Anderson Date: Thu, 21 Aug 2025 15:49:23 -0400 Subject: [PATCH 5/7] revert: restore original ObjectDiffCheckerServiceTest.kt and resource files --- .../service/ObjectDiffCheckerServiceTest.kt | 12 +++--- .../src/test/resources/mapInnerpUpdate.json | 38 +++++++++++++++++++ client/src/test/resources/udpate.json | 29 ++++++++++++++ 3 files changed, 73 insertions(+), 6 deletions(-) create mode 100644 client/src/test/resources/mapInnerpUpdate.json create mode 100644 client/src/test/resources/udpate.json diff --git a/client/src/test/kotlin/com/lowes/auditor/client/infrastructure/frameworks/service/ObjectDiffCheckerServiceTest.kt b/client/src/test/kotlin/com/lowes/auditor/client/infrastructure/frameworks/service/ObjectDiffCheckerServiceTest.kt index 0ca1443..9ce779b 100644 --- a/client/src/test/kotlin/com/lowes/auditor/client/infrastructure/frameworks/service/ObjectDiffCheckerServiceTest.kt +++ b/client/src/test/kotlin/com/lowes/auditor/client/infrastructure/frameworks/service/ObjectDiffCheckerServiceTest.kt @@ -62,7 +62,7 @@ class ObjectDiffCheckerServiceTest : BehaviorSpec({ When("Compare old and new simple object") { val diff = diffChecker.diff(oldItem, newItem).collectList().block() Then("Contains all update, create and delete Events - Simple object") { - diff shouldBe obj.readValue(javaClass.getResource("/update.json").readBytes(), Array::class.java).toList() + diff shouldBe obj.readValue(javaClass.getResource("/udpate.json").readBytes(), Array::class.java).toList() } } @@ -122,21 +122,21 @@ class ObjectDiffCheckerServiceTest : BehaviorSpec({ When("Only new nested list object is present - Create") { val diff = diffChecker.diff(null, oldItem).collectList().block() Then("Only updated values are populates - Nested list object") { - diff shouldBe obj.readValue(javaClass.getResource("/innerListCreate.json").readBytes(), Array::class.java).toList() + diff shouldBe obj.readValue(javaClass.getResource("/InnerlistCreate.json").readBytes(), Array::class.java).toList() } } When("Compare old and new nested list object") { val diff = diffChecker.diff(oldItem, newItem).collectList().block() Then("Conatains all update, create and delete Events - Nested list object") { - diff shouldBe obj.readValue(javaClass.getResource("/innerListUpdate.json").readBytes(), Array::class.java).toList() + diff shouldBe obj.readValue(javaClass.getResource("/InnerlistUpdate.json").readBytes(), Array::class.java).toList() } } When("Only old nested list object is present - Delete") { val diff = diffChecker.diff(newItem, null).collectList().block() Then("Only previous values are populates - Nested list object") { - diff shouldBe obj.readValue(javaClass.getResource("/innerListDelete.json").readBytes(), Array::class.java).toList() + diff shouldBe obj.readValue(javaClass.getResource("/InnerlistDelete.json").readBytes(), Array::class.java).toList() } } @@ -177,7 +177,7 @@ class ObjectDiffCheckerServiceTest : BehaviorSpec({ When("Only old collection list object is present - Delete") { val diff = diffChecker.diff(newItem, null).collectList().block() Then("Only previous values are populates - Collection list object") { - diff shouldBe obj.readValue(javaClass.getResource("/listDelete.json").readBytes(), Array::class.java).toList() + diff shouldBe obj.readValue(javaClass.getResource("/listdelete.json").readBytes(), Array::class.java).toList() } } @@ -262,7 +262,7 @@ class ObjectDiffCheckerServiceTest : BehaviorSpec({ When("Compare old and new collection mapInner objects") { val diff = diffChecker.diff(oldItem, newItem).collectList().block() Then("Contains all update, create and delete Events - Collection mapInner object") { - diff shouldBe obj.readValue(javaClass.getResource("/mapInnerUpdate.json").readBytes(), Array::class.java).toList() + diff shouldBe obj.readValue(javaClass.getResource("/mapInnerpUpdate.json").readBytes(), Array::class.java).toList() } } diff --git a/client/src/test/resources/mapInnerpUpdate.json b/client/src/test/resources/mapInnerpUpdate.json new file mode 100644 index 0000000..ae5a9b1 --- /dev/null +++ b/client/src/test/resources/mapInnerpUpdate.json @@ -0,0 +1,38 @@ +[ + { + "name": "value", + "updatedValue": null, + "previousValue": "randMetamap007", + "metadata": { + "fqdn": "com.lowes.auditor.client.infrastructure.frameworks.model.Item.metadataRand.new_item_id.mapList.list_007.0.value", + "identifiers": null + } + }, + { + "name": "uom", + "updatedValue": "randMetain", + "previousValue": null, + "metadata": { + "fqdn": "com.lowes.auditor.client.infrastructure.frameworks.model.Item.metadataRand.new_item_id.mapList.list.0.uom", + "identifiers": null + } + }, + { + "name": "uom", + "updatedValue": null, + "previousValue": "randMetain007", + "metadata": { + "fqdn": "com.lowes.auditor.client.infrastructure.frameworks.model.Item.metadataRand.new_item_id.mapList.list_007.0.uom", + "identifiers": null + } + }, + { + "name": "value", + "updatedValue": "randMetamap", + "previousValue": null, + "metadata": { + "fqdn": "com.lowes.auditor.client.infrastructure.frameworks.model.Item.metadataRand.new_item_id.mapList.list.0.value", + "identifiers": null + } + } +] \ No newline at end of file diff --git a/client/src/test/resources/udpate.json b/client/src/test/resources/udpate.json new file mode 100644 index 0000000..b7e4b3d --- /dev/null +++ b/client/src/test/resources/udpate.json @@ -0,0 +1,29 @@ +[ + { + "name": "price", + "updatedValue": "5.67", + "previousValue": "1.23", + "metadata": { + "fqdn": "com.lowes.auditor.client.infrastructure.frameworks.model.Item.price", + "identifiers": null + } + }, + { + "name": "itemNumber", + "updatedValue": "1234", + "previousValue": "123", + "metadata": { + "fqdn": "com.lowes.auditor.client.infrastructure.frameworks.model.Item.itemNumber", + "identifiers": null + } + }, + { + "name": "description", + "updatedValue": "new_item", + "previousValue": "old_item", + "metadata": { + "fqdn": "com.lowes.auditor.client.infrastructure.frameworks.model.Item.description", + "identifiers": null + } + } +] \ No newline at end of file From 9f2a44943f053d79d4f5cffec117e2d47798e849 Mon Sep 17 00:00:00 2001 From: Avery Anderson Date: Thu, 21 Aug 2025 15:49:23 -0400 Subject: [PATCH 6/7] cleanup --- .../service/ObjectDiffCheckerServiceTest.kt | 12 ++++++------ .../{mapInnerUpdate.json => mapInnerpUpdate.json} | 0 .../src/test/resources/{update.json => udpate.json} | 0 3 files changed, 6 insertions(+), 6 deletions(-) rename client/src/test/resources/{mapInnerUpdate.json => mapInnerpUpdate.json} (100%) rename client/src/test/resources/{update.json => udpate.json} (100%) diff --git a/client/src/test/kotlin/com/lowes/auditor/client/infrastructure/frameworks/service/ObjectDiffCheckerServiceTest.kt b/client/src/test/kotlin/com/lowes/auditor/client/infrastructure/frameworks/service/ObjectDiffCheckerServiceTest.kt index 0ca1443..9ce779b 100644 --- a/client/src/test/kotlin/com/lowes/auditor/client/infrastructure/frameworks/service/ObjectDiffCheckerServiceTest.kt +++ b/client/src/test/kotlin/com/lowes/auditor/client/infrastructure/frameworks/service/ObjectDiffCheckerServiceTest.kt @@ -62,7 +62,7 @@ class ObjectDiffCheckerServiceTest : BehaviorSpec({ When("Compare old and new simple object") { val diff = diffChecker.diff(oldItem, newItem).collectList().block() Then("Contains all update, create and delete Events - Simple object") { - diff shouldBe obj.readValue(javaClass.getResource("/update.json").readBytes(), Array::class.java).toList() + diff shouldBe obj.readValue(javaClass.getResource("/udpate.json").readBytes(), Array::class.java).toList() } } @@ -122,21 +122,21 @@ class ObjectDiffCheckerServiceTest : BehaviorSpec({ When("Only new nested list object is present - Create") { val diff = diffChecker.diff(null, oldItem).collectList().block() Then("Only updated values are populates - Nested list object") { - diff shouldBe obj.readValue(javaClass.getResource("/innerListCreate.json").readBytes(), Array::class.java).toList() + diff shouldBe obj.readValue(javaClass.getResource("/InnerlistCreate.json").readBytes(), Array::class.java).toList() } } When("Compare old and new nested list object") { val diff = diffChecker.diff(oldItem, newItem).collectList().block() Then("Conatains all update, create and delete Events - Nested list object") { - diff shouldBe obj.readValue(javaClass.getResource("/innerListUpdate.json").readBytes(), Array::class.java).toList() + diff shouldBe obj.readValue(javaClass.getResource("/InnerlistUpdate.json").readBytes(), Array::class.java).toList() } } When("Only old nested list object is present - Delete") { val diff = diffChecker.diff(newItem, null).collectList().block() Then("Only previous values are populates - Nested list object") { - diff shouldBe obj.readValue(javaClass.getResource("/innerListDelete.json").readBytes(), Array::class.java).toList() + diff shouldBe obj.readValue(javaClass.getResource("/InnerlistDelete.json").readBytes(), Array::class.java).toList() } } @@ -177,7 +177,7 @@ class ObjectDiffCheckerServiceTest : BehaviorSpec({ When("Only old collection list object is present - Delete") { val diff = diffChecker.diff(newItem, null).collectList().block() Then("Only previous values are populates - Collection list object") { - diff shouldBe obj.readValue(javaClass.getResource("/listDelete.json").readBytes(), Array::class.java).toList() + diff shouldBe obj.readValue(javaClass.getResource("/listdelete.json").readBytes(), Array::class.java).toList() } } @@ -262,7 +262,7 @@ class ObjectDiffCheckerServiceTest : BehaviorSpec({ When("Compare old and new collection mapInner objects") { val diff = diffChecker.diff(oldItem, newItem).collectList().block() Then("Contains all update, create and delete Events - Collection mapInner object") { - diff shouldBe obj.readValue(javaClass.getResource("/mapInnerUpdate.json").readBytes(), Array::class.java).toList() + diff shouldBe obj.readValue(javaClass.getResource("/mapInnerpUpdate.json").readBytes(), Array::class.java).toList() } } diff --git a/client/src/test/resources/mapInnerUpdate.json b/client/src/test/resources/mapInnerpUpdate.json similarity index 100% rename from client/src/test/resources/mapInnerUpdate.json rename to client/src/test/resources/mapInnerpUpdate.json diff --git a/client/src/test/resources/update.json b/client/src/test/resources/udpate.json similarity index 100% rename from client/src/test/resources/update.json rename to client/src/test/resources/udpate.json From aede3805652a7e38d0daf1fb32fb1210786ea742 Mon Sep 17 00:00:00 2001 From: Avery Anderson Date: Tue, 26 Aug 2025 12:02:44 -0400 Subject: [PATCH 7/7] test: add test cases for list order changes in nested and deeply nested structures --- .../infrastructure/frameworks/model/Item.kt | 6 + .../service/ObjectDiffCheckerServiceTest.kt | 131 ++++++++++++++++++ .../deeplyNestedListOrderChange.json | 128 +++++++++++++++++ .../src/test/resources/listOrderChange.json | 20 +++ .../test/resources/nestedListOrderChange.json | 38 +++++ 5 files changed, 323 insertions(+) create mode 100644 client/src/test/resources/deeplyNestedListOrderChange.json create mode 100644 client/src/test/resources/listOrderChange.json create mode 100644 client/src/test/resources/nestedListOrderChange.json diff --git a/client/src/test/kotlin/com/lowes/auditor/client/infrastructure/frameworks/model/Item.kt b/client/src/test/kotlin/com/lowes/auditor/client/infrastructure/frameworks/model/Item.kt index 4310252..bd9206c 100644 --- a/client/src/test/kotlin/com/lowes/auditor/client/infrastructure/frameworks/model/Item.kt +++ b/client/src/test/kotlin/com/lowes/auditor/client/infrastructure/frameworks/model/Item.kt @@ -15,6 +15,7 @@ data class Item( val price: BigDecimal? = null, val subList: List? = null, val subMap: Map? = null, + val nestedList: List? = null, ) data class Rand( @@ -25,6 +26,11 @@ data class Rand( val mapList: Map>? = null, ) +data class NestedItem( + val id: String, + val items: List, +) + data class SubObject( val value: String? = null, val uom: String? = null, diff --git a/client/src/test/kotlin/com/lowes/auditor/client/infrastructure/frameworks/service/ObjectDiffCheckerServiceTest.kt b/client/src/test/kotlin/com/lowes/auditor/client/infrastructure/frameworks/service/ObjectDiffCheckerServiceTest.kt index 9ce779b..f942598 100644 --- a/client/src/test/kotlin/com/lowes/auditor/client/infrastructure/frameworks/service/ObjectDiffCheckerServiceTest.kt +++ b/client/src/test/kotlin/com/lowes/auditor/client/infrastructure/frameworks/service/ObjectDiffCheckerServiceTest.kt @@ -5,6 +5,7 @@ import com.lowes.auditor.client.entities.domain.AuditorEventConfig import com.lowes.auditor.client.infrastructure.frameworks.config.FrameworkModule import com.lowes.auditor.client.infrastructure.frameworks.model.DummyClass import com.lowes.auditor.client.infrastructure.frameworks.model.Item +import com.lowes.auditor.client.infrastructure.frameworks.model.NestedItem import com.lowes.auditor.client.infrastructure.frameworks.model.Rand import com.lowes.auditor.client.infrastructure.frameworks.model.SubObject import com.lowes.auditor.core.entities.domain.Element @@ -280,4 +281,134 @@ class ObjectDiffCheckerServiceTest : BehaviorSpec({ } } } + + Given("List with different order") { + val item1 = + Item( + itemNumber = "123", + stringList = listOf("a", "b", "c"), + ) + + val item2 = + Item( + itemNumber = "123", + stringList = listOf("c", "b", "a"), + ) + + When("comparing items with lists in different orders") { + val diff = diffChecker.diff(item1, item2).collectList().block() + + Then("should detect the order change as a difference") { + diff shouldBe + obj.readValue( + javaClass.getResource("/listOrderChange.json").readBytes(), + Array::class.java, + ).toList() + } + } + } + + Given("Nested list with different order") { + val item1 = + Item( + itemNumber = "123", + rand = + Rand( + id = "1", + doubleList = + listOf( + SubObject("a", "uom1"), + SubObject("b", "uom2"), + ), + ), + ) + + val item2 = + Item( + itemNumber = "123", + rand = + Rand( + id = "1", + doubleList = + listOf( + SubObject("b", "uom2"), + SubObject("a", "uom1"), + ), + ), + ) + + When("comparing items with nested lists in different orders") { + val diff = diffChecker.diff(item1, item2).collectList().block() + + Then("should detect the order change in nested list as a difference") { + diff shouldBe + obj.readValue( + javaClass.getResource("/nestedListOrderChange.json").readBytes(), + Array::class.java, + ).toList() + } + } + } + + Given("Deeply nested list with different order") { + val item1 = + Item( + itemNumber = "123", + nestedList = + listOf( + NestedItem( + "n1", + listOf( + SubObject("a", "uom1"), + SubObject("b", "uom2"), + ), + ), + NestedItem( + "n2", + listOf( + SubObject("x", "uom3"), + SubObject("y", "uom4"), + SubObject("z", "uom5"), + ), + ), + ), + ) + + val item2 = + Item( + itemNumber = "123", + nestedList = + listOf( + NestedItem( + "n2", + listOf( + SubObject("z", "uom5"), + SubObject("y", "uom4"), + SubObject("x", "uom3"), + ), + ), + NestedItem( + "n1", + listOf( + SubObject("b", "uom2"), + SubObject("a", "uom1"), + ), + ), + ), + ) + + When("comparing items with deeply nested lists in different orders") { + val diff = diffChecker.diff(item1, item2).collectList().block() + + Then("should detect the order changes in deeply nested lists with correct FQDNs") { + val expected = + obj.readValue( + javaClass.getResource("/deeplyNestedListOrderChange.json").readBytes(), + Array::class.java, + ).toList() + + diff shouldBe expected + } + } + } }) diff --git a/client/src/test/resources/deeplyNestedListOrderChange.json b/client/src/test/resources/deeplyNestedListOrderChange.json new file mode 100644 index 0000000..d286a52 --- /dev/null +++ b/client/src/test/resources/deeplyNestedListOrderChange.json @@ -0,0 +1,128 @@ +[ + { + "name": "id", + "previousValue": "n2", + "updatedValue": "n1", + "metadata": { + "fqdn": "com.lowes.auditor.client.infrastructure.frameworks.model.Item.nestedList.1.id", + "identifiers": null + } + }, + { + "name": "value", + "previousValue": null, + "updatedValue": "x", + "metadata": { + "fqdn": "com.lowes.auditor.client.infrastructure.frameworks.model.Item.nestedList.0.items.2.value", + "identifiers": null + } + }, + { + "name": "id", + "previousValue": "n1", + "updatedValue": "n2", + "metadata": { + "fqdn": "com.lowes.auditor.client.infrastructure.frameworks.model.Item.nestedList.0.id", + "identifiers": null + } + }, + { + "name": "uom", + "previousValue": null, + "updatedValue": "uom3", + "metadata": { + "fqdn": "com.lowes.auditor.client.infrastructure.frameworks.model.Item.nestedList.0.items.2.uom", + "identifiers": null + } + }, + { + "name": "uom", + "previousValue": "uom2", + "updatedValue": "uom4", + "metadata": { + "fqdn": "com.lowes.auditor.client.infrastructure.frameworks.model.Item.nestedList.0.items.1.uom", + "identifiers": null + } + }, + { + "name": "value", + "previousValue": "z", + "updatedValue": null, + "metadata": { + "fqdn": "com.lowes.auditor.client.infrastructure.frameworks.model.Item.nestedList.1.items.2.value", + "identifiers": null + } + }, + { + "name": "uom", + "previousValue": "uom5", + "updatedValue": null, + "metadata": { + "fqdn": "com.lowes.auditor.client.infrastructure.frameworks.model.Item.nestedList.1.items.2.uom", + "identifiers": null + } + }, + { + "name": "value", + "previousValue": "b", + "updatedValue": "y", + "metadata": { + "fqdn": "com.lowes.auditor.client.infrastructure.frameworks.model.Item.nestedList.0.items.1.value", + "identifiers": null + } + }, + { + "name": "value", + "previousValue": "a", + "updatedValue": "z", + "metadata": { + "fqdn": "com.lowes.auditor.client.infrastructure.frameworks.model.Item.nestedList.0.items.0.value", + "identifiers": null + } + }, + { + "name": "value", + "previousValue": "y", + "updatedValue": "a", + "metadata": { + "fqdn": "com.lowes.auditor.client.infrastructure.frameworks.model.Item.nestedList.1.items.1.value", + "identifiers": null + } + }, + { + "name": "value", + "previousValue": "x", + "updatedValue": "b", + "metadata": { + "fqdn": "com.lowes.auditor.client.infrastructure.frameworks.model.Item.nestedList.1.items.0.value", + "identifiers": null + } + }, + { + "name": "uom", + "previousValue": "uom3", + "updatedValue": "uom2", + "metadata": { + "fqdn": "com.lowes.auditor.client.infrastructure.frameworks.model.Item.nestedList.1.items.0.uom", + "identifiers": null + } + }, + { + "name": "uom", + "previousValue": "uom1", + "updatedValue": "uom5", + "metadata": { + "fqdn": "com.lowes.auditor.client.infrastructure.frameworks.model.Item.nestedList.0.items.0.uom", + "identifiers": null + } + }, + { + "name": "uom", + "previousValue": "uom4", + "updatedValue": "uom1", + "metadata": { + "fqdn": "com.lowes.auditor.client.infrastructure.frameworks.model.Item.nestedList.1.items.1.uom", + "identifiers": null + } + } + ] \ No newline at end of file diff --git a/client/src/test/resources/listOrderChange.json b/client/src/test/resources/listOrderChange.json new file mode 100644 index 0000000..48d21a4 --- /dev/null +++ b/client/src/test/resources/listOrderChange.json @@ -0,0 +1,20 @@ +[ + { + "name": "stringList", + "previousValue": "a", + "updatedValue": "c", + "metadata": { + "fqdn": "com.lowes.auditor.client.infrastructure.frameworks.model.Item.stringList.0", + "identifiers": null + } + }, + { + "name": "stringList", + "previousValue": "c", + "updatedValue": "a", + "metadata": { + "fqdn": "com.lowes.auditor.client.infrastructure.frameworks.model.Item.stringList.2", + "identifiers": null + } + } +] diff --git a/client/src/test/resources/nestedListOrderChange.json b/client/src/test/resources/nestedListOrderChange.json new file mode 100644 index 0000000..5aba5ec --- /dev/null +++ b/client/src/test/resources/nestedListOrderChange.json @@ -0,0 +1,38 @@ +[ + { + "name": "uom", + "previousValue": "uom2", + "updatedValue": "uom1", + "metadata": { + "fqdn": "com.lowes.auditor.client.infrastructure.frameworks.model.Item.rand.doubleList.1.uom", + "identifiers": null + } + }, + { + "name": "uom", + "previousValue": "uom1", + "updatedValue": "uom2", + "metadata": { + "fqdn": "com.lowes.auditor.client.infrastructure.frameworks.model.Item.rand.doubleList.0.uom", + "identifiers": null + } + }, + { + "name": "value", + "previousValue": "b", + "updatedValue": "a", + "metadata": { + "fqdn": "com.lowes.auditor.client.infrastructure.frameworks.model.Item.rand.doubleList.1.value", + "identifiers": null + } + }, + { + "name": "value", + "previousValue": "a", + "updatedValue": "b", + "metadata": { + "fqdn": "com.lowes.auditor.client.infrastructure.frameworks.model.Item.rand.doubleList.0.value", + "identifiers": null + } + } +]