diff --git a/CHANGELOG.md b/CHANGELOG.md index 19c11f7..c94274e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +### 1.1.7 - 01.08.2023 + +- Expand `PathQueryCondition` to enable matching requests on multiple query parameters (per issue [#16](https://github.com/DroidsOnRoids/mockwebserver-path-dispatcher/issues/16)) +- Compatibility is maintained with previous versions + +### 1.1.6 - 09.01.2023 + +- Add option to mock timeout failure + ### 1.1.5 - 01.12.2022 - Make FixtureDispatcher thread-safe diff --git a/README.md b/README.md index 2f39a70..63534c3 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,11 @@ fun factory() { dispatcher.putResponse(factory.withPathSuffixAndQueryParameter("suffix", "param"), "response_with_query_parameter") // match all URLs with path ending with "suffix" and have "param" with "value" as query parameter e.g. http://example.test/prefix/user/suffix?param=value dispatcher.putResponse(factory.withPathSuffixAndQueryParameter("suffix", "param", "value"), "response_with_query_parameter_and_value") + // match all URLs with path ending with "suffix" and have multiple parameter name/value pairs e.g.http://example.test/prefix/user/suffix?param=value¶m2=value2 + dispatcher.putResponse( + factory.withPathSuffixAndQueryParameters("suffix", mapOf("param" to "value", "param2" to "value2")), + "response_with_multiple_query_parameters" + ) mockWebServer.setDispatcher(dispatcher) } ``` @@ -140,6 +145,15 @@ fun pathQueryCondition() { } ``` +Also supports a map of multiple query parameters: + +```kotlin +fun pathQueryConditions() { + val dispatcher = FixtureDispatcher() + dispatcher.putResponse(PathQueryCondition("/prefix/suffix", mapOf("param" to "value", "param2" to "value2")), "response_with_query_parameters_and_values") + mockWebServer.setDispatcher(dispatcher) +} +``` `HttpUrlCondition` - when you want to match by some part of URL other than path or single query parameter: @@ -175,15 +189,15 @@ fun condition() { For unit tests: ```gradle -testImplementation 'pl.droidsonroids.testing:mockwebserver-path-dispatcher:1.1.1' +testImplementation 'pl.droidsonroids.testing:mockwebserver-path-dispatcher:1.1.7' ``` or for Android instrumentation tests: ```gradle -androidTestImplementation 'pl.droidsonroids.testing:mockwebserver-path-dispatcher:1.1.1' +androidTestImplementation 'pl.droidsonroids.testing:mockwebserver-path-dispatcher:1.1.7' ``` ### License -Library uses the MIT License. See [LICENSE](LICENSE) file. \ No newline at end of file +Library uses the MIT License. See [LICENSE](LICENSE) file. diff --git a/dispatcher/src/main/kotlin/pl/droidsonroids/testing/mockwebserver/condition/PathQueryCondition.kt b/dispatcher/src/main/kotlin/pl/droidsonroids/testing/mockwebserver/condition/PathQueryCondition.kt index c1c9b41..d364281 100644 --- a/dispatcher/src/main/kotlin/pl/droidsonroids/testing/mockwebserver/condition/PathQueryCondition.kt +++ b/dispatcher/src/main/kotlin/pl/droidsonroids/testing/mockwebserver/condition/PathQueryCondition.kt @@ -3,19 +3,36 @@ package pl.droidsonroids.testing.mockwebserver.condition import okhttp3.HttpUrl /** - * A [Condition] matching by URL path, optional query parameter name and optional query parameter value for that name. - * Instances are sorted from least to most general e.g. one containing query parameter name comes before - * another containing path only. + * A [Condition] matching by URL path and a [Map] of query parameter name/value pairs. + * Instances are sorted from least to most general, based on the number of specified query parameter names and + * the corresponding non-null values. * @property path URL path, required - * @property queryParameterName query parameter name, optional - * @property queryParameterName query parameter value for given name, optional + * @property httpMethod [HTTPMethod] to map to for the given condition, optional + * @property queryParameters [Map] query parameter name/value pairs, optional, defaulting to an empty map */ data class PathQueryCondition( internal val path: String, override val httpMethod: HTTPMethod, - internal val queryParameterName: String? = null, - internal val queryParameterValue: String? = null + internal val queryParameters: Map = emptyMap() ) : HttpUrlCondition() { + constructor(path: String, queryParameters: Map) : + this(path, HTTPMethod.ANY, queryParameters) + + /** + * + * A [Condition] matching by URL path, optional query parameter name and optional query parameter value for that name. + * Instances are sorted from least to most general e.g. one containing query parameter name comes before + * another containing path only. + * @property path URL path, required + * @property httpMethod [HTTPMethod] to map to for the given condition, optional + * @property queryParameterName query parameter name, optional + * @property queryParameterName query parameter value for given name, optional + */ + constructor(path: String, httpMethod: HTTPMethod, queryParameterName: String, queryParameterValue: String? = null) : + this(path, httpMethod, mapOf(queryParameterName to queryParameterValue)) + + constructor(path: String, queryParameterName: String, queryParameterValue: String? = null) : + this(path, HTTPMethod.ANY, mapOf(queryParameterName to queryParameterValue)) override fun compareTo(other: Condition) = when { other == this -> 0 @@ -26,14 +43,12 @@ data class PathQueryCondition( } override fun isUrlMatching(url: HttpUrl): Boolean { - val requestQueryParameterNames = url.queryParameterNames if (url.encodedPath == path) { when { - queryParameterName == null -> return true - requestQueryParameterNames.contains(queryParameterName) -> { - val requestQueryParameterValue = url.queryParameter(queryParameterName) - if (queryParameterValue == null || queryParameterValue == requestQueryParameterValue) { - return true + queryParameters.isEmpty() -> return true + queryParameters.keys.all { it in url.queryParameterNames } -> { + return queryParameters.all { + it.value == null || it.value == url.queryParameter(it.key) } } } @@ -42,13 +57,6 @@ data class PathQueryCondition( } private val score: Int - get() { - if (queryParameterName == null) { - return 0 - } else if (queryParameterValue == null) { - return 1 - } - return 2 - } - + get() = queryParameters.keys.count() + + queryParameters.values.count { !it.isNullOrEmpty() } } diff --git a/dispatcher/src/main/kotlin/pl/droidsonroids/testing/mockwebserver/condition/PathQueryConditionFactory.kt b/dispatcher/src/main/kotlin/pl/droidsonroids/testing/mockwebserver/condition/PathQueryConditionFactory.kt index 948bc1b..b54f155 100644 --- a/dispatcher/src/main/kotlin/pl/droidsonroids/testing/mockwebserver/condition/PathQueryConditionFactory.kt +++ b/dispatcher/src/main/kotlin/pl/droidsonroids/testing/mockwebserver/condition/PathQueryConditionFactory.kt @@ -7,12 +7,33 @@ package pl.droidsonroids.testing.mockwebserver.condition * @constructor creates new factory with given prefix */ class PathQueryConditionFactory constructor(private val pathPrefix: String = "") { + /** + * Creates condition with path and a Map of one or more query parameter name/value pairs. + * @param pathSuffix path suffix, may be empty + * @param queryParameters Map of query parameter name/value pairs + * @param httpMethod optional HTTPMethod for matching, defaults to HTTPMethod.ANY + * @return a PathQueryCondition + * @since 1.1.7 + */ + @JvmOverloads + fun withPathSuffixAndQueryParameters( + pathSuffix: String, + queryParameters: Map, + httpMethod: HTTPMethod = HTTPMethod.ANY + ) = + PathQueryCondition( + pathPrefix + pathSuffix, + httpMethod, + queryParameters + ) + /** * Creates condition with both path, queryParameterName * and queryParameterValue. * @param pathSuffix path suffix, may be empty * @param queryParameterName query parameter name queryParameterName * @param queryParameterValue query parameter value for given + * @param httpMethod optional HTTPMethod for matching, defaults to HTTPMethod.ANY * @return a PathQueryCondition * @since 1.1.0 */ @@ -33,6 +54,7 @@ class PathQueryConditionFactory constructor(private val pathPrefix: String = "") /** * Creates condition with path only. * @param pathSuffix path suffix, may be empty + * @param httpMethod optional HTTPMethod for matching, defaults to HTTPMethod.ANY * @return a PathQueryCondition * @since 1.1.0 */ @@ -48,6 +70,7 @@ class PathQueryConditionFactory constructor(private val pathPrefix: String = "") /** * Creates condition with path only. * @param path the path + * @param httpMethod optional HTTPMethod for matching, defaults to HTTPMethod.ANY * @return a PathQueryCondition */ fun withPath( @@ -108,4 +131,4 @@ class PathQueryConditionFactory constructor(private val pathPrefix: String = "") @Deprecated("Infix renamed to suffix", replaceWith = ReplaceWith("withPathSuffix")) fun withPathInfix(pathInfix: String) = withPathSuffix(pathInfix) -} \ No newline at end of file +} diff --git a/dispatcher/src/test/kotlin/pl/droidsonroids/testing/mockwebserver/PathQueryConditionTest.kt b/dispatcher/src/test/kotlin/pl/droidsonroids/testing/mockwebserver/PathQueryConditionTest.kt index 423dc1b..b1ac0ab 100644 --- a/dispatcher/src/test/kotlin/pl/droidsonroids/testing/mockwebserver/PathQueryConditionTest.kt +++ b/dispatcher/src/test/kotlin/pl/droidsonroids/testing/mockwebserver/PathQueryConditionTest.kt @@ -14,11 +14,18 @@ import pl.droidsonroids.testing.mockwebserver.condition.PathQueryConditionFactor private const val INFIX = "/suffix" private const val PARAMETER_NAME = "param" private const val PARAMETER_VALUE = "value" +private const val PARAMETER_NAME2 = "param2" +private const val PARAMETER_VALUE2 = "value2" +private val PARAMETER_MAP_SINGLE = mapOf(PARAMETER_NAME to PARAMETER_VALUE) +private val PARAMETER_MAP_MULTIPLE_NULL = mapOf(PARAMETER_NAME to PARAMETER_VALUE, PARAMETER_NAME2 to null) +private val PARAMETER_MAP_MULTIPLE = mapOf(PARAMETER_NAME to PARAMETER_VALUE, PARAMETER_NAME2 to PARAMETER_VALUE2) class PathQueryConditionTest { private lateinit var suffixPathQueryCondition: PathQueryCondition private lateinit var parameterNamePathQueryCondition: PathQueryCondition private lateinit var parameterValuePathQueryCondition: PathQueryCondition + private lateinit var parameterMapPathQueryCondition: PathQueryCondition + private lateinit var parameterMapNullPathQueryCondition: PathQueryCondition @Before fun setUp() { @@ -28,6 +35,10 @@ class PathQueryConditionTest { factory.withPathSuffixAndQueryParameter(INFIX, PARAMETER_NAME) parameterValuePathQueryCondition = factory.withPathSuffixAndQueryParameter(INFIX, PARAMETER_NAME, PARAMETER_VALUE) + parameterMapPathQueryCondition = + factory.withPathSuffixAndQueryParameters(INFIX, PARAMETER_MAP_MULTIPLE) + parameterMapNullPathQueryCondition = + factory.withPathSuffixAndQueryParameters(INFIX, PARAMETER_MAP_MULTIPLE_NULL) } @Test @@ -38,14 +49,31 @@ class PathQueryConditionTest { @Test fun `has correct suffix and query parameter name`() { assertThat(parameterNamePathQueryCondition.path).isEqualTo(INFIX) - assertThat(parameterNamePathQueryCondition.queryParameterName).isEqualTo(PARAMETER_NAME) + assertThat(parameterNamePathQueryCondition.queryParameters.keys.first()).isEqualTo(PARAMETER_NAME) } @Test fun `has correct suffix and query parameter name and value`() { assertThat(parameterValuePathQueryCondition.path).isEqualTo(INFIX) - assertThat(parameterValuePathQueryCondition.queryParameterName).isEqualTo(PARAMETER_NAME) - assertThat(parameterValuePathQueryCondition.queryParameterValue).isEqualTo(PARAMETER_VALUE) + assertThat(parameterValuePathQueryCondition.queryParameters.entries.first().key).isEqualTo(PARAMETER_NAME) + assertThat(parameterValuePathQueryCondition.queryParameters.entries.first().value).isEqualTo(PARAMETER_VALUE) + } + + @Test + fun `has correct suffix and query parameter names and values`() { + assertThat(parameterMapPathQueryCondition.path).isEqualTo(INFIX) + assertThat(parameterMapPathQueryCondition.queryParameters.keys).containsAll( + setOf( + PARAMETER_NAME, + PARAMETER_NAME2 + ) + ) + assertThat(parameterMapPathQueryCondition.queryParameters.values).containsAll( + setOf( + PARAMETER_VALUE, + PARAMETER_VALUE2 + ) + ) } @Test @@ -63,6 +91,26 @@ class PathQueryConditionTest { assertThat(parameterValuePathQueryCondition).isLessThan(suffixPathQueryCondition) } + @Test + fun `is map less than suffix`() { + assertThat(parameterMapPathQueryCondition).isLessThan(suffixPathQueryCondition) + } + + @Test + fun `is map less than name`() { + assertThat(parameterMapPathQueryCondition).isLessThan(parameterNamePathQueryCondition) + } + + @Test + fun `is map less than value`() { + assertThat(parameterMapPathQueryCondition).isLessThan(parameterValuePathQueryCondition) + } + + @Test + fun `is map less than map null`() { + assertThat(parameterMapPathQueryCondition).isLessThan(parameterMapNullPathQueryCondition) + } + @Test fun `is unrelated condition not equal to path query condition`() { assertThat(parameterValuePathQueryCondition as Condition).isNotEqualByComparingTo(mock()) @@ -110,6 +158,43 @@ class PathQueryConditionTest { assertThat(parameterValuePathQueryCondition.isUrlMatching(url)).isFalse } + @Test + fun `url with equal suffix, query parameter name and value does not match mapped`() { + PathQueryCondition("/suffix", "param", "value") + val url = "http://test.test/suffix?param=value".toHttpUrl() + assertThat(parameterMapPathQueryCondition.isUrlMatching(url)).isFalse + } + + @Test + fun `url with equal suffix, query parameter names and values matches`() { + val url = "http://test.test/suffix?param=value¶m2=value2".toHttpUrl() + assertThat(parameterMapPathQueryCondition.isUrlMatching(url)).isTrue + } + + @Test + fun `url with equal suffix, first query parameter pair, different second parameter name does not match`() { + val url = "http://test.test/suffix?param=value¶m3".toHttpUrl() + assertThat(parameterMapPathQueryCondition.isUrlMatching(url)).isFalse + } + + @Test + fun `url with equal suffix, first query parameter pair and second parameter name, different second parameter value does not match`() { + val url = "http://test.test/suffix?param=value¶m2=value3".toHttpUrl() + assertThat(parameterMapPathQueryCondition.isUrlMatching(url)).isFalse + } + + @Test + fun `url with equal suffix, first query parameter pair and second parameter name, null second parameter value does not match`() { + val url = "http://test.test/suffix?param=value¶m2".toHttpUrl() + assertThat(parameterMapPathQueryCondition.isUrlMatching(url)).isFalse + } + + @Test + fun `url with equal suffix, first query parameter pair and second parameter name match when second parameter value null`() { + val url = "http://test.test/suffix?param=value¶m2=value3".toHttpUrl() + assertThat(parameterMapNullPathQueryCondition.isUrlMatching(url)).isTrue + } + @Test fun `equals and hashCode match contract`() { EqualsVerifier @@ -117,6 +202,27 @@ class PathQueryConditionTest { .verify() } + @Test + fun `alternate constructors yield equivalent results for map`() { + val withMethod = PathQueryCondition(INFIX, HTTPMethod.ANY, PARAMETER_MAP_MULTIPLE) + val withoutMethod = PathQueryCondition(INFIX, PARAMETER_MAP_MULTIPLE) + assertThat(withMethod.queryParameters).containsAllEntriesOf(withoutMethod.queryParameters) + } + + @Test + fun `alternate constructors yield equivalent results for parameter name`() { + val withMethod = PathQueryCondition(INFIX, HTTPMethod.ANY, PARAMETER_NAME) + val withoutMethod = PathQueryCondition(INFIX, PARAMETER_NAME) + assertThat(withMethod.queryParameters).containsAllEntriesOf(withoutMethod.queryParameters) + } + + @Test + fun `alternate constructors yield equivalent results for name and value`() { + val withMethod = PathQueryCondition(INFIX, HTTPMethod.ANY, PARAMETER_NAME, PARAMETER_VALUE) + val withoutMethod = PathQueryCondition(INFIX, PARAMETER_NAME, PARAMETER_VALUE) + assertThat(withMethod.queryParameters).containsAllEntriesOf(withoutMethod.queryParameters) + } + @Test fun `compareTo should return 0 when conditions are equals without parameters`() { assertThat(suffixPathQueryCondition.compareTo(suffixPathQueryCondition)).isEqualTo(0) @@ -167,6 +273,17 @@ class PathQueryConditionTest { .isEqualTo(-1) } + @Test + fun `compareTo should return -1 when first condition http method has higher precedence than second condition`() { + val firstCondition = PathQueryConditionFactory() + .withPathSuffix("/abc", HTTPMethod.GET) + val secondCondition = PathQueryConditionFactory() + .withPathSuffix("/abc", HTTPMethod.POST) + + assertThat(firstCondition.compareTo(secondCondition)) + .isEqualTo(-1) + } + @Test fun `compareTo should return -1 when the first condition has a parameter name and the second one does not`() { val firstCondition = PathQueryConditionFactory() @@ -232,4 +349,70 @@ class PathQueryConditionTest { assertThat(firstCondition.compareTo(secondCondition)) .isEqualTo(1) } -} \ No newline at end of file + + @Test + fun `compareTo should return 1 when the first condition does not have a parameter and the second one has a parameter value map`() { + val firstCondition = PathQueryConditionFactory() + .withPathSuffix("/abc") + val secondCondition = PathQueryConditionFactory() + .withPathSuffixAndQueryParameters("/abc", PARAMETER_MAP_MULTIPLE) + + assertThat(firstCondition.compareTo(secondCondition)) + .isEqualTo(1) + } + + @Test + fun `compareTo should return 1 when the first condition has a parameter name and the second one has a parameter value map`() { + val firstCondition = PathQueryConditionFactory() + .withPathSuffixAndQueryParameter("/abc", PARAMETER_NAME) + val secondCondition = PathQueryConditionFactory() + .withPathSuffixAndQueryParameters("/abc", PARAMETER_MAP_MULTIPLE) + + assertThat(firstCondition.compareTo(secondCondition)) + .isEqualTo(1) + } + + @Test + fun `compareTo should return 1 when the first condition has a parameter name and value and the second one has a parameter value map with multiple pairs`() { + val firstCondition = PathQueryConditionFactory() + .withPathSuffixAndQueryParameter("/abc", PARAMETER_NAME, PARAMETER_VALUE) + val secondCondition = PathQueryConditionFactory() + .withPathSuffixAndQueryParameters("/abc", PARAMETER_MAP_MULTIPLE) + + assertThat(firstCondition.compareTo(secondCondition)) + .isEqualTo(1) + } + + @Test + fun `compareTo should return 0 when the first condition has a parameter name and value and the second one has a parameter value map with matching parameters`() { + val firstCondition = PathQueryConditionFactory() + .withPathSuffixAndQueryParameter("/abc", PARAMETER_NAME, PARAMETER_VALUE) + val secondCondition = PathQueryConditionFactory() + .withPathSuffixAndQueryParameters("/abc", PARAMETER_MAP_SINGLE) + + assertThat(firstCondition.compareTo(secondCondition)) + .isEqualTo(0) + } + + @Test + fun `compareTo should return 1 when the first condition has a parameter value map with a null value and the second has a parameter value map with non-null values`() { + val firstCondition = PathQueryConditionFactory() + .withPathSuffixAndQueryParameters("/abc", PARAMETER_MAP_MULTIPLE_NULL) + val secondCondition = PathQueryConditionFactory() + .withPathSuffixAndQueryParameters("/abc", PARAMETER_MAP_MULTIPLE) + + assertThat(firstCondition.compareTo(secondCondition)) + .isEqualTo(1) + } + + @Test + fun `compareTo should return -1 when the first condition has a parameter value map with non-null values and the second has a map with a null value`() { + val firstCondition = PathQueryConditionFactory() + .withPathSuffixAndQueryParameters("/abc", PARAMETER_MAP_MULTIPLE) + val secondCondition = PathQueryConditionFactory() + .withPathSuffixAndQueryParameters("/abc", PARAMETER_MAP_MULTIPLE_NULL) + + assertThat(firstCondition.compareTo(secondCondition)) + .isEqualTo(-1) + } +} diff --git a/gradle.properties b/gradle.properties index 430fe14..7a24291 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ GROUP=pl.droidsonroids.testing -VERSION_NAME=1.1.5 +VERSION_NAME=1.1.7 POM_INCEPTION_YEAR=2017 POM_ARTIFACT_ID=mockwebserver-path-dispatcher POM_DESCRIPTION=Mockwebserver path dispatcher