From 70903acb6f36b596f5da9d3a5861d409ea0f9698 Mon Sep 17 00:00:00 2001 From: Yogesh Ananda Nikam Date: Wed, 20 Nov 2024 08:34:47 +0530 Subject: [PATCH] Implement filtering in the virtual service for the get all endpoint --- .../stub/stateful/StatefulHttpStub.kt | 6 +- .../io/specmatic/stub/stateful/StubCache.kt | 25 +++++++- .../stub/stateful/StatefulHttpStubTest.kt | 61 ++++++++++++++++--- .../spec_with_strictly_restful_apis.yaml | 9 +++ 4 files changed, 89 insertions(+), 12 deletions(-) diff --git a/core/src/main/kotlin/io/specmatic/stub/stateful/StatefulHttpStub.kt b/core/src/main/kotlin/io/specmatic/stub/stateful/StatefulHttpStub.kt index 26e35f0c2..bf8232cd7 100644 --- a/core/src/main/kotlin/io/specmatic/stub/stateful/StatefulHttpStub.kt +++ b/core/src/main/kotlin/io/specmatic/stub/stateful/StatefulHttpStub.kt @@ -276,7 +276,11 @@ class StatefulHttpStub( } if(method == "GET" && pathSegments.size == 1) { - val responseBody = stubCache.findAllResponsesFor(resourcePath, attributeSelectionKeys) + val responseBody = stubCache.findAllResponsesFor( + resourcePath, + attributeSelectionKeys, + httpRequest.queryParams.asMap() + ) return generatedResponse.withUpdated(responseBody, attributeSelectionKeys) } diff --git a/core/src/main/kotlin/io/specmatic/stub/stateful/StubCache.kt b/core/src/main/kotlin/io/specmatic/stub/stateful/StubCache.kt index 39ee5e3a1..6902673b3 100644 --- a/core/src/main/kotlin/io/specmatic/stub/stateful/StubCache.kt +++ b/core/src/main/kotlin/io/specmatic/stub/stateful/StubCache.kt @@ -2,6 +2,7 @@ package io.specmatic.stub.stateful import io.specmatic.core.value.JSONArrayValue import io.specmatic.core.value.JSONObjectValue +import io.specmatic.core.value.Value import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock @@ -39,9 +40,17 @@ class StubCache { } } - fun findAllResponsesFor(path: String, attributeSelectionKeys: Set): JSONArrayValue = lock.withLock { - val responseBodies = cachedResponses.filter { it.path == path }.map { - it.responseBody.removeKeysNotPresentIn(attributeSelectionKeys) + fun findAllResponsesFor( + path: String, + attributeSelectionKeys: Set, + filter: Map = emptyMap() + ): JSONArrayValue = lock.withLock { + val responseBodies = cachedResponses.filter { + it.path == path + }.map{ it.responseBody }.filter { + it.jsonObject.satisfiesFilter(filter) + }.map { + it.removeKeysNotPresentIn(attributeSelectionKeys) } return JSONArrayValue(responseBodies) } @@ -52,4 +61,14 @@ class StubCache { cachedResponses.remove(existingResponse) } } + + private fun Map.satisfiesFilter(filter: Map): Boolean { + if(filter.isEmpty()) return true + + return filter.all { (key, filterValue) -> + if(this.containsKey(key).not()) return@all true + val actualValue = this[key] ?: return@all false + actualValue.toStringLiteral() == filterValue + } + } } \ No newline at end of file diff --git a/core/src/test/kotlin/io/specmatic/stub/stateful/StatefulHttpStubTest.kt b/core/src/test/kotlin/io/specmatic/stub/stateful/StatefulHttpStubTest.kt index 5cc322fa8..18b6fcf39 100644 --- a/core/src/test/kotlin/io/specmatic/stub/stateful/StatefulHttpStubTest.kt +++ b/core/src/test/kotlin/io/specmatic/stub/stateful/StatefulHttpStubTest.kt @@ -64,7 +64,25 @@ class StatefulHttpStubTest { ) ) + val anotherResponse = httpStub.client.execute( + HttpRequest( + method = "POST", + path = "/products", + body = parsedJSONObject( + """ + { + "name": "Product B", + "description": "A detailed description of Product B.", + "price": 100, + "inStock": false + } + """.trimIndent() + ) + ) + ) + assertThat(response.status).isEqualTo(201) + assertThat(anotherResponse.status).isEqualTo(201) val responseBody = response.body as JSONObjectValue resourceId = responseBody.getStringValue("id").orEmpty() @@ -87,8 +105,11 @@ class StatefulHttpStubTest { assertThat(response.status).isEqualTo(200) assertThat(response.body).isInstanceOf(JSONArrayValue::class.java) + val responseBody = response.body as JSONArrayValue - val responseObjectFromResponseBody = (response.body as JSONArrayValue).list.first() as JSONObjectValue + assertThat(responseBody.list.size).isEqualTo(2) + + val responseObjectFromResponseBody = responseBody.list.first() as JSONObjectValue assertThat(responseObjectFromResponseBody.getStringValue("name")).isEqualTo("Product A") assertThat(responseObjectFromResponseBody.getStringValue("description")).isEqualTo("A detailed description of Product A.") @@ -98,6 +119,30 @@ class StatefulHttpStubTest { @Test @Order(3) + fun `should get the list of products filtered based on name and price passed in query params`() { + val response = httpStub.client.execute( + HttpRequest( + method = "GET", + path = "/products?name=Product%20A&price=19.99" + ) + ) + + assertThat(response.status).isEqualTo(200) + assertThat(response.body).isInstanceOf(JSONArrayValue::class.java) + val responseBody = response.body as JSONArrayValue + + assertThat(responseBody.list.size).isEqualTo(1) + + val responseObjectFromResponseBody = responseBody.list.first() as JSONObjectValue + + assertThat(responseObjectFromResponseBody.getStringValue("name")).isEqualTo("Product A") + assertThat(responseObjectFromResponseBody.getStringValue("description")).isEqualTo("A detailed description of Product A.") + assertThat(responseObjectFromResponseBody.getStringValue("price")).isEqualTo("19.99") + assertThat(responseObjectFromResponseBody.getStringValue("inStock")).isEqualTo("true") + } + + @Test + @Order(4) fun `should update an existing product with patch`() { val response = httpStub.client.execute( HttpRequest( @@ -125,7 +170,7 @@ class StatefulHttpStubTest { } @Test - @Order(4) + @Order(5) fun `should get the updated product`() { val response = httpStub.client.execute( HttpRequest( @@ -145,7 +190,7 @@ class StatefulHttpStubTest { } @Test - @Order(5) + @Order(6) fun `should delete a product`() { val response = httpStub.client.execute( HttpRequest( @@ -167,7 +212,7 @@ class StatefulHttpStubTest { } @Test - @Order(6) + @Order(7) fun `should post a product even though the request contains unknown keys`() { val response = httpStub.client.execute( HttpRequest( @@ -200,7 +245,7 @@ class StatefulHttpStubTest { } @Test - @Order(7) + @Order(8) fun `should get a 400 response in a structured manner for an invalid post request`() { val response = httpStub.client.execute( HttpRequest( @@ -226,7 +271,7 @@ class StatefulHttpStubTest { assertThat(error).contains("Contract expected boolean but request contained \"true\"") } - @Order(8) + @Order(9) @Test fun `should get a 400 response as a string for an invalid get request where 400 sceham is not defined for the same in the spec`() { val response = httpStub.client.execute( @@ -243,7 +288,7 @@ class StatefulHttpStubTest { } @Test - @Order(9) + @Order(10) fun `should get a 404 response in a structured manner for a get request where the entry with requested id is not present in the cache`() { val response = httpStub.client.execute( HttpRequest( @@ -259,7 +304,7 @@ class StatefulHttpStubTest { } @Test - @Order(10) + @Order(11) fun `should get a 404 response as a string for a delete request with missing id where 404 schema is not defined for the same in the spec`() { val response = httpStub.client.execute( HttpRequest( diff --git a/core/src/test/resources/openapi/spec_with_strictly_restful_apis/spec_with_strictly_restful_apis.yaml b/core/src/test/resources/openapi/spec_with_strictly_restful_apis/spec_with_strictly_restful_apis.yaml index 3c42996f3..6f853da66 100644 --- a/core/src/test/resources/openapi/spec_with_strictly_restful_apis/spec_with_strictly_restful_apis.yaml +++ b/core/src/test/resources/openapi/spec_with_strictly_restful_apis/spec_with_strictly_restful_apis.yaml @@ -15,6 +15,15 @@ paths: schema: type: string description: Specify which fields to retrieve in the response + - in: query + name: name + schema: + type: string + - in: query + name: price + schema: + type: number + format: float responses: '200': description: A list of products