diff --git a/application/src/main/kotlin/application/ReclaredAPICommand.kt b/application/src/main/kotlin/application/ReclaredAPICommand.kt index 719174bd9..7ee45eccb 100644 --- a/application/src/main/kotlin/application/ReclaredAPICommand.kt +++ b/application/src/main/kotlin/application/ReclaredAPICommand.kt @@ -203,7 +203,7 @@ fun findRedeclarations( ): List { val newPathToContractMap = newPaths.map { newPath -> val matchingContracts = contracts.filter { (feature, _) -> - feature.scenarios.map { it.httpRequestPattern.httpUrlPattern!!.path }.any { scenarioPath -> + feature.scenarios.map { it.httpRequestPattern.httpPathPattern!!.path }.any { scenarioPath -> scenarioPath == newPath } }.map { it.second } @@ -231,7 +231,7 @@ fun urlPaths(newerContractYaml: String, contractPath: String): List? { } private fun pathsFromFeature(newContract: Feature) = - newContract.scenarios.map { it.httpRequestPattern.httpUrlPattern!!.path }.sorted().distinct() + newContract.scenarios.map { it.httpRequestPattern.httpPathPattern!!.path }.sorted().distinct() open class CanonicalFile(val file: File) { val path: String = file.path diff --git a/application/src/main/kotlin/application/ValidateViaLogs.kt b/application/src/main/kotlin/application/ValidateViaLogs.kt index d8af23bd2..b87ce6841 100644 --- a/application/src/main/kotlin/application/ValidateViaLogs.kt +++ b/application/src/main/kotlin/application/ValidateViaLogs.kt @@ -28,7 +28,7 @@ class ValidateViaLogs : Callable { override fun call() { val feature = OpenApiSpecification.fromFile(contractPath).toFeature() - val httpUrlMatchers: List> = findMatchingURLMatchers(feature) + val httpUrlMatchers: List> = findMatchingURLMatchers(feature) val requestLogs = parsedJSONArray(File(logDirPath).readText()) @@ -84,7 +84,7 @@ class ValidateViaLogs : Callable { private fun stubFromExpectationLog( log: JSONObjectValue, - httpUrlMatchers: List> + httpUrlMatchers: List> ): Pair? { val status = log.findFirstChildByPath("http-response.status")?.toStringLiteral() @@ -107,14 +107,14 @@ class ValidateViaLogs : Callable { private fun stubFromExpectationLog( stubRequestPathLog: Value, log: JSONObjectValue, - httpUrlMatchers: List> + httpUrlMatchers: List> ): ScenarioStub? { val path = stubRequestPathLog.toStringLiteral() val body = log.findFirstChildByPath("http-request.body") as JSONObjectValue if (httpUrlMatchers.any { (matcher, resolver) -> - matcher.matchesPath(path, resolver) is Result.Success + matcher.matches(path, resolver) is Result.Success }) return mockFromJSON(body.jsonObject) @@ -123,7 +123,7 @@ class ValidateViaLogs : Callable { private fun stubFromRequestLog( path: String, - httpUrlMatchers: List>, + httpUrlMatchers: List>, log: JSONObjectValue ): ScenarioStub? { val headers = log.findFirstChildByPath("http-response.headers") as JSONObjectValue? @@ -133,16 +133,16 @@ class ValidateViaLogs : Callable { return null if (httpUrlMatchers.any { (matcher, resolver) -> - matcher.matches(HttpRequest(path = path), resolver) is Result.Success + matcher.matches(path, resolver) is Result.Success }) return mockFromJSON(log.jsonObject) return null } - private fun findMatchingURLMatchers(feature: Feature): List> { - val httpUrlMatchers: List> = feature.scenarios.map { - Pair(it.httpRequestPattern.httpUrlPattern, it.resolver) + private fun findMatchingURLMatchers(feature: Feature): List> { + val httpUrlMatchers: List> = feature.scenarios.map { + Pair(it.httpRequestPattern.httpPathPattern, it.resolver) }.map { (matcher, resolver) -> Triple(matcher, matcher?.matches(URI.create(urlPathFilter)), resolver) }.filter { diff --git a/core/src/main/kotlin/in/specmatic/conversions/APIKeyInQueryParamSecurityScheme.kt b/core/src/main/kotlin/in/specmatic/conversions/APIKeyInQueryParamSecurityScheme.kt index ce76d95e5..c0259ccaf 100644 --- a/core/src/main/kotlin/in/specmatic/conversions/APIKeyInQueryParamSecurityScheme.kt +++ b/core/src/main/kotlin/in/specmatic/conversions/APIKeyInQueryParamSecurityScheme.kt @@ -29,11 +29,10 @@ class APIKeyInQueryParamSecurityScheme(val name: String, private val apiKey:Stri } return requestPattern.copy( - httpUrlPattern = requestPattern.httpUrlPattern?.copy( - queryPatterns = requestPattern.httpUrlPattern.queryPatterns.plus(name to queryParamValueType) + httpQueryParamPattern = requestPattern.httpQueryParamPattern.copy( + queryPatterns = requestPattern.httpQueryParamPattern.queryPatterns.plus(name to queryParamValueType) ) ) - } override fun isInRow(row: Row): Boolean = row.containsField(name) diff --git a/core/src/main/kotlin/in/specmatic/conversions/OpenApiSpecification.kt b/core/src/main/kotlin/in/specmatic/conversions/OpenApiSpecification.kt index 584d8e3f0..deac79b00 100644 --- a/core/src/main/kotlin/in/specmatic/conversions/OpenApiSpecification.kt +++ b/core/src/main/kotlin/in/specmatic/conversions/OpenApiSpecification.kt @@ -157,7 +157,7 @@ class OpenApiSpecification(private val openApiFilePath: String, private val pars matchingScenarioInfos.isEmpty() -> MatchFailure( Failure( """Scenario: "${specmaticScenarioInfo.scenarioName}" PATH: "${ - specmaticScenarioInfo.httpRequestPattern.httpUrlPattern!!.generatePath(Resolver()) + specmaticScenarioInfo.httpRequestPattern.httpPathPattern!!.generate(Resolver()) }" is not as per included wsdl / OpenApi spec""" ) ) @@ -228,8 +228,8 @@ class OpenApiSpecification(private val openApiFilePath: String, private val pars val (specmaticScenarioInfo, openApiScenarioInfos) = parameters return MatchSuccess(specmaticScenarioInfo to openApiScenarioInfos.map { openApiScenario -> - val queryPattern = openApiScenario.httpRequestPattern.httpUrlPattern?.queryPatterns ?: emptyMap() - val zippedPathPatterns = (specmaticScenarioInfo.httpRequestPattern.httpUrlPattern?.pathSegmentPatterns ?: emptyList()).zip(openApiScenario.httpRequestPattern.httpUrlPattern?.pathSegmentPatterns ?: emptyList()) + val queryPattern = openApiScenario.httpRequestPattern.httpQueryParamPattern?.queryPatterns ?: emptyMap() + val zippedPathPatterns = (specmaticScenarioInfo.httpRequestPattern.httpPathPattern?.pathSegmentPatterns ?: emptyList()).zip(openApiScenario.httpRequestPattern.httpPathPattern?.pathSegmentPatterns ?: emptyList()) val pathPatterns = zippedPathPatterns.map { (fromWrapper, fromOpenApi) -> if(fromWrapper.pattern is ExactValuePattern) @@ -238,9 +238,10 @@ class OpenApiSpecification(private val openApiFilePath: String, private val pars fromOpenApi.copy(key = fromWrapper.key) } - val httpUrlPattern = HttpURLPattern(queryPattern, pathPatterns, openApiScenario.httpRequestPattern.httpUrlPattern?.path ?: "") + val httpPathPattern = HttpPathPattern(pathPatterns, openApiScenario.httpRequestPattern.httpPathPattern?.path ?: "") + val httpQueryParamPattern = HttpQueryParamPattern(queryPattern) - val httpRequestPattern = openApiScenario.httpRequestPattern.copy(httpUrlPattern = httpUrlPattern) + val httpRequestPattern = openApiScenario.httpRequestPattern.copy(httpPathPattern = httpPathPattern, httpQueryParamPattern = httpQueryParamPattern) openApiScenario.copy(httpRequestPattern = httpRequestPattern) }) } @@ -248,11 +249,12 @@ class OpenApiSpecification(private val openApiFilePath: String, private val pars private fun openApiToScenarioInfos(): Pair, Map>>> { val data: List, Map>>>> = openApiPaths().map { (openApiPath, pathItem) -> openApiOperations(pathItem).map { (httpMethod, operation) -> - val specmaticPath = toSpecmaticPath(openApiPath, operation) + val specmaticPathParam = toSpecmaticPathParam(openApiPath, operation) + val specmaticQueryParam = toSpecmaticQueryParam(operation) val httpRequestPatterns: List>>> = toHttpRequestPatterns( - specmaticPath, httpMethod, operation + specmaticPathParam, specmaticQueryParam, httpMethod, operation ) val httpResponsePatterns: List = toHttpResponsePatterns(operation.responses) @@ -339,12 +341,13 @@ class OpenApiSpecification(private val openApiFilePath: String, private val pars val scenarioInfos = openApiPaths().map { (openApiPath, pathItem) -> openApiOperations(pathItem).map { (httpMethod, operation) -> - val specmaticPath = toSpecmaticPath(openApiPath, operation) + val specmaticPathParam = toSpecmaticPathParam(openApiPath, operation) + val specmaticQueryParam = toSpecmaticQueryParam(operation) val requestBody: RequestBody? = resolveRequestBody(operation) val httpResponsePatterns = toHttpResponsePatterns(operation.responses) - val httpRequestPatterns = toHttpRequestPatterns(specmaticPath, httpMethod, operation) + val httpRequestPatterns = toHttpRequestPatterns(specmaticPathParam, specmaticQueryParam, httpMethod, operation) httpResponsePatterns.map { (response, responseMediaType, httpResponsePattern) -> val responseExamples: Map = responseMediaType.examples.orEmpty() @@ -356,7 +359,7 @@ class OpenApiSpecification(private val openApiFilePath: String, private val pars val ignoreFailure = operation.tags.orEmpty().map { it.trim() }.contains("WIP") - val operationIdentifier = OperationIdentifier(httpMethod, specmaticPath.path, httpResponsePattern.status) + val operationIdentifier = OperationIdentifier(httpMethod, specmaticPathParam.path, httpResponsePattern.status) val relevantExternalizedJSONExamples = externalizedJSONExamples[operationIdentifier] val rowsToBeUsed: List = relevantExternalizedJSONExamples ?: specmaticExampleRows @@ -650,7 +653,7 @@ class OpenApiSpecification(private val openApiFilePath: String, private val pars } private fun toHttpRequestPatterns( - httpUrlPattern: HttpURLPattern, httpMethod: String, operation: Operation + httpPathPattern: HttpPathPattern, httpQueryParamPattern: HttpQueryParamPattern, httpMethod: String, operation: Operation ): List>>> { val securitySchemes: Map = @@ -666,7 +669,8 @@ class OpenApiSpecification(private val openApiFilePath: String, private val pars val headersPattern = HttpHeadersPattern(headersMap) val requestPattern = HttpRequestPattern( - httpUrlPattern = httpUrlPattern, + httpPathPattern = httpPathPattern, + httpQueryParamPattern = httpQueryParamPattern, method = httpMethod, headersPattern = headersPattern, securitySchemes = operationSecuritySchemes(operation, securitySchemes) @@ -685,7 +689,7 @@ class OpenApiSpecification(private val openApiFilePath: String, private val pars val pathParams = examplePathParams[exampleName] ?: emptyMap() val headerParams = exampleHeaderParams[exampleName] ?: emptyMap() - val path = pathParams.entries.fold(httpUrlPattern.toOpenApiPath()) { acc, (key, value) -> + val path = pathParams.entries.fold(httpPathPattern.toOpenApiPath()) { acc, (key, value) -> acc.replace("{$key}", value) } @@ -763,7 +767,7 @@ class OpenApiSpecification(private val openApiFilePath: String, private val pars val httpRequest = HttpRequest( method = httpMethod, - path = httpUrlPattern.path, + path = httpPathPattern.path, queryParams = queryParams, body = parsedValue(it.value ?: "") ) @@ -1325,8 +1329,21 @@ class OpenApiSpecification(private val openApiFilePath: String, private val pars private fun componentNameFromReference(component: String) = component.substringAfterLast("/") - private fun toSpecmaticPath(openApiPath: String, operation: Operation): HttpURLPattern { - val parameters = operation.parameters ?: return toURLMatcherWithOptionalQueryParams(openApiPath) + private fun toSpecmaticQueryParam(operation: Operation): HttpQueryParamPattern { + val parameters = operation.parameters ?: return HttpQueryParamPattern(emptyMap()) + val queryPattern: Map = parameters.filterIsInstance(QueryParameter::class.java).associate { + val specmaticPattern: Pattern = if (it.schema.type == "array") { + CsvPattern(toSpecmaticPattern(schema = it.schema.items, typeStack = emptyList())) + } else { + toSpecmaticPattern(schema = it.schema, typeStack = emptyList(), patternName = it.name) + } + + "${it.name}?" to specmaticPattern + } + return HttpQueryParamPattern(queryPattern) + } + private fun toSpecmaticPathParam(openApiPath: String, operation: Operation): HttpPathPattern { + val parameters = operation.parameters ?: return buildHttpPathPattern(openApiPath) val pathStringParts: List = openApiPath.removePrefix("/").removeSuffix("/").let { if (it.isBlank()) @@ -1350,19 +1367,9 @@ class OpenApiSpecification(private val openApiFilePath: String, private val pars } } - val queryPattern: Map = parameters.filterIsInstance(QueryParameter::class.java).associate { - val specmaticPattern: Pattern = if (it.schema.type == "array") { - CsvPattern(toSpecmaticPattern(schema = it.schema.items, typeStack = emptyList())) - } else { - toSpecmaticPattern(schema = it.schema, typeStack = emptyList(), patternName = it.name) - } - - "${it.name}?" to specmaticPattern - } - val specmaticPath = toSpecmaticFormattedPathString(parameters, openApiPath) - return HttpURLPattern(queryPattern, pathPattern, specmaticPath) + return HttpPathPattern(pathPattern, specmaticPath) } private fun toSpecmaticFormattedPathString( diff --git a/core/src/main/kotlin/in/specmatic/core/Feature.kt b/core/src/main/kotlin/in/specmatic/core/Feature.kt index b1ed7d3af..edd99b88d 100644 --- a/core/src/main/kotlin/in/specmatic/core/Feature.kt +++ b/core/src/main/kotlin/in/specmatic/core/Feature.kt @@ -292,12 +292,12 @@ data class Feature( private fun getBadRequestsOrDefault(scenario: Scenario): BadRequestOrDefault? { val badRequestResponses = scenarios.filter { - it.httpRequestPattern.httpUrlPattern!!.path == scenario.httpRequestPattern.httpUrlPattern!!.path + it.httpRequestPattern.httpPathPattern!!.path == scenario.httpRequestPattern.httpPathPattern!!.path && it.httpResponsePattern.status.toString().startsWith("4") }.associate { it.httpResponsePattern.status to it.httpResponsePattern } val defaultResponse: HttpResponsePattern? = scenarios.find { - it.httpRequestPattern.httpUrlPattern!!.path == scenario.httpRequestPattern.httpUrlPattern!!.path + it.httpRequestPattern.httpPathPattern!!.path == scenario.httpRequestPattern.httpPathPattern!!.path && it.httpResponsePattern.status == DEFAULT_RESPONSE_CODE }?.httpResponsePattern @@ -366,16 +366,16 @@ data class Feature( } private fun convergeURLMatcher(baseScenario: Scenario, newScenario: Scenario): Scenario { - if (baseScenario.httpRequestPattern.httpUrlPattern!!.encompasses( - newScenario.httpRequestPattern.httpUrlPattern!!, + if (baseScenario.httpRequestPattern.httpPathPattern!!.encompasses( + newScenario.httpRequestPattern.httpPathPattern!!, baseScenario.resolver, newScenario.resolver ) is Result.Success ) return baseScenario - val basePathParts = baseScenario.httpRequestPattern.httpUrlPattern.pathSegmentPatterns - val newPathParts = newScenario.httpRequestPattern.httpUrlPattern.pathSegmentPatterns + val basePathParts = baseScenario.httpRequestPattern.httpPathPattern.pathSegmentPatterns + val newPathParts = newScenario.httpRequestPattern.httpPathPattern.pathSegmentPatterns val convergedPathPattern: List = basePathParts.zip(newPathParts).map { (base, new) -> if(base.pattern.encompasses(new.pattern, baseScenario.resolver, newScenario.resolver) is Result.Success) @@ -384,7 +384,7 @@ data class Feature( if(isInteger(base) && isInteger(new)) URLPathSegmentPattern(NumberPattern(), key = "id") else - throw ContractException("Can't figure out how to converge these URLs: ${baseScenario.httpRequestPattern.httpUrlPattern.path}, ${newScenario.httpRequestPattern.httpUrlPattern.path}") + throw ContractException("Can't figure out how to converge these URLs: ${baseScenario.httpRequestPattern.httpPathPattern.path}, ${newScenario.httpRequestPattern.httpPathPattern.path}") } } @@ -395,11 +395,11 @@ data class Feature( } }.let { if(it.startsWith("/")) it else "/$it"} - val convergedHttpURLPattern: HttpURLPattern = baseScenario.httpRequestPattern.httpUrlPattern.copy(pathSegmentPatterns = convergedPathPattern, path = convergedPath) + val convergedHttpPathPattern: HttpPathPattern = baseScenario.httpRequestPattern.httpPathPattern.copy(pathSegmentPatterns = convergedPathPattern, path = convergedPath) return baseScenario.copy( httpRequestPattern = baseScenario.httpRequestPattern.copy( - httpUrlPattern = convergedHttpURLPattern + httpPathPattern = convergedHttpPathPattern ) ) } @@ -423,7 +423,7 @@ data class Feature( return if (baseScenario.httpRequestPattern.formFieldsPattern.size == 1) { if (newScenario.httpRequestPattern.formFieldsPattern.size != 1) - throw ContractException("${baseScenario.httpRequestPattern.method} ${baseScenario.httpRequestPattern.httpUrlPattern?.path} exists with different form fields") + throw ContractException("${baseScenario.httpRequestPattern.method} ${baseScenario.httpRequestPattern.httpPathPattern?.path} exists with different form fields") val baseRawPattern = baseScenario.httpRequestPattern.formFieldsPattern.values.first() val resolvedBasePattern = resolvedHop(baseRawPattern, baseScenario.resolver) @@ -432,12 +432,12 @@ data class Feature( val resolvedNewPattern = resolvedHop(newRawPattern, newScenario.resolver) if (isObjectType(resolvedBasePattern) && !isObjectType(resolvedNewPattern)) - throw ContractException("${baseScenario.httpRequestPattern.method} ${baseScenario.httpRequestPattern.httpUrlPattern?.path} exists with multiple payload types") + throw ContractException("${baseScenario.httpRequestPattern.method} ${baseScenario.httpRequestPattern.httpPathPattern?.path} exists with multiple payload types") val converged: Pattern = when { resolvedBasePattern.pattern is String && builtInPatterns.contains(resolvedBasePattern.pattern) -> { if (resolvedBasePattern.pattern != resolvedNewPattern.pattern) - throw ContractException("Cannot converge ${baseScenario.httpRequestPattern.method} ${baseScenario.httpRequestPattern.httpUrlPattern?.path} because there are multiple types of request payloads") + throw ContractException("Cannot converge ${baseScenario.httpRequestPattern.method} ${baseScenario.httpRequestPattern.httpPathPattern?.path} because there are multiple types of request payloads") resolvedBasePattern } @@ -445,10 +445,10 @@ data class Feature( if (baseRawPattern.pattern == newRawPattern.pattern && isObjectType(resolvedBasePattern)) baseRawPattern else - throw ContractException("Cannot converge different types ${baseRawPattern.pattern} and ${newRawPattern.pattern} found in ${baseScenario.httpRequestPattern.method} ${baseScenario.httpRequestPattern.httpUrlPattern?.path}") + throw ContractException("Cannot converge different types ${baseRawPattern.pattern} and ${newRawPattern.pattern} found in ${baseScenario.httpRequestPattern.method} ${baseScenario.httpRequestPattern.httpPathPattern?.path}") } else -> - TODO("Converging of type ${resolvedBasePattern.pattern} and ${resolvedNewPattern.pattern} in ${baseScenario.httpRequestPattern.method} ${baseScenario.httpRequestPattern.httpUrlPattern?.path}") + TODO("Converging of type ${resolvedBasePattern.pattern} and ${resolvedNewPattern.pattern} in ${baseScenario.httpRequestPattern.method} ${baseScenario.httpRequestPattern.httpPathPattern?.path}") } baseScenario.copy( @@ -531,14 +531,14 @@ data class Feature( baseRequestBody is DeferredPattern && newRequestBody is DeferredPattern && baseRequestBody.pattern == newRequestBody.pattern private fun convergeQueryParameters(baseScenario: Scenario, newScenario: Scenario): Scenario { - val baseQueryParams = baseScenario.httpRequestPattern.httpUrlPattern?.queryPatterns!! - val newQueryParams = newScenario.httpRequestPattern.httpUrlPattern?.queryPatterns!! + val baseQueryParams = baseScenario.httpRequestPattern.httpQueryParamPattern?.queryPatterns!! + val newQueryParams = newScenario.httpRequestPattern.httpQueryParamPattern?.queryPatterns!! val convergedQueryParams = convergePatternMap(baseQueryParams, newQueryParams) return baseScenario.copy( httpRequestPattern = baseScenario.httpRequestPattern.copy( - httpUrlPattern = baseScenario.httpRequestPattern.httpUrlPattern.copy(queryPatterns = convergedQueryParams) + httpQueryParamPattern = baseScenario.httpRequestPattern.httpQueryParamPattern.copy(queryPatterns = convergedQueryParams) ) ) } @@ -604,7 +604,7 @@ data class Feature( throw ContractException("Scenario ${it.name} has no method") } - scenarios.find { it.httpRequestPattern.httpUrlPattern == null }?.let { + scenarios.find { it.httpRequestPattern.httpPathPattern == null }?.let { throw ContractException("Scenario ${it.name} has no path") } @@ -616,13 +616,13 @@ data class Feature( }.let { if(it.startsWith("/")) it else "/$it"} val urlPrefixMap = toOpenAPIURLPrefixMap(scenarios.mapNotNull { - it.httpRequestPattern.httpUrlPattern?.path + it.httpRequestPattern.httpPathPattern?.path }.map { normalize(it) }.toSet().toList(), MappedURLType.PATH_ONLY) val payloadAdjustedScenarios: List = scenarios.map { rawScenario -> - val prefix = urlPrefixMap.getValue(normalize(rawScenario.httpRequestPattern.httpUrlPattern?.path!!)) + val prefix = urlPrefixMap.getValue(normalize(rawScenario.httpRequestPattern.httpPathPattern?.path!!)) var scenario = rawScenario @@ -643,7 +643,7 @@ data class Feature( patterns = newTypes, httpRequestPattern = scenario.httpRequestPattern.copy( body = newRequestBody, - httpUrlPattern = numberTemplatized(scenario.httpRequestPattern.httpUrlPattern) + httpPathPattern = numberTemplatized(scenario.httpRequestPattern.httpPathPattern) ) ) } @@ -688,7 +688,7 @@ data class Feature( } val paths: List> = rawCombinedScenarios.fold(emptyList()) { acc, scenario -> - val pathName = scenario.httpRequestPattern.httpUrlPattern!!.toOpenApiPath() + val pathName = scenario.httpRequestPattern.httpPathPattern!!.toOpenApiPath() val existingPathItem = acc.find { it.first == pathName }?.second val pathItem = existingPathItem ?: PathItem() @@ -703,7 +703,7 @@ data class Feature( this.summary = withoutQueryParams(scenario.name) } - val pathParameters = scenario.httpRequestPattern.httpUrlPattern.pathParameters() + val pathParameters = scenario.httpRequestPattern.httpPathPattern.pathParameters() val openApiPathParameters = pathParameters.map { val pathParameter: Parameter = PathParameter() @@ -711,7 +711,7 @@ data class Feature( pathParameter.schema = toOpenApiSchema(it.pattern) pathParameter } - val queryParameters = scenario.httpRequestPattern.httpUrlPattern.queryPatterns + val queryParameters = scenario.httpRequestPattern.httpQueryParamPattern!!.queryPatterns val openApiQueryParameters = queryParameters.map { (key, pattern) -> val queryParameter: Parameter = QueryParameter() queryParameter.name = key.removeSuffix("?") @@ -835,11 +835,11 @@ data class Feature( return name.replace(Regex("""\?.*$"""), "") } - private fun numberTemplatized(httpUrlPattern: HttpURLPattern?): HttpURLPattern { - if(httpUrlPattern!!.pathSegmentPatterns.any { it.pattern !is ExactValuePattern }) - return httpUrlPattern + private fun numberTemplatized(httpPathPattern: HttpPathPattern?): HttpPathPattern { + if(httpPathPattern!!.pathSegmentPatterns.any { it.pattern !is ExactValuePattern }) + return httpPathPattern - val numberTemplatizedPathPattern: List = httpUrlPattern.pathSegmentPatterns.map { type -> + val numberTemplatizedPathPattern: List = httpPathPattern.pathSegmentPatterns.map { type -> if(isInteger(type)) URLPathSegmentPattern(NumberPattern(), key = "id") else @@ -853,7 +853,7 @@ data class Feature( } }.let { if(it.startsWith("/")) it else "/$it"} - return httpUrlPattern.copy(pathSegmentPatterns = numberTemplatizedPathPattern, path = numberTemplatizedPath) + return httpPathPattern.copy(pathSegmentPatterns = numberTemplatizedPathPattern, path = numberTemplatizedPath) } private fun requestBodySchema( @@ -1307,8 +1307,8 @@ private fun lexScenario( when (step.keyword) { in HTTP_METHODS -> { step.words.getOrNull(1)?.let { - val urlPattern = try { - toURLMatcherWithOptionalQueryParams(URI.create(step.rest)) + val pathParamPattern = try { + buildHttpPathPattern(URI.create(step.rest)) } catch (e: Throwable) { throw Exception( "Could not parse the contract URL \"${step.rest}\" in scenario \"${scenarioInfo.scenarioName}\"", @@ -1316,9 +1316,12 @@ private fun lexScenario( ) } + val queryParamPattern = buildQueryPattern(URI.create(step.rest)) + scenarioInfo.copy( httpRequestPattern = scenarioInfo.httpRequestPattern.copy( - httpUrlPattern = urlPattern, + httpPathPattern = pathParamPattern, + httpQueryParamPattern = queryParamPattern, method = step.keyword.uppercase() ) ) @@ -1772,11 +1775,11 @@ private fun List.second(): String { } fun similarURLPath(baseScenario: Scenario, newScenario: Scenario): Boolean { - if(baseScenario.httpRequestPattern.httpUrlPattern?.encompasses(newScenario.httpRequestPattern.httpUrlPattern!!, baseScenario.resolver, newScenario.resolver) is Result.Success) + if(baseScenario.httpRequestPattern.httpPathPattern?.encompasses(newScenario.httpRequestPattern.httpPathPattern!!, baseScenario.resolver, newScenario.resolver) is Result.Success) return true - val basePathParts = baseScenario.httpRequestPattern.httpUrlPattern!!.pathSegmentPatterns - val newPathParts = newScenario.httpRequestPattern.httpUrlPattern!!.pathSegmentPatterns + val basePathParts = baseScenario.httpRequestPattern.httpPathPattern!!.pathSegmentPatterns + val newPathParts = newScenario.httpRequestPattern.httpPathPattern!!.pathSegmentPatterns if(basePathParts.size != newPathParts.size) return false diff --git a/core/src/main/kotlin/in/specmatic/core/HttpPathPattern.kt b/core/src/main/kotlin/in/specmatic/core/HttpPathPattern.kt new file mode 100644 index 000000000..66b2858dc --- /dev/null +++ b/core/src/main/kotlin/in/specmatic/core/HttpPathPattern.kt @@ -0,0 +1,236 @@ +package `in`.specmatic.core + +import `in`.specmatic.core.Result.Failure +import `in`.specmatic.core.Result.Success +import `in`.specmatic.core.pattern.* +import `in`.specmatic.core.value.StringValue +import io.ktor.util.reflect.* +import java.net.URI + +val OMIT = listOf("(OMIT)", "(omit)") + +data class HttpPathPattern( + val pathSegmentPatterns: List, + val path: String +) { + fun encompasses(otherHttpPathPattern: HttpPathPattern, thisResolver: Resolver, otherResolver: Resolver): Result { + if (this.matches(URI.create(otherHttpPathPattern.path), resolver=thisResolver) is Success) + return Success() + + val mismatchedPartResults = + this.pathSegmentPatterns.zip(otherHttpPathPattern.pathSegmentPatterns).map { (thisPathItem, otherPathItem) -> + thisPathItem.pattern.encompasses(otherPathItem, thisResolver, otherResolver) + } + + val failures = mismatchedPartResults.filterIsInstance() + + if (failures.isEmpty()) + return Success() + + return Result.fromFailures(failures) + } + + fun matches(uri: URI, resolver: Resolver = Resolver()): Result { + return matches(uri.path, resolver) + } + + fun matches(path: String, resolver: Resolver): Result { + val httpRequest = HttpRequest(path = path) + return matches(httpRequest, resolver).withFailureReason(FailureReason.URLPathMisMatch) + } + + fun matches(httpRequest: HttpRequest, resolver: Resolver): Result { + val path = httpRequest.path!! + val pathSegments = path.split("/".toRegex()).filter { it.isNotEmpty() }.toTypedArray() + + if (pathSegmentPatterns.size != pathSegments.size) + return Failure( + "Expected $path (having ${pathSegments.size} path segments) to match ${this.path} (which has ${pathSegmentPatterns.size} path segments).", + breadCrumb = "PATH" + ) + + pathSegmentPatterns.zip(pathSegments).forEach { (urlPathPattern, token) -> + try { + val parsedValue = urlPathPattern.tryParse(token, resolver) + val result = resolver.matchesPattern(urlPathPattern.key, urlPathPattern.pattern, parsedValue) + if (result is Failure) { + return when (urlPathPattern.key) { + null -> result.breadCrumb("PATH ($path)") + else -> result.breadCrumb("PATH ($path)").breadCrumb(urlPathPattern.key) + } + } + } catch (e: ContractException) { + e.failure().breadCrumb("PATH ($path)").let { failure -> + urlPathPattern.key?.let { failure.breadCrumb(urlPathPattern.key) } ?: failure + } + } catch (e: Throwable) { + Failure(e.localizedMessage).breadCrumb("PATH ($path)").let { failure -> + urlPathPattern.key?.let { failure.breadCrumb(urlPathPattern.key) } ?: failure + } + } + } + + return Success() + } + + fun generate(resolver: Resolver): String { + return attempt(breadCrumb = "PATH") { + ("/" + pathSegmentPatterns.mapIndexed { index, urlPathPattern -> + attempt(breadCrumb = "[$index]") { + val key = urlPathPattern.key + resolver.withCyclePrevention(urlPathPattern.pattern) { cyclePreventedResolver -> + if (key != null) + cyclePreventedResolver.generate(key, urlPathPattern.pattern) + else urlPathPattern.pattern.generate(cyclePreventedResolver) + } + } + }.joinToString("/")).let { + if (path.endsWith("/") && !it.endsWith("/")) "$it/" else it + }.let { + if (path.startsWith("/") && !it.startsWith("/")) "$/it" else it + } + } + } + + fun newBasedOn( + row: Row, + resolver: Resolver + ): List> { + val generatedPatterns = newBasedOn(pathSegmentPatterns.mapIndexed { index, urlPathParamPattern -> + val key = urlPathParamPattern.key + if (key === null || !row.containsField(key)) return@mapIndexed urlPathParamPattern + attempt(breadCrumb = "[$index]") { + val rowValue = row.getField(key) + when { + isPatternToken(rowValue) -> attempt("Pattern mismatch in example of path param \"${urlPathParamPattern.key}\"") { + val rowPattern = resolver.getPattern(rowValue) + when (val result = urlPathParamPattern.encompasses(rowPattern, resolver, resolver)) { + is Success -> urlPathParamPattern.copy(pattern = rowPattern) + is Failure -> throw ContractException(result.toFailureReport()) + } + } + + else -> attempt("Format error in example of path parameter \"$key\"") { + val value = urlPathParamPattern.parse(rowValue, resolver) + + val matchResult = urlPathParamPattern.matches(value, resolver) + if (matchResult is Failure) + throw ContractException("""Could not run contract test, the example value ${value.toStringLiteral()} provided "id" does not match the contract.""") + + URLPathSegmentPattern( + ExactValuePattern( + value + ) + ) + } + } + } + }, row, resolver) + + //TODO: replace this with Generics + return generatedPatterns.map { list -> list.map { it as URLPathSegmentPattern } } + } + + fun newBasedOn(resolver: Resolver): List> { + val generatedPatterns = newBasedOn(pathSegmentPatterns.mapIndexed { index, urlPathPattern -> + attempt(breadCrumb = "[$index]") { + urlPathPattern + } + }, resolver) + + //TODO: replace this with Generics + return generatedPatterns.map { list -> list.map { it as URLPathSegmentPattern } } + } + + override fun toString(): String { + return path + } + + fun toOpenApiPath(): String { + val pathParamsWithPattern = + this.path.split("/").filter { it.startsWith("(") }.map { it.replace("(", "").replace(")", "").split(":") } + return this.path.replace("(", "{").replace(""":[a-z,A-Z]*?\)""".toRegex(), "}") + } + + fun pathParameters(): List { + return pathSegmentPatterns.filter { !it.pattern.instanceOf(ExactValuePattern::class) } + } + + fun negativeBasedOn( + row: Row, + resolver: Resolver + ): List> { + val newPathPartsList: List> = newBasedOn(pathSegmentPatterns.mapIndexed { index, urlPathPattern -> + val key = urlPathPattern.key + + attempt(breadCrumb = "[$index]") { + when { + key !== null && row.containsField(key) -> { + val rowValue = row.getField(key) + when { + isPatternToken(rowValue) -> attempt("Pattern mismatch in example of path param \"${urlPathPattern.key}\"") { + val rowPattern = resolver.getPattern(rowValue) + when (val result = urlPathPattern.encompasses(rowPattern, resolver, resolver)) { + is Success -> urlPathPattern.copy(pattern = rowPattern) + is Failure -> throw ContractException(result.toFailureReport()) + } + } + + else -> attempt("Format error in example of path parameter \"$key\"") { + val value = urlPathPattern.parse(rowValue, resolver) + + val matchResult = urlPathPattern.matches(value, resolver) + if (matchResult is Failure) + throw ContractException("""Could not run contract test, the example value ${value.toStringLiteral()} provided "id" does not match the contract.""") + + URLPathSegmentPattern( + ExactValuePattern( + value + ) + ) + } + } + } + + else -> urlPathPattern + } + } + }, row, resolver) + + //TODO: Replace with Generics + val newURLPathSegmentPatternsList = newPathPartsList.map { list -> list.map { it as URLPathSegmentPattern } } + return newURLPathSegmentPatternsList + } +} + +internal fun buildHttpPathPattern( + url: String +): HttpPathPattern = + buildHttpPathPattern(URI.create(url)) + +internal fun buildHttpPathPattern( + urlPattern: URI +): HttpPathPattern { + val path = urlPattern.path + val pathPattern = pathToPattern(urlPattern.rawPath) + return HttpPathPattern(path = path, pathSegmentPatterns = pathPattern) +} + +internal fun pathToPattern(rawPath: String): List = + rawPath.trim('/').split("/").filter { it.isNotEmpty() }.map { part -> + when { + isPatternToken(part) -> { + val pieces = withoutPatternDelimiters(part).split(":").map { it.trim() } + if (pieces.size != 2) { + throw ContractException("In path ${rawPath}, $part must be of the format (param_name:type), e.g. (id:number)") + } + + val (name, type) = pieces + + URLPathSegmentPattern(DeferredPattern(withPatternDelimiters(type)), name) + } + + else -> URLPathSegmentPattern(ExactValuePattern(StringValue(part))) + } + } + diff --git a/core/src/main/kotlin/in/specmatic/core/HttpQueryParamPattern.kt b/core/src/main/kotlin/in/specmatic/core/HttpQueryParamPattern.kt new file mode 100644 index 000000000..64a8cc6cd --- /dev/null +++ b/core/src/main/kotlin/in/specmatic/core/HttpQueryParamPattern.kt @@ -0,0 +1,122 @@ +package `in`.specmatic.core + +import `in`.specmatic.core.pattern.* +import `in`.specmatic.core.pattern.withoutOptionality +import `in`.specmatic.core.utilities.URIUtils +import `in`.specmatic.core.value.StringValue +import java.net.URI + +data class HttpQueryParamPattern(val queryPatterns: Map) { + fun generate(resolver: Resolver): Map { + return attempt(breadCrumb = "QUERY-PARAMS") { + queryPatterns.mapKeys { it.key.removeSuffix("?") }.map { (name, pattern) -> + attempt(breadCrumb = name) { + name to resolver.withCyclePrevention(pattern) { it.generate(name, pattern) }.toString() + } + }.toMap() + } + } + + fun newBasedOn( + row: Row, + resolver: Resolver + ): List> { + val newQueryParamsList = attempt(breadCrumb = QUERY_PARAMS_BREADCRUMB) { + val optionalQueryParams = queryPatterns + + forEachKeyCombinationIn(row.withoutOmittedKeys(optionalQueryParams), row) { entry -> + newBasedOn(entry.mapKeys { withoutOptionality(it.key) }, row, resolver) + } + } + return newQueryParamsList + } + + fun matches(httpRequest: HttpRequest, resolver: Resolver): Result { + val keyErrors = + resolver.findKeyErrorList(queryPatterns, httpRequest.queryParams.mapValues { StringValue(it.value) }) + val keyErrorList: List = keyErrors.map { + it.missingKeyToResult("query param", resolver.mismatchMessages).breadCrumb(it.name) + } + val results: List = queryPatterns.keys.map { key -> + val keyName = key.removeSuffix("?") + + if (!httpRequest.queryParams.containsKey(keyName)) + null + else { + try { + val patternValue = queryPatterns.getValue(key) + val sampleValue = httpRequest.queryParams.getValue(keyName) + + val parsedValue = try { + patternValue.parse(sampleValue, resolver) + } catch (e: Exception) { + StringValue(sampleValue) + } + resolver.matchesPattern(keyName, patternValue, parsedValue).breadCrumb(keyName) + } catch (e: ContractException) { + e.failure().breadCrumb(keyName) + } catch (e: Throwable) { + Result.Failure(e.localizedMessage).breadCrumb(keyName) + } + } + } + val failures = keyErrorList.plus(results).filterIsInstance() + return if (failures.isNotEmpty()) + Result.Failure.fromFailures(failures).breadCrumb(QUERY_PARAMS_BREADCRUMB) + else + Result.Success() + } + + fun newBasedOn(resolver: Resolver): List> { + return attempt(breadCrumb = QUERY_PARAMS_BREADCRUMB) { + val optionalQueryParams = queryPatterns + + allOrNothingCombinationIn(optionalQueryParams) { entry -> + newBasedOn(entry.mapKeys { withoutOptionality(it.key) }, resolver) + } + } + } + + override fun toString(): String { + return if (queryPatterns.isNotEmpty()) { + "?" + queryPatterns.mapKeys { it.key.removeSuffix("?") }.map { (key, value) -> + "$key=$value" + }.toList().joinToString(separator = "&") + } else "" + } + + fun negativeBasedOn(row: Row, resolver: Resolver): List> { + return attempt(breadCrumb = QUERY_PARAMS_BREADCRUMB) { + val optionalQueryParams = queryPatterns + + forEachKeyCombinationIn(row.withoutOmittedKeys(optionalQueryParams), row) { entry -> + negativeBasedOn(entry.mapKeys { withoutOptionality(it.key) }, row, resolver, true) + } + } + } + + fun matches(uri: URI, queryParams: Map, resolver: Resolver = Resolver()): Result { + return matches(HttpRequest(path = uri.path, queryParams = queryParams), resolver) + } +} + +internal fun buildQueryPattern( + urlPattern: URI, + apiKeyQueryParams: Set = emptySet() +): HttpQueryParamPattern { + val queryPattern = URIUtils.parseQuery(urlPattern.query).mapKeys { + "${it.key}?" + }.mapValues { + if (isPatternToken(it.value)) + DeferredPattern(it.value, it.key) + else + ExactValuePattern(StringValue(it.value)) + }.let { queryParams -> + apiKeyQueryParams.associate { apiKeyQueryParam -> + Pair("${apiKeyQueryParam}?", StringPattern()) + }.plus(queryParams) + } + return HttpQueryParamPattern(queryPattern) +} + +const val QUERY_PARAMS_BREADCRUMB = "QUERY-PARAMS" \ No newline at end of file diff --git a/core/src/main/kotlin/in/specmatic/core/HttpRequest.kt b/core/src/main/kotlin/in/specmatic/core/HttpRequest.kt index 35d63b5a4..f8911d7dd 100644 --- a/core/src/main/kotlin/in/specmatic/core/HttpRequest.kt +++ b/core/src/main/kotlin/in/specmatic/core/HttpRequest.kt @@ -141,7 +141,8 @@ data class HttpRequest( return HttpRequestPattern( headersPattern = HttpHeadersPattern(mapToPattern(headers)), - httpUrlPattern = HttpURLPattern(mapToPattern(queryParams), pathToPattern(pathForPattern), pathForPattern), + httpPathPattern = HttpPathPattern(pathToPattern(pathForPattern), pathForPattern), + httpQueryParamPattern = HttpQueryParamPattern(mapToPattern(queryParams)), method = this.method, body = this.body.exactMatchElseType(), formFieldsPattern = mapToPattern(formFields), diff --git a/core/src/main/kotlin/in/specmatic/core/HttpRequestPattern.kt b/core/src/main/kotlin/in/specmatic/core/HttpRequestPattern.kt index 7c651df5e..f8528f889 100644 --- a/core/src/main/kotlin/in/specmatic/core/HttpRequestPattern.kt +++ b/core/src/main/kotlin/in/specmatic/core/HttpRequestPattern.kt @@ -19,7 +19,8 @@ data class HeaderMatchParams(val request: HttpRequest, val headersResolver: Reso data class HttpRequestPattern( val headersPattern: HttpHeadersPattern = HttpHeadersPattern(), - val httpUrlPattern: HttpURLPattern? = null, + val httpPathPattern: HttpPathPattern? = null, + val httpQueryParamPattern: HttpQueryParamPattern = HttpQueryParamPattern(emptyMap()), val method: String? = null, val body: Pattern = EmptyStringPattern, val formFieldsPattern: Map = emptyMap(), @@ -70,7 +71,7 @@ data class HttpRequestPattern( } fun matchesSignature(other: HttpRequestPattern): Boolean = - httpUrlPattern!!.path == other.httpUrlPattern!!.path && method.equals(method) + httpPathPattern!!.path == other.httpPathPattern!!.path && method.equals(method) private fun matchMultiPartFormData(parameters: Triple>): MatchingResult>> { val (httpRequest, resolver, failures) = parameters @@ -193,7 +194,7 @@ data class HttpRequestPattern( private fun matchPath(parameters: Pair): MatchingResult> { val (httpRequest, resolver) = parameters - val result = httpUrlPattern!!.matchesPath(httpRequest.path!!, resolver) + val result = httpPathPattern!!.matches(httpRequest.path!!, resolver) return if (result is Failure) MatchFailure(result) @@ -204,10 +205,10 @@ data class HttpRequestPattern( private fun matchQuery(parameters: Triple>): MatchingResult>> { val (httpRequest, resolver, failures) = parameters - val result = httpUrlPattern!!.matchesQuery(httpRequest, resolver) + val result = httpQueryParamPattern!!.matches(httpRequest, resolver) return if (result is Failure) - MatchSuccess(Triple(httpRequest, resolver, failures.plus(result.breadCrumb("QUERY-PARAMS")))) + MatchSuccess(Triple(httpRequest, resolver, failures.plus(result))) else MatchSuccess(parameters) } @@ -219,22 +220,22 @@ data class HttpRequestPattern( if (method == null) { throw missingParam("HTTP method") } - if (httpUrlPattern == null) { + if (httpPathPattern == null) { throw missingParam("URL path") } + if (httpQueryParamPattern == null) { + throw missingParam("URL query") + } requestType = requestType.copy(method = request.method) requestType = attempt(breadCrumb = "URL") { val path = request.path ?: "" val pathTypes = pathToPattern(path) - val queryParamTypes = toTypeMap( - request.queryParams, - httpUrlPattern.queryPatterns, - resolver - ).mapKeys { it.key.removeSuffix("?") } + val queryParamTypes = toTypeMap(request.queryParams, httpQueryParamPattern.queryPatterns, resolver) + .mapKeys { it.key.removeSuffix("?") } - requestType.copy(httpUrlPattern = HttpURLPattern(queryParamTypes, pathTypes, path)) + requestType.copy(httpPathPattern = HttpPathPattern(pathTypes, path), httpQueryParamPattern = HttpQueryParamPattern(queryParamTypes)) } requestType = attempt(breadCrumb = "HEADERS") { @@ -316,13 +317,16 @@ data class HttpRequestPattern( if (method == null) { throw missingParam("HTTP method") } - if (httpUrlPattern == null) { + if (httpPathPattern == null) { throw missingParam("URL path") } + if (httpQueryParamPattern == null) { + throw missingParam("Query params") + } newRequest = newRequest.updateMethod(method) attempt(breadCrumb = "URL") { - newRequest = newRequest.updatePath(httpUrlPattern.generatePath(resolver)) - val queryParams = httpUrlPattern.generateQuery(resolver) + newRequest = newRequest.updatePath(httpPathPattern.generate(resolver)) + val queryParams = httpQueryParamPattern.generate(resolver) for (key in queryParams.keys) { newRequest = newRequest.updateQueryParam(key, queryParams[key] ?: "") } @@ -383,26 +387,30 @@ data class HttpRequestPattern( } return attempt(breadCrumb = "REQUEST") { - val newHttpURLPatterns = httpUrlPattern?.newBasedOn(row, resolver) ?: listOf(null) - val newBodies: List = attempt(breadCrumb = "BODY") { + val newHttpPathPatterns = httpPathPattern?.let { httpPathPattern -> + val newURLPathSegmentPatternsList = httpPathPattern.newBasedOn(row, resolver) + newURLPathSegmentPatternsList.map { HttpPathPattern(it, httpPathPattern.path) } + } ?: listOf(null) + + val newQueryParamsPatterns = httpQueryParamPattern.newBasedOn(row, resolver).map { HttpQueryParamPattern(it) } + + val newBodies: List = attempt(breadCrumb = "BODY") { body.let { - if(it is DeferredPattern && row.containsField(it.pattern)) { + if (it is DeferredPattern && row.containsField(it.pattern)) { val example = row.getField(it.pattern) listOf(ExactValuePattern(it.parse(example, resolver))) - } - else if(it.typeAlias?.let { p -> isPatternToken(p) } == true && row.containsField(it.typeAlias!!)) { + } else if (it.typeAlias?.let { p -> isPatternToken(p) } == true && row.containsField(it.typeAlias!!)) { val example = row.getField(it.typeAlias!!) listOf(ExactValuePattern(it.parse(example, resolver))) - } - else if(it is XMLPattern && it.referredType?.let { referredType -> row.containsField("($referredType)") } == true) { + } else if (it is XMLPattern && it.referredType?.let { referredType -> row.containsField("($referredType)") } == true) { val referredType = "(${it.referredType})" val example = row.getField(referredType) listOf(ExactValuePattern(it.parse(example, resolver))) - } else if(row.containsField("(REQUEST-BODY)")) { + } else if (row.containsField("(REQUEST-BODY)")) { val example = row.getField("(REQUEST-BODY)") val value = it.parse(example, resolver) - if(! isInvalidRequestResponse(status)) { + if (!isInvalidRequestResponse(status)) { val result = body.matches(value, resolver) if (result is Failure) throw ContractException(result.toFailureReport()) @@ -421,27 +429,30 @@ data class HttpRequestPattern( val newFormFieldsPatterns = newBasedOn(formFieldsPattern, row, resolver) val newFormDataPartLists = newMultiPartBasedOn(multiPartFormDataPattern, row, resolver) - newHttpURLPatterns.flatMap { newURLMatcher -> - newBodies.flatMap { newBody -> - newHeadersPattern.flatMap { newHeadersPattern -> - newFormFieldsPatterns.flatMap { newFormFieldsPattern -> - newFormDataPartLists.flatMap { newFormDataPartList -> - val newRequestPattern = HttpRequestPattern( - headersPattern = newHeadersPattern, - httpUrlPattern = newURLMatcher, - method = method, - body = newBody, - formFieldsPattern = newFormFieldsPattern, - multiPartFormDataPattern = newFormDataPartList - ) - - val schemeInRow = securitySchemes.find { it.isInRow(row) } - - if(schemeInRow != null) { - listOf(schemeInRow.addTo(newRequestPattern, row)) - } else { - securitySchemes.map { - newRequestPattern.copy(securitySchemes = listOf(it)) + newHttpPathPatterns.flatMap { newPathParamPattern -> + newQueryParamsPatterns.flatMap { newQueryParamPattern -> + newBodies.flatMap { newBody -> + newHeadersPattern.flatMap { newHeadersPattern -> + newFormFieldsPatterns.flatMap { newFormFieldsPattern -> + newFormDataPartLists.flatMap { newFormDataPartList -> + val newRequestPattern = HttpRequestPattern( + headersPattern = newHeadersPattern, + httpPathPattern = newPathParamPattern, + httpQueryParamPattern = newQueryParamPattern, + method = method, + body = newBody, + formFieldsPattern = newFormFieldsPattern, + multiPartFormDataPattern = newFormDataPartList + ) + + val schemeInRow = securitySchemes.find { it.isInRow(row) } + + if (schemeInRow != null) { + listOf(schemeInRow.addTo(newRequestPattern, row)) + } else { + securitySchemes.map { + newRequestPattern.copy(securitySchemes = listOf(it)) + } } } } @@ -458,7 +469,12 @@ data class HttpRequestPattern( fun newBasedOn(resolver: Resolver): List { return attempt(breadCrumb = "REQUEST") { - val newHttpURLPatterns = httpUrlPattern?.newBasedOn(resolver) ?: listOf(null) + val newHttpPathPatterns = httpPathPattern?.let { httpPathPattern -> + val newURLPathSegmentPatternsList = httpPathPattern.newBasedOn(resolver) + newURLPathSegmentPatternsList.map { HttpPathPattern(it, httpPathPattern.path) } + } ?: listOf(null) + + val newQueryParamsPatterns = httpQueryParamPattern.newBasedOn(resolver).map { HttpQueryParamPattern(it) } val newBodies = attempt(breadCrumb = "BODY") { resolver.withCyclePrevention(body) { cyclePreventedResolver -> body.newBasedOn(cyclePreventedResolver) @@ -469,22 +485,25 @@ data class HttpRequestPattern( //TODO: Backward Compatibility val newFormDataPartLists = newMultiPartBasedOn(multiPartFormDataPattern, Row(), resolver) - newHttpURLPatterns.flatMap { newURLMatcher -> - newBodies.flatMap { newBody -> - newHeadersPattern.flatMap { newHeadersPattern -> - newFormFieldsPatterns.flatMap { newFormFieldsPattern -> - newFormDataPartLists.flatMap { newFormDataPartList -> - val newRequestPattern = HttpRequestPattern( - headersPattern = newHeadersPattern, - httpUrlPattern = newURLMatcher, - method = method, - body = newBody, - formFieldsPattern = newFormFieldsPattern, - multiPartFormDataPattern = newFormDataPartList - ) - - securitySchemes.map { - newRequestPattern.copy(securitySchemes = listOf(it)) + newHttpPathPatterns.flatMap { newPathParamPattern -> + newQueryParamsPatterns.flatMap { newQueryParamPattern -> + newBodies.flatMap { newBody -> + newHeadersPattern.flatMap { newHeadersPattern -> + newFormFieldsPatterns.flatMap { newFormFieldsPattern -> + newFormDataPartLists.flatMap { newFormDataPartList -> + val newRequestPattern = HttpRequestPattern( + headersPattern = newHeadersPattern, + httpPathPattern = newPathParamPattern, + httpQueryParamPattern = newQueryParamPattern, + method = method, + body = newBody, + formFieldsPattern = newFormFieldsPattern, + multiPartFormDataPattern = newFormDataPartList + ) + + securitySchemes.map { + newRequestPattern.copy(securitySchemes = listOf(it)) + } } } } @@ -495,42 +514,47 @@ data class HttpRequestPattern( } fun testDescription(): String { - return "$method ${httpUrlPattern.toString()}" + return "$method ${httpPathPattern.toString()}" } fun negativeBasedOn(row: Row, resolver: Resolver): List { return attempt(breadCrumb = "REQUEST") { - val newHttpURLPatterns = httpUrlPattern?.negativeBasedOn(row, resolver) ?: listOf(null) + val newHttpPathPatterns = httpPathPattern?.let { httpPathPattern -> + val newURLPathSegmentPatternsList = httpPathPattern.negativeBasedOn(row, resolver) + newURLPathSegmentPatternsList.map { HttpPathPattern(it, httpPathPattern.path) } + } ?: listOf(null) + + val newQueryParamsPatterns = httpQueryParamPattern.negativeBasedOn(row, resolver).map { HttpQueryParamPattern(it) } + val newBodies: List = attempt(breadCrumb = "BODY") { body.let { - if(it is DeferredPattern && row.containsField(it.pattern)) { + if (it is DeferredPattern && row.containsField(it.pattern)) { val example = row.getField(it.pattern) listOf(ExactValuePattern(it.parse(example, resolver))) - } - else if(it.typeAlias?.let { p->isPatternToken(p) } == true && row.containsField(it.typeAlias!!)) { + } else if (it.typeAlias?.let { p -> isPatternToken(p) } == true && row.containsField(it.typeAlias!!)) { val example = row.getField(it.typeAlias!!) listOf(ExactValuePattern(it.parse(example, resolver))) - } - else if(it is XMLPattern && it.referredType?.let { referredType -> row.containsField("($referredType)") } == true) { + } else if (it is XMLPattern && it.referredType?.let { referredType -> row.containsField("($referredType)") } == true) { val referredType = "(${it.referredType})" val example = row.getField(referredType) listOf(ExactValuePattern(it.parse(example, resolver))) - } else if(row.containsField("(REQUEST-BODY)")) { + } else if (row.containsField("(REQUEST-BODY)")) { val example = row.getField("(REQUEST-BODY)") val value = it.parse(example, resolver) val result = body.matches(value, resolver) - if(result is Failure) + if (result is Failure) throw ContractException(result.toFailureReport()) - val originalRequest = if(value is JSONObjectValue) { + val originalRequest = if (value is JSONObjectValue) { body.negativeBasedOn(row.noteRequestBody(), resolver) } else { listOf(ExactValuePattern(value)) } - val flattenedRequests: List = resolver.withCyclePrevention(body) { cyclePreventedResolver -> - body.newBasedOn(row.noteRequestBody(), cyclePreventedResolver) - } + val flattenedRequests: List = + resolver.withCyclePrevention(body) { cyclePreventedResolver -> + body.newBasedOn(row.noteRequestBody(), cyclePreventedResolver) + } flattenedRequests.plus(originalRequest) @@ -545,18 +569,23 @@ data class HttpRequestPattern( val newFormFieldsPatterns = newBasedOn(formFieldsPattern, row, resolver) val newFormDataPartLists = newMultiPartBasedOn(multiPartFormDataPattern, row, resolver) - - val positivePattern:HttpRequestPattern = newBasedOn(row, resolver).first() + //TODO: figure out a way to optimise generating all positive scenarios + val positivePattern: HttpRequestPattern = newBasedOn(row, resolver).first() val negativeRequestPatterns = mutableListOf() - newHttpURLPatterns.forEach { newUrlMatcher -> + newHttpPathPatterns.forEach { pathParamPattern -> + negativeRequestPatterns.add( + positivePattern.copy(httpPathPattern = pathParamPattern) + ) + } + newQueryParamsPatterns.forEach { queryParamPattern -> negativeRequestPatterns.add( - positivePattern.copy(httpUrlPattern = newUrlMatcher) + positivePattern.copy(httpQueryParamPattern = queryParamPattern) ) } - newBodies.forEach { newbodyPattern -> + newBodies.forEach { newBodyPattern -> negativeRequestPatterns.add( - positivePattern.copy(body = newbodyPattern) + positivePattern.copy(body = newBodyPattern) ) } newHeadersPattern.forEach { newHeaderPattern -> diff --git a/core/src/main/kotlin/in/specmatic/core/HttpURLPattern.kt b/core/src/main/kotlin/in/specmatic/core/HttpURLPattern.kt deleted file mode 100644 index 6de45334a..000000000 --- a/core/src/main/kotlin/in/specmatic/core/HttpURLPattern.kt +++ /dev/null @@ -1,377 +0,0 @@ -package `in`.specmatic.core - -import `in`.specmatic.core.Result.Failure -import `in`.specmatic.core.Result.Success -import `in`.specmatic.core.pattern.* -import `in`.specmatic.core.utilities.URIUtils -import `in`.specmatic.core.value.StringValue -import io.ktor.util.reflect.* -import java.net.URI - -const val QUERY_PARAMS_BREADCRUMB = "QUERY-PARAMS" -val OMIT = listOf("(OMIT)", "(omit)") - -data class HttpURLPattern( - val queryPatterns: Map, - val pathSegmentPatterns: List, - val path: String -) { - fun encompasses(otherHttpURLPattern: HttpURLPattern, thisResolver: Resolver, otherResolver: Resolver): Result { - if (this.matches(HttpRequest("GET", URI.create(otherHttpURLPattern.path)), thisResolver) is Success) - return Success() - - val mismatchedPartResults = - this.pathSegmentPatterns.zip(otherHttpURLPattern.pathSegmentPatterns).map { (thisPathItem, otherPathItem) -> - thisPathItem.pattern.encompasses(otherPathItem, thisResolver, otherResolver) - } - - val failures = mismatchedPartResults.filterIsInstance() - - if (failures.isEmpty()) - return Success() - - return Result.fromFailures(failures) - } - - fun matches(uri: URI, sampleQuery: Map = emptyMap(), resolver: Resolver = Resolver()): Result { - val httpRequest = HttpRequest(path = uri.path, queryParams = sampleQuery) - return matches(httpRequest, resolver) - } - - fun matchesPath(path: String, resolver: Resolver): Result { - return HttpRequest(path = path) to resolver to - ::matchesPath otherwise - ::handleError toResult - ::returnResult - } - - fun matches(httpRequest: HttpRequest, resolver: Resolver): Result { - return httpRequest to resolver to - ::matchesPath then - ::matchesQuery otherwise - ::handleError toResult - ::returnResult - } - - fun matchesPath(parameters: Pair): MatchingResult> { - val (httpRequest, resolver) = parameters - return when (val pathResult = matchesPath(URI(httpRequest.path!!), resolver)) { - is Failure -> MatchFailure(pathResult.copy(failureReason = FailureReason.URLPathMisMatch)) - else -> MatchSuccess(parameters) - } - } - - fun matchesQuery(parameters: Pair): MatchingResult> { - val (httpRequest, resolver) = parameters - return when (val queryResult = matchesQuery(httpRequest, resolver)) { - is Failure -> MatchFailure(queryResult.breadCrumb(QUERY_PARAMS_BREADCRUMB)) - else -> MatchSuccess(parameters) - } - } - - fun matchesQuery(httpRequest: HttpRequest, resolver: Resolver): Result { - return matchesQuery(queryPatterns, httpRequest.queryParams, resolver) - } - - private fun matchesPath(uri: URI, resolver: Resolver): Result { - val pathParts = uri.path.split("/".toRegex()).filter { it.isNotEmpty() }.toTypedArray() - - if (pathSegmentPatterns.size != pathParts.size) - return Failure( - "Expected $uri (having ${pathParts.size} path segments) to match $path (which has ${pathSegmentPatterns.size} path segments).", - breadCrumb = "PATH" - ) - - pathSegmentPatterns.zip(pathParts).forEach { (urlPathPattern, token) -> - try { - val parsedValue = urlPathPattern.tryParse(token, resolver) - val result = resolver.matchesPattern(urlPathPattern.key, urlPathPattern.pattern, parsedValue) - if (result is Failure) { - return when (urlPathPattern.key) { - null -> result.breadCrumb("PATH ($uri)") - else -> result.breadCrumb("PATH ($uri)").breadCrumb(urlPathPattern.key) - } - } - } catch (e: ContractException) { - e.failure().breadCrumb("PATH ($uri)").let { failure -> - urlPathPattern.key?.let { failure.breadCrumb(urlPathPattern.key) } ?: failure - } - } catch (e: Throwable) { - Failure(e.localizedMessage).breadCrumb("PATH ($uri)").let { failure -> - urlPathPattern.key?.let { failure.breadCrumb(urlPathPattern.key) } ?: failure - } - } - } - - return Success() - } - - fun generatePath(resolver: Resolver): String { - return attempt(breadCrumb = "PATH") { - ("/" + pathSegmentPatterns.mapIndexed { index, urlPathPattern -> - attempt(breadCrumb = "[$index]") { - val key = urlPathPattern.key - resolver.withCyclePrevention(urlPathPattern.pattern) { cyclePreventedResolver -> - if (key != null) - cyclePreventedResolver.generate(key, urlPathPattern.pattern) - else urlPathPattern.pattern.generate(cyclePreventedResolver) - } - } - }.joinToString("/")).let { - if (path.endsWith("/") && !it.endsWith("/")) "$it/" else it - }.let { - if (path.startsWith("/") && !it.startsWith("/")) "$/it" else it - } - } - } - - fun generateQuery(resolver: Resolver): Map { - return attempt(breadCrumb = "QUERY-PARAMS") { - queryPatterns.mapKeys { it.key.removeSuffix("?") }.map { (name, pattern) -> - attempt(breadCrumb = name) { - name to resolver.withCyclePrevention(pattern) { it.generate(name, pattern) }.toString() - } - }.toMap() - } - } - - fun newBasedOn(row: Row, resolver: Resolver): List { - val generatedPatterns = newBasedOn(pathSegmentPatterns.mapIndexed { index, urlPathParamPattern -> - val key = urlPathParamPattern.key - if (key === null || !row.containsField(key)) return@mapIndexed urlPathParamPattern - attempt(breadCrumb = "[$index]") { - val rowValue = row.getField(key) - when { - isPatternToken(rowValue) -> attempt("Pattern mismatch in example of path param \"${urlPathParamPattern.key}\"") { - val rowPattern = resolver.getPattern(rowValue) - when (val result = urlPathParamPattern.encompasses(rowPattern, resolver, resolver)) { - is Success -> urlPathParamPattern.copy(pattern = rowPattern) - is Failure -> throw ContractException(result.toFailureReport()) - } - } - - else -> attempt("Format error in example of path parameter \"$key\"") { - val value = urlPathParamPattern.parse(rowValue, resolver) - - val matchResult = urlPathParamPattern.matches(value, resolver) - if (matchResult is Failure) - throw ContractException("""Could not run contract test, the example value ${value.toStringLiteral()} provided "id" does not match the contract.""") - - URLPathSegmentPattern( - ExactValuePattern( - value - ) - ) - } - } - } - }, row, resolver) - - //TODO: replace this with Generics - val generatedURLPathSegmentPatterns = generatedPatterns.map { list -> list.map { it as URLPathSegmentPattern } } - - val newQueryParamsList = attempt(breadCrumb = QUERY_PARAMS_BREADCRUMB) { - val optionalQueryParams = queryPatterns - - forEachKeyCombinationIn(row.withoutOmittedKeys(optionalQueryParams), row) { entry -> - newBasedOn(entry.mapKeys { withoutOptionality(it.key) }, row, resolver) - } - } - - return generatedURLPathSegmentPatterns.flatMap { urlPathSegmentPatterns -> - newQueryParamsList.map { newQueryParams -> - HttpURLPattern(newQueryParams, urlPathSegmentPatterns, path) - } - } - } - - fun newBasedOn(resolver: Resolver): List { - val generatedPatterns = newBasedOn(pathSegmentPatterns.mapIndexed { index, urlPathPattern -> - attempt(breadCrumb = "[$index]") { - urlPathPattern - } - }, resolver) - - //TODO: replace this with Generics - val generatedURLPathSegmentPatterns = generatedPatterns.map { list -> list.map { it as URLPathSegmentPattern } } - - val newQueryParamsList = attempt(breadCrumb = QUERY_PARAMS_BREADCRUMB) { - val optionalQueryParams = queryPatterns - - allOrNothingCombinationIn(optionalQueryParams) { entry -> - newBasedOn(entry.mapKeys { withoutOptionality(it.key) }, resolver) - } - } - - return generatedURLPathSegmentPatterns.flatMap { newURLPathPatterns -> - newQueryParamsList.map { newQueryParams -> - HttpURLPattern(newQueryParams, newURLPathPatterns, path) - } - } - } - - override fun toString(): String { - val stringizedQuery = if (queryPatterns.isNotEmpty()) { - "?" + queryPatterns.mapKeys { it.key.removeSuffix("?") }.map { (key, value) -> - "$key=$value" - }.toList().joinToString(separator = "&") - } else "" - - return path + stringizedQuery - } - - fun toOpenApiPath(): String { - val pathParamsWithPattern = - this.path.split("/").filter { it.startsWith("(") }.map { it.replace("(", "").replace(")", "").split(":") } - return this.path.replace("(", "{").replace(""":[a-z,A-Z]*?\)""".toRegex(), "}") - } - - fun pathParameters(): List { - return pathSegmentPatterns.filter { !it.pattern.instanceOf(ExactValuePattern::class) } - } - - fun negativeBasedOn(row: Row, resolver: Resolver): List { - val newPathPartsList: List> = newBasedOn(pathSegmentPatterns.mapIndexed { index, urlPathPattern -> - val key = urlPathPattern.key - - attempt(breadCrumb = "[$index]") { - when { - key !== null && row.containsField(key) -> { - val rowValue = row.getField(key) - when { - isPatternToken(rowValue) -> attempt("Pattern mismatch in example of path param \"${urlPathPattern.key}\"") { - val rowPattern = resolver.getPattern(rowValue) - when (val result = urlPathPattern.encompasses(rowPattern, resolver, resolver)) { - is Success -> urlPathPattern.copy(pattern = rowPattern) - is Failure -> throw ContractException(result.toFailureReport()) - } - } - - else -> attempt("Format error in example of path parameter \"$key\"") { - val value = urlPathPattern.parse(rowValue, resolver) - - val matchResult = urlPathPattern.matches(value, resolver) - if (matchResult is Failure) - throw ContractException("""Could not run contract test, the example value ${value.toStringLiteral()} provided "id" does not match the contract.""") - - URLPathSegmentPattern( - ExactValuePattern( - value - ) - ) - } - } - } - - else -> urlPathPattern - } - } - }, row, resolver) - - val newURLPathSegmentPatternsList = newPathPartsList.map { list -> list.map { it as URLPathSegmentPattern } } - - val newQueryParamsList = attempt(breadCrumb = QUERY_PARAMS_BREADCRUMB) { - val optionalQueryParams = queryPatterns - - forEachKeyCombinationIn(row.withoutOmittedKeys(optionalQueryParams), row) { entry -> - negativeBasedOn(entry.mapKeys { withoutOptionality(it.key) }, row, resolver, true) - } - } - - return newURLPathSegmentPatternsList.flatMap { newURLPathPatterns -> - newQueryParamsList.map { newQueryParams -> - HttpURLPattern(newQueryParams, newURLPathPatterns, path) - } - } - } -} - -internal fun toURLMatcherWithOptionalQueryParams( - url: String, - apiKeyQueryParams: Set = emptySet() -): HttpURLPattern = - toURLMatcherWithOptionalQueryParams(URI.create(url), apiKeyQueryParams) - -internal fun toURLMatcherWithOptionalQueryParams( - urlPattern: URI, - apiKeyQueryParams: Set = emptySet() -): HttpURLPattern { - val path = urlPattern.path - - val pathPattern = pathToPattern(urlPattern.rawPath) - - val queryPattern = URIUtils.parseQuery(urlPattern.query).mapKeys { - "${it.key}?" - }.mapValues { - if (isPatternToken(it.value)) - DeferredPattern(it.value, it.key) - else - ExactValuePattern(StringValue(it.value)) - }.let { queryParams -> - apiKeyQueryParams.map { apiKeyQueryParam -> - Pair("${apiKeyQueryParam}?", StringPattern()) - }.toMap().plus(queryParams) - } - - return HttpURLPattern(queryPatterns = queryPattern, path = path, pathSegmentPatterns = pathPattern) -} - -internal fun pathToPattern(rawPath: String): List = - rawPath.trim('/').split("/").filter { it.isNotEmpty() }.map { part -> - when { - isPatternToken(part) -> { - val pieces = withoutPatternDelimiters(part).split(":").map { it.trim() } - if (pieces.size != 2) { - throw ContractException("In path ${rawPath}, $part must be of the format (param_name:type), e.g. (id:number)") - } - - val (name, type) = pieces - - URLPathSegmentPattern(DeferredPattern(withPatternDelimiters(type)), name) - } - - else -> URLPathSegmentPattern(ExactValuePattern(StringValue(part))) - } - } - -internal fun matchesQuery( - queryPattern: Map, - sampleQuery: Map, - resolver: Resolver -): Result { - val keyErrors = resolver.findKeyErrorList(queryPattern, sampleQuery.mapValues { StringValue(it.value) }) - val keyErrorList: List = keyErrors.map { - it.missingKeyToResult("query param", resolver.mismatchMessages).breadCrumb(it.name) - } - - val results: List = queryPattern.keys.map { key -> - val keyName = key.removeSuffix("?") - - if (!sampleQuery.containsKey(keyName)) - null - else { - try { - val patternValue = queryPattern.getValue(key) - val sampleValue = sampleQuery.getValue(keyName) - - val parsedValue = try { - patternValue.parse(sampleValue, resolver) - } catch (e: Exception) { - StringValue(sampleValue) - } - resolver.matchesPattern(keyName, patternValue, parsedValue).breadCrumb(keyName) - } catch (e: ContractException) { - e.failure().breadCrumb(keyName) - } catch (e: Throwable) { - Failure(e.localizedMessage).breadCrumb(keyName) - } - } - } - - val failures = keyErrorList.plus(results).filterIsInstance() - - return if (failures.isNotEmpty()) - Failure.fromFailures(failures) - else - Success() -} diff --git a/core/src/main/kotlin/in/specmatic/core/Result.kt b/core/src/main/kotlin/in/specmatic/core/Result.kt index 95007ba91..c6f6eb84a 100644 --- a/core/src/main/kotlin/in/specmatic/core/Result.kt +++ b/core/src/main/kotlin/in/specmatic/core/Result.kt @@ -69,6 +69,7 @@ sealed class Result { abstract fun isPartialSuccess(): Boolean abstract fun testResult(): TestResult + abstract fun withFailureReason(urlPathMisMatch: FailureReason): Result data class FailureCause(val message: String="", var cause: Failure? = null) @@ -111,6 +112,10 @@ sealed class Result { return TestResult.Failed } + override fun withFailureReason(failureReason: FailureReason): Result { + return copy(failureReason = failureReason) + } + fun reason(errorMessage: String) = Failure(errorMessage, this) override fun breadCrumb(breadCrumb: String) = Failure(cause = this, breadCrumb = breadCrumb) override fun failureReason(failureReason: FailureReason?): Result { @@ -178,6 +183,10 @@ sealed class Result { override fun testResult(): TestResult { return TestResult.Success } + + override fun withFailureReason(urlPathMisMatch: FailureReason): Result { + return this + } } } diff --git a/core/src/main/kotlin/in/specmatic/core/Scenario.kt b/core/src/main/kotlin/in/specmatic/core/Scenario.kt index 6deb68812..5eff988e4 100644 --- a/core/src/main/kotlin/in/specmatic/core/Scenario.kt +++ b/core/src/main/kotlin/in/specmatic/core/Scenario.kt @@ -86,7 +86,7 @@ data class Scenario( override val path: String get() { - return httpRequestPattern.httpUrlPattern?.path ?: "" + return httpRequestPattern.httpPathPattern?.path ?: "" } override val status: Int @@ -389,7 +389,7 @@ data class Scenario( response: HttpResponse, mismatchMessages: MismatchMessages = DefaultMismatchMessages ): Result { - return scenarioBreadCrumb(this) { + scenarioBreadCrumb(this) { val resolver = Resolver( IgnoreFacts(), true, @@ -434,7 +434,7 @@ data class Scenario( override fun testDescription(): String { val method = this.httpRequestPattern.method - val path = this.httpRequestPattern.httpUrlPattern?.path ?: "" + val path = this.httpRequestPattern.httpPathPattern?.path ?: "" val responseStatus = this.httpResponsePattern.status val exampleIdentifier = if(exampleName.isNullOrBlank()) "" else { " | EX:${exampleName.trim()}" } diff --git a/core/src/main/kotlin/in/specmatic/core/ScenarioInfo.kt b/core/src/main/kotlin/in/specmatic/core/ScenarioInfo.kt index 6af62fd3f..3e3177476 100644 --- a/core/src/main/kotlin/in/specmatic/core/ScenarioInfo.kt +++ b/core/src/main/kotlin/in/specmatic/core/ScenarioInfo.kt @@ -27,8 +27,8 @@ data class ScenarioInfo( fun matchesGherkinWrapperPath(scenarioInfos: List, apiSpecification: ApiSpecification): List = scenarioInfos.filter { openApiScenarioInfo -> - val pathPatternFromOpenApi = openApiScenarioInfo.httpRequestPattern.httpUrlPattern!!.pathSegmentPatterns - val pathPatternFromWrapper = this.httpRequestPattern.httpUrlPattern!!.pathSegmentPatterns + val pathPatternFromOpenApi = openApiScenarioInfo.httpRequestPattern.httpPathPattern!!.pathSegmentPatterns + val pathPatternFromWrapper = this.httpRequestPattern.httpPathPattern!!.pathSegmentPatterns if(pathPatternFromOpenApi.size != pathPatternFromWrapper.size) return@filter false @@ -44,7 +44,7 @@ data class ScenarioInfo( Pair("exact", "exact") -> apiSpecification.exactValuePatternsAreEqual(openapiURLPart, wrapperURLPart) Pair("exact", "pattern") -> false Pair("pattern", "exact") -> { - attempt("Error matching url ${this.httpRequestPattern.httpUrlPattern.path} to the specification") { + attempt("Error matching url ${this.httpRequestPattern.httpPathPattern.path} to the specification") { apiSpecification.patternMatchesExact( wrapperURLPart, openapiURLPart, diff --git a/core/src/main/kotlin/in/specmatic/stub/HttpStub.kt b/core/src/main/kotlin/in/specmatic/stub/HttpStub.kt index 5f6466b22..0c27ea49c 100644 --- a/core/src/main/kotlin/in/specmatic/stub/HttpStub.kt +++ b/core/src/main/kotlin/in/specmatic/stub/HttpStub.kt @@ -344,7 +344,7 @@ class HttpStub( return result.response } - private suspend fun handleExpectationCreationRequest(httpRequest: HttpRequest): HttpStubResponse { + private fun handleExpectationCreationRequest(httpRequest: HttpRequest): HttpStubResponse { return try { if (httpRequest.body.toStringLiteral().isEmpty()) throw ContractException("Expectation payload was empty") @@ -352,7 +352,7 @@ class HttpStub( val mock: ScenarioStub = stringToMockScenario(httpRequest.body) val stub: HttpStubData = setExpectation(mock) - HttpStubResponse(HttpResponse.OK, contractPath = stub?.contractPath ?: "") + HttpStubResponse(HttpResponse.OK, contractPath = stub.contractPath) } catch (e: ContractException) { HttpStubResponse( HttpResponse( diff --git a/core/src/test/kotlin/in/specmatic/core/FeatureTest.kt b/core/src/test/kotlin/in/specmatic/core/FeatureTest.kt index 9b1c7f5c4..b49ca9de5 100644 --- a/core/src/test/kotlin/in/specmatic/core/FeatureTest.kt +++ b/core/src/test/kotlin/in/specmatic/core/FeatureTest.kt @@ -883,14 +883,14 @@ Feature: Contract for /balance API @Test fun `successfully matches valid form fields`() { - val requestPattern = HttpRequestPattern(HttpHeadersPattern(), null, null, EmptyStringPattern, mapOf("Data" to NumberPattern())) + val requestPattern = HttpRequestPattern(HttpHeadersPattern(), formFieldsPattern = mapOf("Data" to NumberPattern())) val request = HttpRequest().copy(formFields = mapOf("Data" to "10")) assertTrue(requestPattern.matchFormFields(Triple(request, Resolver(), emptyList())) is MatchSuccess) } @Test fun `returns error for form fields`() { - val requestPattern = HttpRequestPattern(HttpHeadersPattern(), null, null, EmptyStringPattern, mapOf("Data" to NumberPattern())) + val requestPattern = HttpRequestPattern(HttpHeadersPattern(), formFieldsPattern = mapOf("Data" to NumberPattern())) val request = HttpRequest().copy(formFields = mapOf("Data" to "hello")) val result: MatchingResult>> = requestPattern.matchFormFields(Triple(request, Resolver(), emptyList())) result as MatchSuccess>> diff --git a/core/src/test/kotlin/in/specmatic/core/HttpPathPatternTest.kt b/core/src/test/kotlin/in/specmatic/core/HttpPathPatternTest.kt new file mode 100644 index 000000000..d53d31b06 --- /dev/null +++ b/core/src/test/kotlin/in/specmatic/core/HttpPathPatternTest.kt @@ -0,0 +1,191 @@ +package `in`.specmatic.core + +import `in`.specmatic.GENERATION +import `in`.specmatic.core.value.NumberValue +import `in`.specmatic.core.value.StringValue +import io.mockk.every +import io.mockk.mockk +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import `in`.specmatic.core.pattern.* +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Tag +import java.io.UnsupportedEncodingException +import java.net.URI +import java.net.URISyntaxException +import java.util.* + +internal class HttpPathPatternTest { + @Test + @Throws(URISyntaxException::class, UnsupportedEncodingException::class) + fun `should not match url when number of path parts do not match`() { + val urlPattern = buildHttpPathPattern(URI("/pets/123/owners/hari")) + urlPattern.matches(URI("/pets/123/owners"), Resolver()).let { + assertThat(it is Result.Failure).isTrue() + assertThat((it as Result.Failure).toMatchFailureDetails()).isEqualTo( + MatchFailureDetails( + listOf("PATH"), + listOf("""Expected /pets/123/owners (having 3 path segments) to match /pets/123/owners/hari (which has 4 path segments).""") + ) + ) + } + } + + @Test + @Throws(URISyntaxException::class, UnsupportedEncodingException::class) + fun `should match url with only path parameters`() { + val urlPattern = buildHttpPathPattern(URI("/pets/(petid:number)/owner/(owner:string)")) + urlPattern.matches(URI("/pets/123123/owner/hari")).let { + assertThat(it is Result.Success).isTrue() + } + } + + @Test + @Throws(URISyntaxException::class, UnsupportedEncodingException::class) + fun `should not match when all parts of the path do not match`() { + val urlPattern = buildHttpPathPattern(URI("/pets/(petid:number)")) + val queryParameters = HashMap() + urlPattern.matches(URI("/owners/123123"), Resolver()).let { + assertThat(it is Result.Failure).isTrue() + assertThat((it as Result.Failure).toMatchFailureDetails()).isEqualTo( + MatchFailureDetails( + listOf("PATH (/owners/123123)"), + listOf("""Expected "pets", actual was "owners"""") + ) + ) + } + } + + @Test + fun `should generate path when URI contains only query parameters`() { + val urlPattern = buildHttpPathPattern(URI("/pets?petid=(number)")) + urlPattern.generate(Resolver()).let { + assertThat(it).isEqualTo("/pets") + } + } + + @Test + fun `should generate path when url has only path parameters`() { + val urlPattern = buildHttpPathPattern(URI("/pets/(petid:number)/owner/(owner:string)")) + val resolver = mockk().also { + every { + it.withCyclePrevention( + ExactValuePattern(StringValue("pets")), + any() + ) + } returns StringValue("pets") + every { + it.withCyclePrevention( + DeferredPattern("(number)", "petid"), + any() + ) + } returns NumberValue(123) + every { + it.withCyclePrevention( + ExactValuePattern(StringValue("owner")), + any() + ) + } returns StringValue("owner") + every { + it.withCyclePrevention( + DeferredPattern("(string)", "owner"), + any() + ) + } returns StringValue("hari") + } + urlPattern.generate(resolver).let { + assertThat(it).isEqualTo("/pets/123/owner/hari") + } + } + + @Test + @Tag(GENERATION) + fun `should pick up facts`() { + val urlPattern = buildHttpPathPattern(URI("/pets/(id:number)")) + val resolver = Resolver(mapOf("id" to StringValue("10"))) + + val newURLPatterns = urlPattern.newBasedOn(Row(), resolver) + val urlPathSegmentPatterns = newURLPatterns.first() + assertEquals(2, urlPathSegmentPatterns.size) + val path = urlPathSegmentPatterns.joinToString("/") { it.generate(resolver).toStringLiteral() } + assertEquals("pets/10", path) + } + + @Test + fun `request url with no query params should match a url pattern with query params`() { + val matcher = buildHttpPathPattern(URI("/pets?id=(string)")) + assertThat(matcher.matches(URI("/pets"))).isInstanceOf(Result.Success::class.java) + } + + @Test + fun `should match a number in a path only when resolver has mock matching on`() { + val matcher = buildHttpPathPattern(URI("/pets/(id:number)")) + assertThat( + matcher.matches( + URI.create("/pets/10"), + Resolver() + ) + ).isInstanceOf(Result.Success::class.java) + assertThat( + matcher.matches( + URI.create("/pets/(id:number)"), + Resolver(mockMode = true) + ) + ).isInstanceOf(Result.Success::class.java) + assertThat( + matcher.matches( + URI.create("/pets/(id:number)"), + Resolver(mockMode = false) + ) + ).isInstanceOf(Result.Failure::class.java) + } + + @Test + fun `should match a boolean in a path only when resolver has mock matching on`() { + val matcher = buildHttpPathPattern(URI("/pets/(status:boolean)")) + assertThat( + matcher.matches( + URI.create("/pets/true"), + Resolver() + ) + ).isInstanceOf(Result.Success::class.java) + assertThat( + matcher.matches( + URI.create("/pets/(status:boolean)"), + Resolver(mockMode = true) + ) + ).isInstanceOf(Result.Success::class.java) + assertThat( + matcher.matches( + URI.create("/pets/(status:boolean)"), + Resolver(mockMode = false) + ) + ).isInstanceOf(Result.Failure::class.java) + } + + @Test + @Tag(GENERATION) + fun `should generate a path with a concrete value given a path pattern with newBasedOn`() { + val matcher = buildHttpPathPattern(URI("/pets/(status:boolean)")) + val matchers = matcher.newBasedOn(Row(), Resolver()) + assertThat(matchers).hasSize(1) + assertThat(matchers.single()).isEqualTo( + listOf( + URLPathSegmentPattern(ExactValuePattern(StringValue("pets"))), + URLPathSegmentPattern(BooleanPattern(), "status") + ) + ) + } + + @Tag(GENERATION) + @Test + fun `should generate negative values for a number`() { + val headers = HttpHeadersPattern(mapOf("X-TraceID" to NumberPattern())) + val newHeaders = headers.negativeBasedOn(Row(), Resolver()) + + assertThat(newHeaders).containsExactlyInAnyOrder( + HttpHeadersPattern(mapOf("X-TraceID" to StringPattern())), + HttpHeadersPattern(mapOf("X-TraceID" to BooleanPattern())), + ) + } +} diff --git a/core/src/test/kotlin/in/specmatic/core/HttpQueryParamPatternTest.kt b/core/src/test/kotlin/in/specmatic/core/HttpQueryParamPatternTest.kt new file mode 100644 index 000000000..b3d2fdfbc --- /dev/null +++ b/core/src/test/kotlin/in/specmatic/core/HttpQueryParamPatternTest.kt @@ -0,0 +1,225 @@ +package `in`.specmatic.core + +import `in`.specmatic.GENERATION +import `in`.specmatic.core.Result.Failure +import `in`.specmatic.core.Result.Success +import `in`.specmatic.core.pattern.* +import `in`.specmatic.core.value.NumberValue +import `in`.specmatic.core.value.StringValue +import io.mockk.every +import io.mockk.mockk +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import java.io.UnsupportedEncodingException +import java.net.URI +import java.net.URISyntaxException + +class HttpQueryParamPatternTest { + @Test + fun `request url query params should not match a url with unknown query params`() { + val matcher = buildQueryPattern(URI("/pets?id=(string)")) + assertThat(matcher.matches(URI("/pets"), mapOf("name" to "Jack Daniel"))).isInstanceOf(Failure::class.java) + } + + @Test + fun `should match a boolean in a query only when resolver has mock matching on`() { + val matcher = buildQueryPattern(URI("/pets?available=(boolean)")) + assertThat(matcher.matches(URI.create("/pets"), mapOf("available" to "true"), Resolver())).isInstanceOf(Success::class.java) + assertThat(matcher.matches(URI.create("/pets"), mapOf("available" to "(boolean)"), Resolver(mockMode = true))).isInstanceOf( + Success::class.java) + assertThat(matcher.matches(URI.create("/pets"), mapOf("available" to "(boolean)"), Resolver(mockMode = false))).isInstanceOf( + Failure::class.java) + } + + @Test + fun `url matcher with a mandatory query param should not match empty query params`() { + val matcher = HttpQueryParamPattern(mapOf("name" to StringPattern())) + val result = matcher.matches(URI("/"), emptyMap(), Resolver()) + assertThat(result.isSuccess()).isFalse() + } + + @Test + fun `should match a number in a query only when resolver has mock matching on`() { + val matcher = buildQueryPattern(URI("/pets?id=(number)")) + assertThat(matcher.matches(URI.create("/pets"), mapOf("id" to "10"), Resolver())).isInstanceOf(Success::class.java) + assertThat(matcher.matches(URI.create("/pets"), mapOf("id" to "(number)"), Resolver(mockMode = true))).isInstanceOf( + Success::class.java) + assertThat(matcher.matches(URI.create("/pets"), mapOf("id" to "(number)"), Resolver(mockMode = false))).isInstanceOf( + Failure::class.java) + } + + @Test + @Throws(URISyntaxException::class, UnsupportedEncodingException::class) + fun `should not match url when query parameters do not match`() { + val urlPattern = buildQueryPattern(URI("/pets?petid=(number)")) + val queryParameters = mapOf("petid" to "text") + + urlPattern.matches(URI("/pets"), queryParameters, Resolver()).let { + assertThat(it is Failure).isTrue() + assertThat((it as Failure).toMatchFailureDetails()).isEqualTo(MatchFailureDetails(listOf(QUERY_PARAMS_BREADCRUMB, "petid"), listOf("""Expected number, actual was "text""""))) + } + } + + @Test + @Throws(URISyntaxException::class, UnsupportedEncodingException::class) + fun `should match url with only query parameters`() { + val urlPattern = buildQueryPattern(URI("/pets?petid=(number)&owner=(string)")) + val queryParameters = hashMapOf( + "petid" to "123123", + "owner" to "hari" + ) + urlPattern.matches(URI("/pets"), queryParameters, Resolver()).let { + assertThat(it is Success).isTrue() + } + } + + @Test + fun `request url with 1 query param should match a url pattern with superset of 2 params`() { + val matcher = buildQueryPattern(URI("/pets?id=(string)&name=(string)")) + assertThat( + matcher.matches( + URI("/pets"), + mapOf("name" to "Jack Daniel") + ) + ).isInstanceOf(Success::class.java) + } + + @Test + fun `should generate query`() { + val urlPattern = buildQueryPattern(URI("/pets?petid=(number)&owner=(string)")) + val resolver = mockk().also { + every { + it.withCyclePrevention( + ExactValuePattern(StringValue("pets")), + any() + ) + } returns StringValue("pets") + every { + it.withCyclePrevention( + DeferredPattern("(number)", "petid"), + any() + ) + } returns NumberValue(123) + every { + it.withCyclePrevention( + DeferredPattern("(string)", "owner"), + any() + ) + } returns StringValue("hari") + } + urlPattern.generate(resolver).let { + assertThat(it).isEqualTo(hashMapOf("petid" to "123", "owner" to "hari")) + } + } + + @Test + @Tag(GENERATION) + fun `should generate a valid query string when there is a single row with matching columns`() { + val resolver = Resolver() + val row = Row(listOf("status", "type"), listOf("available", "dog")) + val generatedPatterns = buildQueryPattern(URI("/pets?status=(string)&type=(string)")).newBasedOn(row, resolver) + assertEquals(1, generatedPatterns.size) + val pattern = HttpQueryParamPattern(generatedPatterns.first()).generate(resolver) + assertEquals("available", pattern.getValue("status")) + assertEquals("dog", pattern.getValue("type")) + } + + @Test + fun `given a pattern in a query param, it should generate a random value matching that pattern`() { + val matcher = buildQueryPattern(URI("/pets?id=(string)")) + val query = matcher.generate(Resolver()) + + Assertions.assertNotEquals("(string)", query.getValue("id")) + assertTrue(query.getValue("id").isNotEmpty()) + } + + @Test + fun `url matcher with 2 non optional query params should not match a url with just one of the specified query params`() { + val matcher = + HttpQueryParamPattern(queryPatterns = mapOf("name" to StringPattern(), "string" to StringPattern())) + + val result = matcher.matches(HttpRequest(queryParams = mapOf("name" to "Archie")), Resolver()) + .breadCrumb(QUERY_PARAMS_BREADCRUMB) + assertThat(result.isSuccess()).isFalse() + } + + @Test + fun `should stringify date time query param to date time pattern`() { + val httpQueryParamPattern = HttpQueryParamPattern(mapOf("before" to DateTimePattern)) + assertThat(httpQueryParamPattern.toString()).isEqualTo("?before=(datetime)") + } + + @Test + @Tag(GENERATION) + fun `should generate a path with a concrete value given a query param with newBasedOn`() { + val matcher = buildQueryPattern(URI("/pets?available=(boolean)")) + val matchers = matcher.newBasedOn(Row(), Resolver()) + assertThat(matchers).hasSize(2) + assertThat(matchers).contains(emptyMap()) + assertThat(matchers).contains(mapOf("available" to BooleanPattern())) + } + + @Test + @Throws(URISyntaxException::class, UnsupportedEncodingException::class) + fun `should match url with both path and query parameters`() { + val urlPattern = buildQueryPattern(URI("/pets/(petid:number)?owner=(string)")) + val queryParameters = hashMapOf("owner" to "Hari") + urlPattern.matches(URI("/pets/123123"), queryParameters, Resolver()).let { + assertThat(it is Success).isTrue() + } + } + + @Tag(GENERATION) + @Test + fun `should generate negative values for a string`() { + val urlMatchers = buildQueryPattern(URI("/pets?name=(string)")).negativeBasedOn(Row(), Resolver()) + assertThat(urlMatchers).containsExactly(emptyMap()) + } + + @Test + @Tag(GENERATION) + fun `should create 2^n matchers on an empty Row`() { + val patterns = buildQueryPattern(URI("/pets?status=(string)&type=(string)")) + val generatedPatterns = patterns.newBasedOn(Row(), Resolver()) + assertThat(generatedPatterns).containsExactlyInAnyOrder( + emptyMap(), + mapOf("status" to StringPattern()), + mapOf("type" to StringPattern()), + mapOf("status" to StringPattern(), "type" to StringPattern()), + ) + } + + @Test + fun `should correctly stringize a url matching having a query param with an array type`() { + val matcher = HttpQueryParamPattern(mapOf("data" to CsvPattern(NumberPattern()))) + assertThat(matcher.toString()).isEqualTo("?data=(csv/number)") + } + + @Nested + inner class ReturnMultipleErrors { + val urlMatcher = buildQueryPattern(URI.create("http://example.com/?hello=(number)")) + val result = urlMatcher.matches(HttpRequest("GET", "/", queryParams = mapOf("hello" to "world", "hi" to "all")), Resolver()) as Failure + val resultText = result.toReport().toText() + + @Test + fun `should return as many errors as there are value mismatches`() { + assertThat(result.toMatchFailureDetailList()).hasSize(2) + } + + @Test + fun `keys with errors should be present in the error list`() { + assertThat(resultText).contains(">> QUERY-PARAMS.hello") + assertThat(resultText).contains(">> QUERY-PARAMS.hi") + } + + @Test + fun `key presence errors should appear before value errors`() { + assertThat(resultText.indexOf(">> QUERY-PARAMS.hi")).isLessThan(resultText.indexOf(">> QUERY-PARAMS.hello")) + } + } +} \ No newline at end of file diff --git a/core/src/test/kotlin/in/specmatic/core/HttpRequestPatternTest.kt b/core/src/test/kotlin/in/specmatic/core/HttpRequestPatternTest.kt index c09490ce5..5bb8fc74a 100644 --- a/core/src/test/kotlin/in/specmatic/core/HttpRequestPatternTest.kt +++ b/core/src/test/kotlin/in/specmatic/core/HttpRequestPatternTest.kt @@ -17,7 +17,7 @@ internal class HttpRequestPatternTest { @Test fun `should not match when url does not match`() { val httpRequestPattern = HttpRequestPattern( - httpUrlPattern = toURLMatcherWithOptionalQueryParams(URI("/matching_path"))) + httpPathPattern = buildHttpPathPattern(URI("/matching_path"))) val httpRequest = HttpRequest().updateWith(URI("/unmatched_path")) httpRequestPattern.matches(httpRequest, Resolver()).let { assertThat(it).isInstanceOf(Failure::class.java) @@ -28,7 +28,7 @@ internal class HttpRequestPatternTest { @Test fun `should not match when method does not match`() { val httpRequestPattern = HttpRequestPattern( - httpUrlPattern = toURLMatcherWithOptionalQueryParams(URI("/matching_path")), + httpPathPattern = buildHttpPathPattern(URI("/matching_path")), method = "POST") val httpRequest = HttpRequest() .updateWith(URI("/matching_path")) @@ -43,7 +43,7 @@ internal class HttpRequestPatternTest { fun `should not match when body does not match`() { val httpRequestPattern = HttpRequestPattern( - httpUrlPattern = toURLMatcherWithOptionalQueryParams(URI("/matching_path")), + httpPathPattern = buildHttpPathPattern(URI("/matching_path")), method = "POST", body = parsedPattern("""{"name": "Hari"}""")) val httpRequest = HttpRequest() @@ -59,7 +59,7 @@ internal class HttpRequestPatternTest { @Test fun `should match when request matches url, method and body`() { val httpRequestPattern = HttpRequestPattern( - httpUrlPattern = toURLMatcherWithOptionalQueryParams(URI("/matching_path")), + httpPathPattern = buildHttpPathPattern(URI("/matching_path")), method = "POST", body = parsedPattern("""{"name": "Hari"}""")) val httpRequest = HttpRequest() @@ -75,18 +75,18 @@ internal class HttpRequestPatternTest { fun `a clone request pattern request should include the headers specified`() { val pattern = HttpRequestPattern( headersPattern = HttpHeadersPattern(mapOf("Test-Header" to stringToPattern("(string)", "Test-Header"))), - httpUrlPattern = toURLMatcherWithOptionalQueryParams(URI("/")), + httpPathPattern = buildHttpPathPattern(URI("/")), method = "GET" ) val newPatterns = pattern.newBasedOn(Row(), Resolver()) - assertEquals("(string)", newPatterns[0].headersPattern.pattern.get("Test-Header").toString()) + assertEquals("(string)", newPatterns[0].headersPattern.pattern["Test-Header"].toString()) } @Test fun `clone request pattern with example of body type should pick up the example`() { val pattern = HttpRequestPattern( - httpUrlPattern = toURLMatcherWithOptionalQueryParams(URI("/")), + httpPathPattern = buildHttpPathPattern(URI("/")), method = "POST", body = DeferredPattern("(Data)") ) @@ -102,7 +102,7 @@ internal class HttpRequestPatternTest { @Test fun `a request with an optional header should result in 2 options for newBasedOn`() { val requests = HttpRequestPattern(method = "GET", - httpUrlPattern = toURLMatcherWithOptionalQueryParams(URI("/")), + httpPathPattern = buildHttpPathPattern(URI("/")), headersPattern = HttpHeadersPattern(mapOf("X-Optional?" to StringPattern()))).newBasedOn(Row(), Resolver()) assertThat(requests).hasSize(2) @@ -119,7 +119,7 @@ internal class HttpRequestPatternTest { @Test fun `number bodies should match numerical strings`() { - val requestPattern = HttpRequestPattern(method = "GET", httpUrlPattern = toURLMatcherWithOptionalQueryParams("/"), body = NumberPattern()) + val requestPattern = HttpRequestPattern(method = "GET", httpPathPattern = buildHttpPathPattern("/"), body = NumberPattern()) val request = HttpRequest("GET", path = "/", body = StringValue("10")) assertThat(requestPattern.matches(request, Resolver())).isInstanceOf(Success::class.java) @@ -127,7 +127,7 @@ internal class HttpRequestPatternTest { @Test fun `boolean bodies should match boolean strings`() { - val requestPattern = HttpRequestPattern(method = "GET", httpUrlPattern = toURLMatcherWithOptionalQueryParams("/"), body = BooleanPattern()) + val requestPattern = HttpRequestPattern(method = "GET", httpPathPattern = buildHttpPathPattern("/"), body = BooleanPattern()) val request = HttpRequest("GET", path = "/", body = StringValue("true")) assertThat(requestPattern.matches(request, Resolver())).isInstanceOf(Success::class.java) @@ -135,7 +135,7 @@ internal class HttpRequestPatternTest { @Test fun `boolean bodies should not match non-boolean strings`() { - val requestPattern = HttpRequestPattern(method = "GET", httpUrlPattern = toURLMatcherWithOptionalQueryParams("/"), body = BooleanPattern()) + val requestPattern = HttpRequestPattern(method = "GET", httpPathPattern = buildHttpPathPattern("/"), body = BooleanPattern()) val request = HttpRequest("GET", path = "/", body = StringValue("10")) assertThat(requestPattern.matches(request, Resolver())).isInstanceOf(Failure::class.java) @@ -143,7 +143,7 @@ internal class HttpRequestPatternTest { @Test fun `integer bodies should not match non-integer strings`() { - val requestPattern = HttpRequestPattern(method = "GET", httpUrlPattern = toURLMatcherWithOptionalQueryParams("/"), body = NumberPattern()) + val requestPattern = HttpRequestPattern(method = "GET", httpPathPattern = buildHttpPathPattern("/"), body = NumberPattern()) val request = HttpRequest("GET", path = "/", body = StringValue("not a number")) assertThat(requestPattern.matches(request, Resolver())).isInstanceOf(Failure::class.java) @@ -155,7 +155,7 @@ internal class HttpRequestPatternTest { "data1", StringPattern(), ), MultiPartContentPattern("data2", StringPattern())) - val requestPattern = HttpRequestPattern(method = "GET", httpUrlPattern = toURLMatcherWithOptionalQueryParams("/"), multiPartFormDataPattern = parts) + val requestPattern = HttpRequestPattern(method = "GET", httpPathPattern = buildHttpPathPattern("/"), multiPartFormDataPattern = parts) val patterns = requestPattern.newBasedOn(Row(), Resolver()) assertThat(patterns).hasSize(1) @@ -167,13 +167,13 @@ internal class HttpRequestPatternTest { fun `request with an optional part should result in two requests`() { val part = MultiPartContentPattern("data?", StringPattern()) - val requestPattern = HttpRequestPattern(method = "GET", httpUrlPattern = toURLMatcherWithOptionalQueryParams("/"), multiPartFormDataPattern = listOf(part)) + val requestPattern = HttpRequestPattern(method = "GET", httpPathPattern = buildHttpPathPattern("/"), multiPartFormDataPattern = listOf(part)) val patterns = requestPattern.newBasedOn(Row(), Resolver()) assertThat(patterns).hasSize(2) - assertThat(patterns).contains(HttpRequestPattern(method = "GET", httpUrlPattern = toURLMatcherWithOptionalQueryParams("/"), multiPartFormDataPattern = emptyList())) - assertThat(patterns).contains(HttpRequestPattern(method = "GET", httpUrlPattern = toURLMatcherWithOptionalQueryParams("/"), multiPartFormDataPattern = listOf(part.nonOptional()))) + assertThat(patterns).contains(HttpRequestPattern(method = "GET", httpPathPattern = buildHttpPathPattern("/"), multiPartFormDataPattern = emptyList())) + assertThat(patterns).contains(HttpRequestPattern(method = "GET", httpPathPattern = buildHttpPathPattern("/"), multiPartFormDataPattern = listOf(part.nonOptional()))) } @Test @@ -181,12 +181,12 @@ internal class HttpRequestPatternTest { val part = MultiPartContentPattern("data", parsedPattern("""{"name": "(string)"}""")) val example = Row(listOf("name"), listOf("John Doe")) - val requestPattern = HttpRequestPattern(method = "GET", httpUrlPattern = toURLMatcherWithOptionalQueryParams("/"), multiPartFormDataPattern = listOf(part)) + val requestPattern = HttpRequestPattern(method = "GET", httpPathPattern = buildHttpPathPattern("/"), multiPartFormDataPattern = listOf(part)) val patterns = requestPattern.newBasedOn(example, Resolver()) assertThat(patterns).hasSize(1) - val expectedPattern = HttpRequestPattern(method = "GET", httpUrlPattern = toURLMatcherWithOptionalQueryParams("/"), multiPartFormDataPattern = listOf(MultiPartContentPattern( + val expectedPattern = HttpRequestPattern(method = "GET", httpPathPattern = buildHttpPathPattern("/"), multiPartFormDataPattern = listOf(MultiPartContentPattern( "data", toJSONObjectPattern(mapOf("name" to ExactValuePattern(StringValue("John Doe")))), ))) @@ -198,12 +198,12 @@ internal class HttpRequestPatternTest { val part = MultiPartContentPattern("name", StringPattern()) val example = Row(listOf("name"), listOf("John Doe")) - val requestPattern = HttpRequestPattern(method = "GET", httpUrlPattern = toURLMatcherWithOptionalQueryParams("/"), multiPartFormDataPattern = listOf(part)) + val requestPattern = HttpRequestPattern(method = "GET", httpPathPattern = buildHttpPathPattern("/"), multiPartFormDataPattern = listOf(part)) val patterns = requestPattern.newBasedOn(example, Resolver()) assertThat(patterns).hasSize(1) - val expectedPattern = HttpRequestPattern(method = "GET", httpUrlPattern = toURLMatcherWithOptionalQueryParams("/"), multiPartFormDataPattern = listOf(MultiPartContentPattern( + val expectedPattern = HttpRequestPattern(method = "GET", httpPathPattern = buildHttpPathPattern("/"), multiPartFormDataPattern = listOf(MultiPartContentPattern( "name", ExactValuePattern(StringValue("John Doe")), ))) @@ -215,12 +215,12 @@ internal class HttpRequestPatternTest { val part = MultiPartContentPattern("name?", StringPattern()) val example = Row(listOf("name"), listOf("John Doe")) - val requestPattern = HttpRequestPattern(method = "GET", httpUrlPattern = toURLMatcherWithOptionalQueryParams("/"), multiPartFormDataPattern = listOf(part)) + val requestPattern = HttpRequestPattern(method = "GET", httpPathPattern = buildHttpPathPattern("/"), multiPartFormDataPattern = listOf(part)) val patterns = requestPattern.newBasedOn(example, Resolver()) assertThat(patterns).hasSize(1) - val expectedPattern = HttpRequestPattern(method = "GET", httpUrlPattern = toURLMatcherWithOptionalQueryParams("/"), multiPartFormDataPattern = listOf(MultiPartContentPattern( + val expectedPattern = HttpRequestPattern(method = "GET", httpPathPattern = buildHttpPathPattern("/"), multiPartFormDataPattern = listOf(MultiPartContentPattern( "name", ExactValuePattern(StringValue("John Doe")), ))) @@ -231,7 +231,7 @@ internal class HttpRequestPatternTest { fun `request type having an optional part name should match a request in which the part is missing`() { val part = MultiPartContentPattern("name?", StringPattern()) - val requestType = HttpRequestPattern(method = "GET", httpUrlPattern = toURLMatcherWithOptionalQueryParams("/"), multiPartFormDataPattern = listOf(part)) + val requestType = HttpRequestPattern(method = "GET", httpPathPattern = buildHttpPathPattern("/"), multiPartFormDataPattern = listOf(part)) val request = HttpRequest("GET", "/") @@ -270,7 +270,7 @@ internal class HttpRequestPatternTest { fun `should generate a request with an array body if the array is in an example`() { val example = Row(listOf("body"), listOf("[1, 2, 3]")) - val requestType = HttpRequestPattern(httpUrlPattern = toURLMatcherWithOptionalQueryParams("/"), body = parsedPattern("(body: RequestBody)")) + val requestType = HttpRequestPattern(httpPathPattern = buildHttpPathPattern("/"), body = parsedPattern("(body: RequestBody)")) val newRequestTypes = requestType.newBasedOn(example, Resolver(newPatterns = mapOf("(RequestBody)" to parsedPattern("""(number*)""")))) assertThat(newRequestTypes).hasSize(1) @@ -285,7 +285,7 @@ internal class HttpRequestPatternTest { fun `should generate a request with an object body if the object is in an example`() { val example = Row(listOf("body"), listOf("""{"one": 1}""")) - val requestType = HttpRequestPattern(httpUrlPattern = toURLMatcherWithOptionalQueryParams("/"), body = parsedPattern("(body: RequestBody)")) + val requestType = HttpRequestPattern(httpPathPattern = buildHttpPathPattern("/"), body = parsedPattern("(body: RequestBody)")) val newRequestTypes = requestType.newBasedOn(example, Resolver(newPatterns = mapOf("(RequestBody)" to toTabularPattern(mapOf("one" to NumberPattern()))))) assertThat(newRequestTypes).hasSize(1) @@ -298,10 +298,10 @@ internal class HttpRequestPatternTest { @Test fun `should generate a stub request pattern from an http request in which the query params are not optional`() { - val requestType = HttpRequestPattern(method = "GET", httpUrlPattern = HttpURLPattern(mapOf("status" to StringPattern()), pathToPattern("/"), "/")) + val requestType = HttpRequestPattern(method = "GET", httpPathPattern = HttpPathPattern(pathToPattern("/"), "/"), httpQueryParamPattern = HttpQueryParamPattern(mapOf("status" to StringPattern()))) val newRequestType = requestType.generate(HttpRequest("GET", "/", queryParams = mapOf("status" to "available")), Resolver()) - assertThat(newRequestType.httpUrlPattern?.queryPatterns?.keys?.sorted()).isEqualTo(listOf("status")) + assertThat(newRequestType.httpQueryParamPattern.queryPatterns.keys.sorted()).isEqualTo(listOf("status")) } @@ -312,7 +312,7 @@ internal class HttpRequestPatternTest { HttpRequestPattern( method = "POST", - httpUrlPattern = HttpURLPattern(emptyMap(), emptyList(), "/"), + httpPathPattern = HttpPathPattern(emptyList(), "/"), formFieldsPattern = mapOf("Customer" to PatternInStringPattern(customerType, "(customer)")) ).generate(request, Resolver()).let { requestType -> val customerFieldType = requestType.formFieldsPattern.getValue("Customer") @@ -332,7 +332,7 @@ internal class HttpRequestPatternTest { val result = HttpRequestPattern( method = "POST", - httpUrlPattern = HttpURLPattern(emptyMap(), emptyList(), "/"), + httpPathPattern = HttpPathPattern(emptyList(), "/"), formFieldsPattern = mapOf("hello" to NumberPattern(), "world?" to NumberPattern()) ).matches(request, Resolver()) @@ -341,7 +341,7 @@ internal class HttpRequestPatternTest { @Test fun `match errors across the request including header and body will be returned`() { - val type = HttpRequestPattern(method = "POST", httpUrlPattern = toURLMatcherWithOptionalQueryParams("http://helloworld.com/data"), headersPattern = HttpHeadersPattern(mapOf("X-Data" to NumberPattern())), body = JSONObjectPattern(mapOf("id" to NumberPattern()))) + val type = HttpRequestPattern(method = "POST", httpPathPattern = buildHttpPathPattern("http://helloworld.com/data"), headersPattern = HttpHeadersPattern(mapOf("X-Data" to NumberPattern())), body = JSONObjectPattern(mapOf("id" to NumberPattern()))) val request = HttpRequest("POST", "/data", headers = mapOf("X-Data" to "abc123"), body = parsedJSON("""{"id": "abc123"}""")) val result = type.matches(request, Resolver()) @@ -352,7 +352,7 @@ internal class HttpRequestPatternTest { @Test fun `should lower case header keys while loading stub data`() { - val type = HttpRequestPattern(method = "POST", httpUrlPattern = toURLMatcherWithOptionalQueryParams("http://helloworld.com/data"), headersPattern = HttpHeadersPattern(mapOf("x-data" to StringPattern())), body = JSONObjectPattern(mapOf("id" to NumberPattern()))) + val type = HttpRequestPattern(method = "POST", httpPathPattern = buildHttpPathPattern("http://helloworld.com/data"), headersPattern = HttpHeadersPattern(mapOf("x-data" to StringPattern())), body = JSONObjectPattern(mapOf("id" to NumberPattern()))) val request = HttpRequest("POST", "/data", headers = mapOf("X-Data" to "abc123"), body = parsedJSON("""{"id": "abc123"}""")) val httpRequestPattern = type.generate(request, Resolver()) @@ -365,11 +365,11 @@ internal class HttpRequestPatternTest { val result = HttpRequestPattern( method = "POST", - httpUrlPattern = HttpURLPattern(emptyMap(), emptyList(), "/"), + httpPathPattern = HttpPathPattern(emptyList(), "/"), formFieldsPattern = mapOf("hello" to NumberPattern(), "world" to NumberPattern()) ).matches(request, Resolver()) - val reportText = result.reportString() + private val reportText = result.reportString() @Test fun `returns all form field errors`() { @@ -391,14 +391,14 @@ internal class HttpRequestPatternTest { @Nested inner class MultiPartMatchReturnsAllErrors { - val parts = listOf( + private val parts = listOf( MultiPartContentPattern("data1", NumberPattern()), MultiPartContentPattern("data2", NumberPattern())) - val requestPattern = HttpRequestPattern(method = "POST", httpUrlPattern = toURLMatcherWithOptionalQueryParams("/"), multiPartFormDataPattern = parts) + private val requestPattern = HttpRequestPattern(method = "POST", httpPathPattern = buildHttpPathPattern("/"), multiPartFormDataPattern = parts) val request = HttpRequest("POST", "/", multiPartFormData = listOf(MultiPartContentValue("data1", StringValue("abc123")))) val result = requestPattern.matches(request, Resolver()) - val reportText = result.reportString() + private val reportText = result.reportString() @Test fun `returns all multipart field errors`() { @@ -420,7 +420,7 @@ internal class HttpRequestPatternTest { @Test fun `should not generate test request for generative tests more than once`() { - val pattern = HttpRequestPattern(method = "POST", httpUrlPattern = toURLMatcherWithOptionalQueryParams("http://helloworld.com/data"), body = JSONObjectPattern(mapOf("id" to NumberPattern()))) + val pattern = HttpRequestPattern(method = "POST", httpPathPattern = buildHttpPathPattern("http://helloworld.com/data"), body = JSONObjectPattern(mapOf("id" to NumberPattern()))) val row = Row(listOf("(REQUEST-BODY)"), listOf("""{ "id": 10 }""")) val patterns = pattern.newBasedOn(row, Resolver(generation = GenerativeTestsEnabled())) @@ -433,7 +433,7 @@ internal class HttpRequestPatternTest { val httpRequestPattern = HttpRequestPattern( headersPattern = HttpHeadersPattern(contentType = "application/json"), method = "POST", - httpUrlPattern = toURLMatcherWithOptionalQueryParams(URI("/matching_path")), + httpPathPattern = buildHttpPathPattern(URI("/matching_path")), body = StringPattern() ) val httpRequest: HttpRequest = httpRequestPattern.generate(Resolver()) diff --git a/core/src/test/kotlin/in/specmatic/core/HttpURLPatternKtTest.kt b/core/src/test/kotlin/in/specmatic/core/HttpURLPatternKtTest.kt deleted file mode 100644 index 3943d73ac..000000000 --- a/core/src/test/kotlin/in/specmatic/core/HttpURLPatternKtTest.kt +++ /dev/null @@ -1,38 +0,0 @@ -package `in`.specmatic.core - -import `in`.specmatic.core.pattern.CsvPattern -import `in`.specmatic.core.pattern.NumberPattern -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Test - -internal class HttpURLPatternKtTest { - @Nested - inner class ReturnMultipleErrors { - val urlMatcher = toURLMatcherWithOptionalQueryParams("http://example.com/?hello=(number)") - val result = urlMatcher.matches(HttpRequest("GET", "/", queryParams = mapOf("hello" to "world", "hi" to "all")), Resolver()) as Result.Failure - val resultText = result.toReport().toText() - - @Test - fun `should return as many errors as there are value mismatches`() { - assertThat(result.toMatchFailureDetailList()).hasSize(2) - } - - @Test - fun `keys with errors should be present in the error list`() { - assertThat(resultText).contains(">> QUERY-PARAMS.hello") - assertThat(resultText).contains(">> QUERY-PARAMS.hi") - } - - @Test - fun `key presence errors should appear before value errors`() { - assertThat(resultText.indexOf(">> QUERY-PARAMS.hi")).isLessThan(resultText.indexOf(">> QUERY-PARAMS.hello")) - } - - @Test - fun `should correctly stringize a url matching having a query param with an array type`() { - val matcher = HttpURLPattern(mapOf("data" to CsvPattern(NumberPattern())), emptyList(), "/") - assertThat(matcher.toString()).isEqualTo("/?data=(csv/number)") - } - } -} diff --git a/core/src/test/kotlin/in/specmatic/core/HttpURLPatternTest.kt b/core/src/test/kotlin/in/specmatic/core/HttpURLPatternTest.kt deleted file mode 100644 index b53dd5fb3..000000000 --- a/core/src/test/kotlin/in/specmatic/core/HttpURLPatternTest.kt +++ /dev/null @@ -1,275 +0,0 @@ -package `in`.specmatic.core - -import `in`.specmatic.GENERATION -import `in`.specmatic.core.value.NumberValue -import `in`.specmatic.core.value.StringValue -import io.mockk.every -import io.mockk.mockk -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test -import `in`.specmatic.core.pattern.* -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.Tag -import java.io.UnsupportedEncodingException -import java.net.URI -import java.net.URISyntaxException -import java.util.* - -internal class HttpURLPatternTest { - @Test - @Throws(URISyntaxException::class, UnsupportedEncodingException::class) - fun `should match url with only query parameters`() { - val urlPattern = toURLMatcherWithOptionalQueryParams(URI("/pets?petid=(number)&owner=(string)")) - val queryParameters = hashMapOf( - "petid" to "123123", - "owner" to "hari" - ) - urlPattern.matches(URI("/pets"), queryParameters, Resolver()).let { - assertThat(it is Result.Success).isTrue() - } - } - - @Test - @Throws(URISyntaxException::class, UnsupportedEncodingException::class) - fun `should not match url when number of path parts do not match`() { - val urlPattern = toURLMatcherWithOptionalQueryParams(URI("/pets/123/owners/hari")) - urlPattern.matches(URI("/pets/123/owners"), HashMap(), Resolver()).let { - assertThat(it is Result.Failure).isTrue() - assertThat((it as Result.Failure).toMatchFailureDetails()).isEqualTo(MatchFailureDetails(listOf("PATH"), listOf("""Expected /pets/123/owners (having 3 path segments) to match /pets/123/owners/hari (which has 4 path segments)."""))) - } - } - - @Test - @Throws(URISyntaxException::class, UnsupportedEncodingException::class) - fun `should not match url when query parameters do not match`() { - val urlPattern = toURLMatcherWithOptionalQueryParams(URI("/pets?petid=(number)")) - val queryParameters = mapOf("petid" to "text") - - urlPattern.matches(URI("/pets"), queryParameters, Resolver()).let { - assertThat(it is Result.Failure).isTrue() - assertThat((it as Result.Failure).toMatchFailureDetails()).isEqualTo(MatchFailureDetails(listOf(QUERY_PARAMS_BREADCRUMB, "petid"), listOf("""Expected number, actual was "text""""))) - } - } - - @Test - @Throws(URISyntaxException::class, UnsupportedEncodingException::class) - fun `should match url with only path parameters`() { - val urlPattern = toURLMatcherWithOptionalQueryParams(URI("/pets/(petid:number)/owner/(owner:string)")) - urlPattern.matches(URI("/pets/123123/owner/hari")).let { - assertThat(it is Result.Success).isTrue() - } - } - - @Test - @Throws(URISyntaxException::class, UnsupportedEncodingException::class) - fun `should not match when all parts of the path do not match`() { - val urlPattern = toURLMatcherWithOptionalQueryParams(URI("/pets/(petid:number)")) - val queryParameters = HashMap() - urlPattern.matches(URI("/owners/123123"), queryParameters, Resolver()).let { - assertThat(it is Result.Failure).isTrue() - assertThat((it as Result.Failure).toMatchFailureDetails()).isEqualTo(MatchFailureDetails(listOf("PATH (/owners/123123)"), listOf("""Expected "pets", actual was "owners""""))) - } - } - - @Test - @Throws(URISyntaxException::class, UnsupportedEncodingException::class) - fun `should match url with both path and query parameters`() { - val urlPattern = toURLMatcherWithOptionalQueryParams(URI("/pets/(petid:number)?owner=(string)")) - val queryParameters = hashMapOf("owner" to "Hari") - urlPattern.matches(URI("/pets/123123"), queryParameters, Resolver()).let { - assertThat(it is Result.Success).isTrue() - } - } - - @Test - fun `should generate path when URI contains only query parameters`() { - val urlPattern = toURLMatcherWithOptionalQueryParams(URI("/pets?petid=(number)")) - urlPattern.generatePath(Resolver()).let { - assertThat(it).isEqualTo("/pets") - } - } - - @Test - fun `should generate path when url has only path parameters`() { - val urlPattern = toURLMatcherWithOptionalQueryParams(URI("/pets/(petid:number)/owner/(owner:string)")) - val resolver = mockk().also { - every { it.withCyclePrevention(ExactValuePattern(StringValue("pets")), any())} returns StringValue("pets") - every { it.withCyclePrevention(DeferredPattern("(number)", "petid"), any())} returns NumberValue(123) - every { it.withCyclePrevention(ExactValuePattern(StringValue("owner")), any())} returns StringValue("owner") - every { it.withCyclePrevention(DeferredPattern("(string)", "owner"), any())} returns StringValue("hari") - } - urlPattern.generatePath(resolver).let{ - assertThat(it).isEqualTo("/pets/123/owner/hari") - } - } - - @Test - fun `should generate query`() { - val urlPattern = toURLMatcherWithOptionalQueryParams(URI("/pets?petid=(number)&owner=(string)")) - val resolver = mockk().also { - every { it.withCyclePrevention(ExactValuePattern(StringValue("pets")), any())} returns StringValue("pets") - every { it.withCyclePrevention(DeferredPattern("(number)", "petid"), any())} returns NumberValue(123) - every { it.withCyclePrevention(DeferredPattern("(string)", "owner"), any())} returns StringValue("hari") - } - urlPattern.generateQuery(resolver).let { - assertThat(it).isEqualTo(hashMapOf("petid" to "123", "owner" to "hari")) - } - } - - @Test - @Tag(GENERATION) - fun `should pick up facts`() { - val urlPattern = toURLMatcherWithOptionalQueryParams(URI("/pets/(id:number)")) - val resolver = Resolver(mapOf("id" to StringValue("10"))) - - val newURLPatterns = urlPattern.newBasedOn(Row(), resolver) - val path = newURLPatterns.first().generatePath(resolver) - assertEquals("/pets/10", path) - } - - @Test - @Tag(GENERATION) - fun `should create 2^n matchers on an empty Row`() { - val matcher = toURLMatcherWithOptionalQueryParams(URI("/pets?status=(string)&type=(string)")) - val matchers = matcher.newBasedOn(Row(), Resolver()) - - assertEquals(4, matchers.size) - println(matchers) - } - - @Test - @Tag(GENERATION) - fun `should generate a valid query string when there is a single row with matching columns`() { - val row = Row(listOf("status", "type"), listOf("available", "dog")) - val resolver = Resolver() - - val matchers = toURLMatcherWithOptionalQueryParams(URI("/pets?status=(string)&type=(string)")).newBasedOn(row, resolver) - assertEquals(1, matchers.size) - val query = matchers.first().generateQuery(Resolver()) - assertEquals("available", query.getValue("status")) - assertEquals("dog", query.getValue("type")) - } - - @Test - fun `given a pattern in a query param, it should generate a random value matching that pattern`() { - val matcher = toURLMatcherWithOptionalQueryParams(URI("/pets?id=(string)")) - val query = matcher.generateQuery(Resolver()) - - assertNotEquals("(string)", query.getValue("id")) - assertTrue(query.getValue("id").isNotEmpty()) - } - - @Test - fun `request url with no query params should match a url pattern with query params`() { - val matcher = toURLMatcherWithOptionalQueryParams(URI("/pets?id=(string)")) - assertThat(matcher.matches(URI("/pets"), emptyMap())).isInstanceOf(Result.Success::class.java) - } - - @Test - fun `request url with 1 query param should match a url pattern with superset of 2 params`() { - val matcher = toURLMatcherWithOptionalQueryParams(URI("/pets?id=(string)&name=(string)")) - assertThat(matcher.matches(URI("/pets"), mapOf("name" to "Jack Daniel"))).isInstanceOf(Result.Success::class.java) - } - - @Test - fun `request url query params should not match a url with unknown query params`() { - val matcher = toURLMatcherWithOptionalQueryParams(URI("/pets?id=(string)")) - assertThat(matcher.matches(URI("/pets"), mapOf("name" to "Jack Daniel"))).isInstanceOf(Result.Failure::class.java) - } - - @Test - fun `should match a number in a query only when resolver has mock matching on`() { - val matcher = toURLMatcherWithOptionalQueryParams(URI("/pets?id=(number)")) - assertThat(matcher.matches(URI.create("/pets"), mapOf("id" to "10"), Resolver())).isInstanceOf(Result.Success::class.java) - assertThat(matcher.matches(URI.create("/pets"), mapOf("id" to "(number)"), Resolver(mockMode = true))).isInstanceOf(Result.Success::class.java) - assertThat(matcher.matches(URI.create("/pets"), mapOf("id" to "(number)"), Resolver(mockMode = false))).isInstanceOf(Result.Failure::class.java) - } - - @Test - fun `should match a boolean in a query only when resolver has mock matching on`() { - val matcher = toURLMatcherWithOptionalQueryParams(URI("/pets?available=(boolean)")) - assertThat(matcher.matches(URI.create("/pets"), mapOf("available" to "true"), Resolver())).isInstanceOf(Result.Success::class.java) - assertThat(matcher.matches(URI.create("/pets"), mapOf("available" to "(boolean)"), Resolver(mockMode = true))).isInstanceOf(Result.Success::class.java) - assertThat(matcher.matches(URI.create("/pets"), mapOf("available" to "(boolean)"), Resolver(mockMode = false))).isInstanceOf(Result.Failure::class.java) - } - - @Test - fun `should match a number in a path only when resolver has mock matching on`() { - val matcher = toURLMatcherWithOptionalQueryParams(URI("/pets/(id:number)")) - assertThat(matcher.matches(URI.create("/pets/10"), emptyMap(), Resolver())).isInstanceOf(Result.Success::class.java) - assertThat(matcher.matches(URI.create("/pets/(id:number)"), emptyMap(), Resolver(mockMode = true))).isInstanceOf(Result.Success::class.java) - assertThat(matcher.matches(URI.create("/pets/(id:number)"), emptyMap(), Resolver(mockMode = false))).isInstanceOf(Result.Failure::class.java) - } - - @Test - fun `should match a boolean in a path only when resolver has mock matching on`() { - val matcher = toURLMatcherWithOptionalQueryParams(URI("/pets/(status:boolean)")) - assertThat(matcher.matches(URI.create("/pets/true"), emptyMap(), Resolver())).isInstanceOf(Result.Success::class.java) - assertThat(matcher.matches(URI.create("/pets/(status:boolean)"), emptyMap(), Resolver(mockMode = true))).isInstanceOf(Result.Success::class.java) - assertThat(matcher.matches(URI.create("/pets/(status:boolean)"), emptyMap(), Resolver(mockMode = false))).isInstanceOf(Result.Failure::class.java) - } - - @Test - @Tag(GENERATION) - fun `should generate a path with a concrete value given a path pattern with newBasedOn`() { - val matcher = toURLMatcherWithOptionalQueryParams(URI("/pets/(status:boolean)")) - val matchers = matcher.newBasedOn(Row(), Resolver()) - assertThat(matchers).hasSize(1) - assertThat(matchers.single()).isEqualTo(HttpURLPattern(emptyMap(), listOf(URLPathSegmentPattern(ExactValuePattern(StringValue("pets"))), URLPathSegmentPattern(BooleanPattern(), "status")), "/pets/(status:boolean)")) - } - - @Test - @Tag(GENERATION) - fun `should generate a path with a concrete value given a query param with newBasedOn`() { - val matcher = toURLMatcherWithOptionalQueryParams(URI("/pets?available=(boolean)")) - val matchers = matcher.newBasedOn(Row(), Resolver()) - assertThat(matchers).hasSize(2) - - val matcherWithoutQueryParams = HttpURLPattern(emptyMap(), listOf(URLPathSegmentPattern(ExactValuePattern(StringValue("pets")))), "/pets") - assertThat(matchers).contains(matcherWithoutQueryParams) - - val matcherWithQueryParams = HttpURLPattern(mapOf("available" to BooleanPattern()), listOf(URLPathSegmentPattern(ExactValuePattern(StringValue("pets")))), "/pets") - assertThat(matchers).contains(matcherWithQueryParams) - } - - @Tag(GENERATION) - @Test - fun `should generate negative values for a string`() { - val urlMatchers = toURLMatcherWithOptionalQueryParams(URI("/pets?name=(string)")).negativeBasedOn(Row(), Resolver())!! - assertThat(urlMatchers).containsExactly(HttpURLPattern(emptyMap(), listOf(URLPathSegmentPattern(ExactValuePattern(StringValue("pets")))), "/pets")) - } - - @Tag(GENERATION) - @Test - fun `should generate negative values for a number`() { - val headers = HttpHeadersPattern(mapOf("X-TraceID" to NumberPattern())) - val newHeaders = headers.negativeBasedOn(Row(), Resolver()) - - assertThat(newHeaders).containsExactlyInAnyOrder( - HttpHeadersPattern(mapOf("X-TraceID" to StringPattern())), - HttpHeadersPattern(mapOf("X-TraceID" to BooleanPattern())), - ) - } - - @Test - fun `url matcher with a non optional query param should not match empty query params`() { - val matcher = HttpURLPattern(queryPatterns = mapOf("name" to StringPattern()), pathToPattern("/"), "/") - - val result = matcher.matches(URI("/"), emptyMap(), Resolver()) - assertThat(result.isSuccess()).isFalse() - } - - @Test - fun `url matcher with 2 non optional query params should not match a url with just one of the specified query params`() { - val matcher = HttpURLPattern(queryPatterns = mapOf("name" to StringPattern(), "string" to StringPattern()), pathToPattern("/"), "/") - - val result = matcher.matches(URI("/"), mapOf("name" to "Archie"), Resolver()) - assertThat(result.isSuccess()).isFalse() - } - - @Test - fun `should stringify date time query param to date time pattern`() { - val httpUrlPattern = HttpURLPattern(mapOf("before" to DateTimePattern), pathToPattern("/pets"), "/pets") - assertThat(httpUrlPattern.toString()).isEqualTo("/pets?before=(datetime)") - } -} diff --git a/core/src/test/kotlin/in/specmatic/core/ScenarioTest.kt b/core/src/test/kotlin/in/specmatic/core/ScenarioTest.kt index 72564ce68..7dd400613 100644 --- a/core/src/test/kotlin/in/specmatic/core/ScenarioTest.kt +++ b/core/src/test/kotlin/in/specmatic/core/ScenarioTest.kt @@ -85,7 +85,7 @@ internal class ScenarioTest { val state = HashMap(mapOf("id" to True)) val scenario = Scenario( "Test", - HttpRequestPattern(httpUrlPattern = HttpURLPattern(emptyMap(), emptyList(), path="/")), + HttpRequestPattern(httpPathPattern = HttpPathPattern(emptyList(), path="/")), HttpResponsePattern(status=200), state, listOf(example), @@ -104,7 +104,7 @@ internal class ScenarioTest { fun `will not match a mock http request with unexpected request headers`() { val scenario = Scenario( "Test", - HttpRequestPattern(method="GET", httpUrlPattern = HttpURLPattern(emptyMap(), emptyList(), "/"), headersPattern = HttpHeadersPattern(mapOf("X-Expected" to StringPattern()))), + HttpRequestPattern(method="GET", httpPathPattern = HttpPathPattern(emptyList(), "/"), headersPattern = HttpHeadersPattern(mapOf("X-Expected" to StringPattern()))), HttpResponsePattern(status = 200), emptyMap(), emptyList(), @@ -121,7 +121,7 @@ internal class ScenarioTest { fun `will not match a mock http request with unexpected response headers`() { val scenario = Scenario( "Test", - HttpRequestPattern(method="GET", httpUrlPattern = HttpURLPattern(emptyMap(), emptyList(), "/"), headersPattern = HttpHeadersPattern(emptyMap())), + HttpRequestPattern(method="GET", httpPathPattern = HttpPathPattern(emptyList(), "/"), headersPattern = HttpHeadersPattern(emptyMap())), HttpResponsePattern(status = 200, headersPattern = HttpHeadersPattern(mapOf("X-Expected" to StringPattern()))), emptyMap(), emptyList(), @@ -138,7 +138,7 @@ internal class ScenarioTest { fun `will not match a mock http request with unexpected query params`() { val scenario = Scenario( "Test", - HttpRequestPattern(method="GET", httpUrlPattern = HttpURLPattern(mapOf("expected" to StringPattern()), emptyList(), "/"), headersPattern = HttpHeadersPattern(emptyMap(), null)), + HttpRequestPattern(method="GET", httpPathPattern = HttpPathPattern(emptyList(), "/"), httpQueryParamPattern = HttpQueryParamPattern(mapOf("expected" to StringPattern())), headersPattern = HttpHeadersPattern(emptyMap(), null)), HttpResponsePattern(status = 200), emptyMap(), emptyList(), @@ -155,7 +155,7 @@ internal class ScenarioTest { fun `will not match a mock json body with unexpected keys`() { val scenario = Scenario( "Test", - HttpRequestPattern(method="POST", httpUrlPattern = HttpURLPattern(mapOf("expected" to StringPattern()), emptyList(), "/"), headersPattern = HttpHeadersPattern(emptyMap(), null), body = parsedPattern("""{"expected": "value"}""")), + HttpRequestPattern(method="POST", httpPathPattern = HttpPathPattern(emptyList(), "/"), httpQueryParamPattern = HttpQueryParamPattern(mapOf("expected" to StringPattern())), headersPattern = HttpHeadersPattern(emptyMap(), null), body = parsedPattern("""{"expected": "value"}""")), HttpResponsePattern(status = 200), emptyMap(), emptyList(), @@ -445,7 +445,7 @@ And response-body (number) @Test fun `mock should return match errors across both request and response`() { - val requestType = HttpRequestPattern(method = "POST", httpUrlPattern = toURLMatcherWithOptionalQueryParams("http://localhost/data"), body = JSONObjectPattern(mapOf("id" to NumberPattern()))) + val requestType = HttpRequestPattern(method = "POST", httpPathPattern = buildHttpPathPattern("http://localhost/data"), body = JSONObjectPattern(mapOf("id" to NumberPattern()))) val responseType = HttpResponsePattern(status = 200, body = JSONObjectPattern(mapOf("id" to NumberPattern()))) val scenario = Scenario(ScenarioInfo("name", requestType, responseType)) diff --git a/version.properties b/version.properties index e252ccc11..a8e6e22b0 100644 --- a/version.properties +++ b/version.properties @@ -1 +1 @@ -version=1.2.3 +version=1.2.4