From d124cd6fcab9e8a907c229bc8de218df42cf6fd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20S=C5=82abek?= Date: Wed, 23 Oct 2024 09:28:58 +0200 Subject: [PATCH] wip --- .../http/enricher/HttpEnricher.scala | 35 ++-- .../http/enricher/HttpEnricherFactory.scala | 35 ++-- .../http/enricher/HttpEnricherOutput.scala | 14 +- .../enricher/HttpEnricherParameters.scala | 154 +++++++++++++++--- .../http/HttpEnricherBodyTest.scala | 2 +- .../http/HttpEnricherHeadersTest.scala | 43 ++++- .../http/HttpEnricherOutputTest.scala | 1 - .../http/HttpEnricherURLTest.scala | 39 +++++ 8 files changed, 249 insertions(+), 74 deletions(-) diff --git a/components/http/src/main/scala/pl/touk/nussknacker/http/enricher/HttpEnricher.scala b/components/http/src/main/scala/pl/touk/nussknacker/http/enricher/HttpEnricher.scala index c7467617a9a..49848c9d05c 100644 --- a/components/http/src/main/scala/pl/touk/nussknacker/http/enricher/HttpEnricher.scala +++ b/components/http/src/main/scala/pl/touk/nussknacker/http/enricher/HttpEnricher.scala @@ -12,7 +12,7 @@ import pl.touk.nussknacker.http.enricher.HttpEnricher.{ApiKeyConfig, Body, BodyT import pl.touk.nussknacker.http.enricher.HttpEnricherParameters.BodyParam import sttp.client3.basicRequest import sttp.client3.circe._ -import sttp.model.{Header, Method, QueryParams, Uri} +import sttp.model.{Header, Headers, Method, QueryParams, Uri} import java.net.URL import scala.collection.immutable @@ -35,23 +35,25 @@ class HttpEnricher( componentUseCase: ComponentUseCase ): Future[AnyRef] = { val url = { - val urlParam = HttpEnricherParameters.UrlParam.extractor(context, params) - val queryParamsFromParam: QueryParams = HttpEnricherParameters.QueryParamsParam.extractor(context, params) match { - case null => QueryParams() - case jMap => QueryParams.fromMap(jMap.asScala.toMap) - } - val queryParamsApiKeys = securityConfig.collect { case q: ApiKeyInQuery => q.name -> q.value }.toMap - val allQueryParams = queryParamsFromParam.param(queryParamsApiKeys) - buildURL(rootUrl, urlParam, allQueryParams).fold(ex => throw ex, identity) + val urlParam = HttpEnricherParameters.UrlParam.extractor(context, params) + val queryParamsFromParam = HttpEnricherParameters.QueryParamsParam.extractor(context, params) + val queryParamsApiKeys = securityConfig.collect { case q: ApiKeyInQuery => q.name -> q.value }.toList + val allQueryParamsGrouped = + (queryParamsFromParam ++ queryParamsApiKeys) + val finalQueryParams = QueryParams.fromSeq(allQueryParamsGrouped) + buildURL(rootUrl, urlParam, finalQueryParams).fold(ex => throw ex, identity) } - val headers: List[Header] = HttpEnricherParameters.HeadersParam.extractor(context, params) match { - case null => List.empty - case jMap => - jMap.asScala.toMap.map { case (k, v) => - Header(k, v) - }.toList - } + // TODO: merging cookies? + val headersFromParam: List[(String, String)] = HttpEnricherParameters.HeadersParam.extractor(context, params) + val headersApiKeys = securityConfig.collect { case q: ApiKeyInHeader => q.name -> q.value }.toList + val headers = (headersFromParam ++ headersApiKeys) + .groupBy(_._1) + .map { case (k, v) => + k -> v.map(_._2).mkString(",") + } + .map(a => Header.apply(a._1, a._2)) + .toList val body = BodyParam.extractor(context, params, bodyType) @@ -88,7 +90,6 @@ class HttpEnricher( } val requestWithSecurityApplied = securityConfig.foldLeft(requestWithAppliedBody) { (request, securityToApply) => securityToApply match { - case ApiKeyInHeader(name, value) => request.header(name, value) case ApiKeyInCookie(name, value) => request.cookie(name, value) case _ => request } diff --git a/components/http/src/main/scala/pl/touk/nussknacker/http/enricher/HttpEnricherFactory.scala b/components/http/src/main/scala/pl/touk/nussknacker/http/enricher/HttpEnricherFactory.scala index e7c9bee6295..7061fdbd7d6 100644 --- a/components/http/src/main/scala/pl/touk/nussknacker/http/enricher/HttpEnricherFactory.scala +++ b/components/http/src/main/scala/pl/touk/nussknacker/http/enricher/HttpEnricherFactory.scala @@ -1,7 +1,6 @@ package pl.touk.nussknacker.http.enricher import pl.touk.nussknacker.engine.api._ -import pl.touk.nussknacker.engine.api.context.ProcessCompilationError.CustomNodeError import pl.touk.nussknacker.engine.api.context.transformation.{ DefinedEagerParameter, DefinedLazyParameter, @@ -16,14 +15,13 @@ import pl.touk.nussknacker.engine.api.typed.typing import pl.touk.nussknacker.engine.api.typed.typing.TypingResult import pl.touk.nussknacker.http.HttpEnricherConfig import pl.touk.nussknacker.http.client.HttpClientProvider -import pl.touk.nussknacker.http.enricher.HttpEnricher.{BodyType, HttpMethod, buildURL} +import pl.touk.nussknacker.http.enricher.HttpEnricher.{BodyType, HttpMethod} import pl.touk.nussknacker.http.enricher.HttpEnricherFactory.{ BodyParamExtractor, BodyTypeParamExtractor, TransformationState } import pl.touk.nussknacker.http.enricher.HttpEnricherParameters._ -import sttp.model.QueryParams class HttpEnricherFactory(val config: HttpEnricherConfig) extends EagerService @@ -67,10 +65,10 @@ class HttpEnricherFactory(val config: HttpEnricherConfig) implicit nodeId: NodeId ): ContextTransformationDefinition = { case TransformationStep( - (UrlParam.name, DefinedLazyParameter(lazyUrlParam)) :: - (QueryParamsParam.name, _) :: - (MethodParam.name, DefinedEagerParameter(httpMethod: String, _)) :: - (HeadersParam.name, _) :: + (UrlParam.name, DefinedLazyParameter(urlParamTypingResult)) :: + (QueryParamsParam.name, DefinedLazyParameter(queryParamsParamTypingResult)) :: + (MethodParam.name, DefinedEagerParameter(methodParamValue: String, _)) :: + (HeadersParam.name, DefinedLazyParameter(headersParamTypingResult)) :: (BodyTypeParam.name, _) :: parametersTail, Some(TransformationState.BodyTypeDeclared(bodyType)) @@ -78,17 +76,9 @@ class HttpEnricherFactory(val config: HttpEnricherConfig) val outName = OutputVariableNameDependency.extract(dependencies) val method = HttpMethod.values - .find(_.name == httpMethod) + .find(_.name == methodParamValue) .getOrElse(throw new IllegalStateException("Invalid body type parameter value.")) - val compileTimeUrlValidationErrorOpt = lazyUrlParam.valueOpt.flatMap { - case url: String => - buildURL(config.rootUrl, url, QueryParams()).swap.toOption.map(ex => - CustomNodeError(s"Invalid URL: ${ex.cause.getMessage}", Some(UrlParam.name)) - ) - case _ => None - } - val requestBodyTypingResult = bodyType match { case BodyType.None => typing.TypedNull case nonEmptyBodyType => @@ -101,14 +91,23 @@ class HttpEnricherFactory(val config: HttpEnricherConfig) } } + val compileTimeUrlValidation = UrlParam.validate(urlParamTypingResult, config.rootUrl) + val queryParamErrors = QueryParamsParam.validate(queryParamsParamTypingResult) + + // TODO http: add validation for String | List[String] in headers and query params + + val errors = compileTimeUrlValidation ++ queryParamErrors + + val outputTypingResult = HttpEnricherOutput.typingResult(requestBodyTypingResult) + FinalResults.forValidation( context, - compileTimeUrlValidationErrorOpt.toList, + errors, Some(TransformationState.FinalState(bodyType, method)) )(ctx => ctx.withVariable( outName, - HttpEnricherOutput.typingResult(requestBodyTypingResult), + outputTypingResult, Some(ParameterName(OutputVar.CustomNodeFieldName)) ) ) diff --git a/components/http/src/main/scala/pl/touk/nussknacker/http/enricher/HttpEnricherOutput.scala b/components/http/src/main/scala/pl/touk/nussknacker/http/enricher/HttpEnricherOutput.scala index 52e0452e6bb..7da75e0d52c 100644 --- a/components/http/src/main/scala/pl/touk/nussknacker/http/enricher/HttpEnricherOutput.scala +++ b/components/http/src/main/scala/pl/touk/nussknacker/http/enricher/HttpEnricherOutput.scala @@ -8,10 +8,9 @@ import pl.touk.nussknacker.http.enricher.MapExtensions.MapToHashMapExtension import sttp.client3.Response import sttp.model.MediaType -// TODO decision: can we leak headers / url / body in scenario? would it be enough to filter out configured securities? private[enricher] object HttpEnricherOutput { - // TODO: fill out request typing result with values determined at validation + // TODO: add typing results with values if evaulable def typingResult(requestBodyTypingResult: TypingResult): TypedObjectTypingResult = Typed.record( List( "request" -> Typed.record( @@ -26,13 +25,14 @@ private[enricher] object HttpEnricherOutput { List( "statusCode" -> Typed[Int], "statusText" -> Typed[String], - "headers" -> Typed.typedClass[java.util.Map[String, String]], + "headers" -> Typed.genericTypeClass[Map[_, _]](Typed[String] :: Typed[String] :: Nil), "body" -> Unknown ) ), ) ) + // TODO: filter out configured seucurities def buildOutput(response: Response[Either[String, String]], requestBody: Option[Body]): java.util.Map[String, _] = Map( "request" -> Map( @@ -69,7 +69,7 @@ private[enricher] object HttpEnricherOutput { case Right(value) => value } contentType match { - case s if s == MediaType.ApplicationJson.toString() => + case s if s.toLowerCase.contains(MediaType.ApplicationJson.toString()) => io.circe.parser.parse(body) match { case Right(json) => JsonUtils.jsonToAny(json) case Left(err) => @@ -77,11 +77,11 @@ private[enricher] object HttpEnricherOutput { input = body, message = s"Could not parse json: ${err.message}", cause = err.underlying - ) // TODO decision: if we cant parse - throw exception or return null? + ) } - case s if s == MediaType.TextPlain.toString() => body + case s if s.toLowerCase.contains(MediaType.TextPlain.toString()) => body /* - TODO decision: if we cant parse body, do we: + TODO decision: if we get an unsupported body type: 1. treat it as text/plain - pass it as string without parsing 2. throw exception 3. return null diff --git a/components/http/src/main/scala/pl/touk/nussknacker/http/enricher/HttpEnricherParameters.scala b/components/http/src/main/scala/pl/touk/nussknacker/http/enricher/HttpEnricherParameters.scala index 2e6a4826716..0447ca524ca 100644 --- a/components/http/src/main/scala/pl/touk/nussknacker/http/enricher/HttpEnricherParameters.scala +++ b/components/http/src/main/scala/pl/touk/nussknacker/http/enricher/HttpEnricherParameters.scala @@ -1,15 +1,18 @@ package pl.touk.nussknacker.http.enricher -import pl.touk.nussknacker.engine.api.{Context, Params} -import pl.touk.nussknacker.engine.api.definition.{ - FixedExpressionValue, - FixedValuesParameterEditor, - ParameterDeclaration -} +import pl.touk.nussknacker.engine.api.context.ProcessCompilationError.CustomNodeError +import pl.touk.nussknacker.engine.api.definition._ +import pl.touk.nussknacker.engine.api.exception.NonTransientException import pl.touk.nussknacker.engine.api.parameter.ParameterName -import pl.touk.nussknacker.http.enricher.HttpEnricher.{Body, BodyType, HttpMethod} +import pl.touk.nussknacker.engine.api.typed.typing +import pl.touk.nussknacker.engine.api.typed.typing.{TypedClass, TypingResult} +import pl.touk.nussknacker.engine.api.{Context, LazyParameter, NodeId, Params} +import pl.touk.nussknacker.http.enricher.HttpEnricher.{Body, BodyType, HttpMethod, buildURL} +import sttp.model.QueryParams import java.net.URL +import java.util +import scala.jdk.CollectionConverters._ /* TODO decision: add advanced parameters: @@ -22,22 +25,44 @@ private[enricher] object HttpEnricherParameters { object UrlParam { val name: ParameterName = ParameterName("URL") - def declaration(configuredRootUrl: Option[URL]) = { + // TODO http: make SpelTemplateEditor pretty + def declaration( + configuredRootUrl: Option[URL] + ): ParameterExtractor[LazyParameter[String]] with ParameterCreatorWithNoDependency = { val rootUrlHint = configuredRootUrl.map(r => s"""Root URL is configured. For current environment its value is: "${r.toString}"""") ParameterDeclaration .lazyMandatory[String](name) - .withCreator(modify = _.copy(hintText = rootUrlHint)) + .withCreator(modify = _.copy(hintText = rootUrlHint, editor = Some(SpelTemplateParameterEditor))) } val extractor: (Context, Params) => String = (context: Context, params: Params) => declaration(None).extractValueUnsafe(params).evaluate(context) + + def validate(urlParamTypingResult: TypingResult, rootUrl: Option[URL])( + implicit nodeId: NodeId + ): List[CustomNodeError] = { + urlParamTypingResult.valueOpt match { + case Some(url: String) => + buildURL(rootUrl, url, QueryParams()) match { + case Left(ex) => + List(CustomNodeError(s"Invalid URL: ${ex.cause.getMessage}", Some(UrlParam.name))) + case Right(_) => + List.empty + } + case _ => + List.empty + } + } + } object MethodParam { val name: ParameterName = ParameterName("HTTP Method") - def declaration(allowedMethods: List[HttpMethod]) = ParameterDeclaration + def declaration( + allowedMethods: List[HttpMethod] + ): ParameterExtractor[String] with ParameterCreatorWithNoDependency = ParameterDeclaration .mandatory[String](name) .withCreator(modify = _.copy(editor = @@ -51,28 +76,112 @@ private[enricher] object HttpEnricherParameters { } - // TODO decision: change the type to Map[String,AnyRef] to enable multiple values in header object HeadersParam { val name: ParameterName = ParameterName("Headers") - val declaration = - ParameterDeclaration.lazyOptional[java.util.Map[String, String]](name).withCreator() - val extractor: (Context, Params) => java.util.Map[String, String] = (context: Context, params: Params) => - declaration.extractValueUnsafe(params).evaluate(context) + val declaration: ParameterExtractor[LazyParameter[util.Map[String, AnyRef]]] with ParameterCreatorWithNoDependency = + ParameterDeclaration.lazyOptional[java.util.Map[String, AnyRef]](name).withCreator() + + // TODO: after migrating to sttp4 use DuplicateHeaderBehavior.Combine or DuplicateHeaderBehavior.Add + val extractor: (Context, Params) => List[(String, String)] = (context: Context, params: Params) => { + val rawMap = declaration + .extractValueUnsafe(params) + .evaluate(context) + if (rawMap == null) { + List.empty + } else { + rawMap.asScala.toList.flatMap { + case (k, v) => { + v match { + case str: String => List(k -> str) + case list: java.util.List[_] if list.asScala.forall(_.isInstanceOf[String]) => + list.asScala.map(el => k -> el.toString) + case _ => + throw NonTransientException(name.value, s"Invalid header value. Expected 'String' or 'List[String]'.") + } + } + } + } + } + } - // TODO decision: change the type to Map[String,AnyRef] to enable multiple values in query object QueryParamsParam { val name: ParameterName = ParameterName("Query Parameters") - val declaration = - ParameterDeclaration.lazyOptional[java.util.Map[String, String]](name).withCreator() - val extractor: (Context, Params) => java.util.Map[String, String] = (context: Context, params: Params) => - declaration.extractValueUnsafe(params).evaluate(context) + val declaration: ParameterExtractor[LazyParameter[util.Map[String, AnyRef]]] with ParameterCreatorWithNoDependency = + ParameterDeclaration.lazyOptional[java.util.Map[String, AnyRef]](name).withCreator() + + val extractor: (Context, Params) => List[(String, String)] = (context: Context, params: Params) => { + val rawMap = declaration + .extractValueUnsafe(params) + .evaluate(context) + if (rawMap == null) { + List.empty + } else { + rawMap.asScala.toList.flatMap { + case (k, v: String) => List(k -> v) + case (k, list: java.util.List[_]) if list.asScala.forall(_.isInstanceOf[String]) => + list.asScala.toList.asInstanceOf[List[String]].map(value => k -> value) + case _ => + throw NonTransientException( + name.value, + s"Invalid query parameter value. Expected 'String' or 'List[String]'." + ) + } + } + } + + def validate( + queryParamsParamTypingResult: TypingResult + )(implicit nodeId: NodeId): List[CustomNodeError] = + queryParamsParamTypingResult match { + case typing.TypedNull => List.empty +// case typing.TypedClass(klass, typingResultOfKey :: typingResultOfValue :: Nil) +// if klass == classOf[java.util.Map[_, _]] && typingResultOfKey.canBeSubclassOf(typing.Typed[String]) => { +// List.empty +// } + case typing.TypedObjectTypingResult(fields, _, _) => + fields.toList.flatMap { + case (queryParamName, typing.TypedObjectWithValue(tc @ TypedClass(klass, params), _)) => + if (tc.canBeSubclassOf(typing.Typed[String])) { + List.empty + } else if (klass == classOf[java.util.List[_]] && params.forall( + _.canBeSubclassOf(typing.Typed[String]) + )) { + List.empty + } else { + List( + CustomNodeError( + s"Invalid type at key '${queryParamName}'. Expected 'String', got: '${tc.withoutValue.display}'", + Some(QueryParamsParam.name) + ) + ) + } + case (queryParamName, tc @ typing.TypedClass(_, _)) if tc.canBeSubclassOf(typing.Typed[String]) => { + List.empty + } + case (queryParamName, valueTypingResult) => + List( + CustomNodeError( + s"Invalid type at key '${queryParamName}'. Expected 'String', got: '${valueTypingResult.withoutValue.display}'", + Some(QueryParamsParam.name) + ) + ) + } + case unexpectedTypingResult => + List( + CustomNodeError( + s"Invalid type. Expected 'Map[String,String]' or 'Map[String,List[String]]', got: '${unexpectedTypingResult.withoutValue.display}'", + Some(QueryParamsParam.name) + ) + ) + } + } object BodyTypeParam { val name: ParameterName = ParameterName("Body Type") - val declaration = ParameterDeclaration + val declaration: ParameterExtractor[String] with ParameterCreatorWithNoDependency = ParameterDeclaration .mandatory[String](name) .withCreator(modify = _.copy(editor = @@ -93,7 +202,8 @@ private[enricher] object HttpEnricherParameters { object BodyParam { val name: ParameterName = ParameterName("Body") - def declaration(bodyType: BodyType) = + def declaration(bodyType: BodyType): Option[ParameterExtractor[_ >: LazyParameter[String] <: LazyParameter[AnyRef]] + with ParameterCreatorWithNoDependency] = bodyType match { case BodyType.JSON => Some( diff --git a/components/http/src/test/scala/pl/touk/nussknacker/http/HttpEnricherBodyTest.scala b/components/http/src/test/scala/pl/touk/nussknacker/http/HttpEnricherBodyTest.scala index 9fb7544e926..d6f2f0a5587 100644 --- a/components/http/src/test/scala/pl/touk/nussknacker/http/HttpEnricherBodyTest.scala +++ b/components/http/src/test/scala/pl/touk/nussknacker/http/HttpEnricherBodyTest.scala @@ -17,6 +17,7 @@ class HttpEnricherBodyTest extends HttpEnricherTestSuite { ("contentType", "body", "expectedBodyRuntimeValue"), ("application/json", """ "string" """, "string"), ("application/json", "true", true), + ("application/json; charset=UTF-8", "true", true), ( "application/json", TestData.recordWithAllTypesNestedAsJson, @@ -206,7 +207,6 @@ class HttpEnricherBodyTest extends HttpEnricherTestSuite { |} |""".stripMargin.spel - val recordWithAllTypesNestedAsComparableAsNuRuntimeValue: Map[String, Any] = Map( "string" -> "this is a string", "number" -> java.math.BigDecimal.valueOf(123), diff --git a/components/http/src/test/scala/pl/touk/nussknacker/http/HttpEnricherHeadersTest.scala b/components/http/src/test/scala/pl/touk/nussknacker/http/HttpEnricherHeadersTest.scala index e178fb72697..2ef1b156946 100644 --- a/components/http/src/test/scala/pl/touk/nussknacker/http/HttpEnricherHeadersTest.scala +++ b/components/http/src/test/scala/pl/touk/nussknacker/http/HttpEnricherHeadersTest.scala @@ -52,7 +52,7 @@ class HttpEnricherHeadersTest extends HttpEnricherTestSuite { wireMock.stubFor( get(urlEqualTo("/header-test")) .withHeader("spel_header_1_key", new EqualToPattern("spel_header_1_value")) - .withHeader("spel_header_2_key", new EqualToPattern("input_header_2_key")) + .withHeader("spel_header_2_key", new EqualToPattern("input_header_2_value")) .willReturn(aResponse().withStatus(200)) ) val scenario = ScenarioBuilder @@ -69,7 +69,36 @@ class HttpEnricherHeadersTest extends HttpEnricherTestSuite { .emptySink("end", TestScenarioRunner.testResultSink, "value" -> "#httpOutput.response.statusCode".spel) val result = runner - .runWithData[String, Integer](scenario, List("input_header_2_key")) + .runWithData[String, Integer](scenario, List("input_header_2_value")) + .validValue + .successes + .head + + result shouldBe 200 + } + + test("makes request with evaluated header with value of list of strings") { + wireMock.stubFor( + get(urlEqualTo("/header-test")) + .withHeader("spel_header_1_key", new EqualToPattern("spel_header_1_value_1,spel_header_1_value_2")) + .withHeader("spel_header_2_key", new EqualToPattern("spel_header_2_value")) + .willReturn(aResponse().withStatus(200)) + ) + val scenario = ScenarioBuilder + .streaming("id") + .source("start", TestScenarioRunner.testDataSource) + .enricher( + "http-node-id", + "httpOutput", + configuredHeadersEnricher.name, + "URL" -> s"'${wireMock.baseUrl()}/header-test'".spel, + "HTTP Method" -> "'GET'".spel, + "Headers" -> "{ spel_header_1_key : {'spel_header_1_value_1', 'spel_header_1_value_2'}, spel_header_2_key: 'spel_header_2_value' }".spel, + ) + .emptySink("end", TestScenarioRunner.testResultSink, "value" -> "#httpOutput.response.statusCode".spel) + + val result = runner + .runWithData[String, Integer](scenario, List("irrelevant value")) .validValue .successes .head @@ -106,11 +135,10 @@ class HttpEnricherHeadersTest extends HttpEnricherTestSuite { result shouldBe 200 } - // TODO http: is this ok? even if we validate not overriding we cant ensure that in runtime - test("makes request with header from parameter that overwrites configured header") { + test("makes request with header from parameter that is merged with configured api key") { wireMock.stubFor( get(urlEqualTo("/header-test")) - .withHeader("configured_header_1_key", new EqualToPattern("overwriten_spel_header_1_value")) + .withHeader("configured_header_1_key", new EqualToPattern("spel_header_1_value,configured_header_1_value")) .willReturn(aResponse().withStatus(200)) ) val scenario = ScenarioBuilder @@ -122,7 +150,7 @@ class HttpEnricherHeadersTest extends HttpEnricherTestSuite { configuredHeadersEnricher.name, "URL" -> s"'${wireMock.baseUrl()}/header-test'".spel, "HTTP Method" -> "'GET'".spel, - "Headers" -> "{ configured_header_1_key : 'overwriten_spel_header_1_value' }".spel, + "Headers" -> "{ configured_header_1_key : 'spel_header_1_value' }".spel, ) .emptySink("end", TestScenarioRunner.testResultSink, "value" -> "#httpOutput.response.statusCode".spel) @@ -189,7 +217,6 @@ class HttpEnricherHeadersTest extends HttpEnricherTestSuite { result.asScala should contain("response_header_key" -> "response_header_value") } - // TODO http: is this behaviour ok? or should we treat {} as empty map to not confuse users? test("returns error when using list in headers parameter") { val scenario = ScenarioBuilder .streaming("id") @@ -211,7 +238,7 @@ class HttpEnricherHeadersTest extends HttpEnricherTestSuite { result should matchPattern { case ExpressionParserCompilationError( - "Bad expression type, expected: Map[String,String], found: List[Unknown]({})", + "Bad expression type, expected: Map[String,Unknown], found: List[Unknown]({})", _, Some(ParameterName("Headers")), _, diff --git a/components/http/src/test/scala/pl/touk/nussknacker/http/HttpEnricherOutputTest.scala b/components/http/src/test/scala/pl/touk/nussknacker/http/HttpEnricherOutputTest.scala index 4a20c8448cb..b8848713116 100644 --- a/components/http/src/test/scala/pl/touk/nussknacker/http/HttpEnricherOutputTest.scala +++ b/components/http/src/test/scala/pl/touk/nussknacker/http/HttpEnricherOutputTest.scala @@ -6,7 +6,6 @@ import pl.touk.nussknacker.engine.api.context.ValidationContext import pl.touk.nussknacker.engine.api.context.transformation.{ DefinedEagerParameter, DefinedLazyParameter, - DynamicComponent, OutputVariableNameValue } import pl.touk.nussknacker.engine.api.parameter.ParameterName diff --git a/components/http/src/test/scala/pl/touk/nussknacker/http/HttpEnricherURLTest.scala b/components/http/src/test/scala/pl/touk/nussknacker/http/HttpEnricherURLTest.scala index c495db015a1..0363cf14c17 100644 --- a/components/http/src/test/scala/pl/touk/nussknacker/http/HttpEnricherURLTest.scala +++ b/components/http/src/test/scala/pl/touk/nussknacker/http/HttpEnricherURLTest.scala @@ -67,6 +67,45 @@ class HttpEnricherURLTest extends HttpEnricherTestSuite { result shouldBe 200 } + test("makes request with query parameters with spel value and spel list") { + wireMock.stubFor( + get( + urlEqualTo( + "/url-test?spel_query_key_1=spel_query_value_1_1&spel_query_key_1=spel_query_value_1_2&spel_query_key_2=spel_query_value_2" + ) + ) + .willReturn( + aResponse().withStatus(200) + ) + ) + val scenario = ScenarioBuilder + .streaming("id") + .source("start", TestScenarioRunner.testDataSource) + .enricher( + "http", + "httpOutput", + noConfigHttpEnricherName, + "URL" -> s"'${wireMock.baseUrl}/url-test'".spel, + "Query Parameters" -> + """ + |{ + | spel_query_key_1 : {'spel_query_value_1_1', 'spel_query_value_1_2'}, + | spel_query_key_2 : 'spel_query_value_2' + |} + |""".stripMargin.spel, + "HTTP Method" -> "'GET'".spel, + ) + .emptySink("end", TestScenarioRunner.testResultSink, "value" -> "#httpOutput.response.statusCode".spel) + + val result = runner + .runWithData[String, Integer](scenario, List("input_query_value_1")) + .validValue + .successes + .head + + result shouldBe 200 + } + test("makes request under specified url with encoded query params") { wireMock.stubFor( get(urlEqualTo("/url-test?spel_query_unencoded_header_+!%23=spel_query_unencoded_header_+!%23"))