diff --git a/backend/build.sbt b/backend/build.sbt index beee8782..e3428624 100644 --- a/backend/build.sbt +++ b/backend/build.sbt @@ -93,9 +93,9 @@ val mockingbird = (project in file("mockingbird")) ) /* - Отдельный подпроект был создан ради того, чтобы не отключать coursier во всём проекте. - См. https://github.com/coursier/coursier/issues/2016 - Так как netty-tranport-epoll больше не используется можно перенести код отсюда в mockingbird + A separate subproject was created to avoid disabling coursier throughout the entire project. + See https://github.com/coursier/coursier/issues/2016. + Since netty-transport-epoll is no longer used, the code from here can be moved to mockingbird. */ lazy val `mockingbird-api` = (project in file("mockingbird-api")) .enablePlugins(BuildInfoPlugin) diff --git a/backend/circe-utils/src/main/scala/ru/tinkoff/tcb/utils/circe/optics/JsonOptic.scala b/backend/circe-utils/src/main/scala/ru/tinkoff/tcb/utils/circe/optics/JsonOptic.scala index fb05269d..f98eef96 100644 --- a/backend/circe-utils/src/main/scala/ru/tinkoff/tcb/utils/circe/optics/JsonOptic.scala +++ b/backend/circe-utils/src/main/scala/ru/tinkoff/tcb/utils/circe/optics/JsonOptic.scala @@ -19,7 +19,7 @@ final case class JsonOptic private[optics] (private val jsonPath: Seq[PathPart]) def set(v: Json): Json => Json = jsonPath.foldRight[Json => Json](_ => v)((part, f) => modifyPart(v)(part)(f)) /** - * Если передан Some(..) - обновляет поддерево, если передан None - удаляет существующее + * Updates subtress if arg is Some(..), removes subtree otherwise */ def setOpt(vo: Option[Json]): Json => Json = vo match { case Some(j) => set(j) diff --git a/backend/circe-utils/src/main/scala/ru/tinkoff/tcb/utils/circe/package.scala b/backend/circe-utils/src/main/scala/ru/tinkoff/tcb/utils/circe/package.scala index bb8cb05e..01c17423 100644 --- a/backend/circe-utils/src/main/scala/ru/tinkoff/tcb/utils/circe/package.scala +++ b/backend/circe-utils/src/main/scala/ru/tinkoff/tcb/utils/circe/package.scala @@ -71,20 +71,20 @@ package object circe { transformKeys(in => "_([a-z\\d])".r.replaceAllIn(in, _.group(1).toUpperCase)).result /** - * Производит слияние двух json объектов + * Merges two Json objects * * json1 :+ json2 * - * В случае совпадения значений по определённому ключу приоритетными являются значения из json1 + * In case of conflicts values from json1 take precedence */ @inline def :+(other: Json): Json = merge(other, json, false) /** - * Производит слияние двух json объектов + * Merges two Json objects * * json1 +: json2 * - * В случае совпадения значений по определённому ключу приоритетными являются значения из json2 + * In case of conflicts values from json2 take precedence */ @inline def +:(other: Json): Json = merge(other, json, false) diff --git a/backend/dataAccess/src/main/scala/ru/tinkoff/tcb/bson/package.scala b/backend/dataAccess/src/main/scala/ru/tinkoff/tcb/bson/package.scala index c343be2e..78e1111f 100644 --- a/backend/dataAccess/src/main/scala/ru/tinkoff/tcb/bson/package.scala +++ b/backend/dataAccess/src/main/scala/ru/tinkoff/tcb/bson/package.scala @@ -33,7 +33,7 @@ package object bson { private type PartialEndo2[A, B] = PartialFunction[(A, B), (A, B)] /* - Экстракторы + Extractors */ object BUndef { @@ -108,7 +108,7 @@ package object bson { } /* - Расширения + Extensions */ implicit final class BsonDocumentObjExt(private val doc: BsonDocument.type) extends AnyVal { @@ -123,20 +123,20 @@ package object bson { @inline def decodeOpt[T: BsonDecoder]: Option[T] = decodeAs[T].toOption /** - * Производит слияние двух bson значений + * Merges two bson values * * bson1 :+ bson2 * - * В случае совпадения значений по определённому ключу приоритетными являются значения из bson1 + * In case of conflicts values from bson1 take precedence */ @inline def :+(other: BsonValue): BsonValue = merge(other, bv, false) /** - * Производит слияние двух bson значений + * Merges two bson values * * bson1 :+ bson2 * - * В случае совпадения значений по определённому ключу приоритетными являются значения из bson2 + * In case of conflicts values from bson2 take precedence */ @inline def +:(other: BsonValue): BsonValue = merge(other, bv, false) } diff --git a/backend/edsl/src/main/scala/ru/tinkoff/tcb/mockingbird/edsl/ExampleSet.scala b/backend/edsl/src/main/scala/ru/tinkoff/tcb/mockingbird/edsl/ExampleSet.scala index e0fcdbcd..1b0ac0a8 100644 --- a/backend/edsl/src/main/scala/ru/tinkoff/tcb/mockingbird/edsl/ExampleSet.scala +++ b/backend/edsl/src/main/scala/ru/tinkoff/tcb/mockingbird/edsl/ExampleSet.scala @@ -5,156 +5,13 @@ import org.scalactic.source import ru.tinkoff.tcb.mockingbird.edsl.model.* -/** - * ==Описание набора примеров== - * - * `ExampleSet` предоставляет DSL для описания примеров взаимодействия с Mockingbird со стороны внешнего - * приложения/пользователя через его API. Описанные примеры потом можно в Markdown описание последовательности действий - * с примерами HTTP запросов и ответов на них или сгенерировать тесты для scalatest. За это отвечают интерпретаторы DSL - * [[ru.tinkoff.tcb.mockingbird.edsl.interpreter.MarkdownGenerator MarkdownGenerator]] и - * [[ru.tinkoff.tcb.mockingbird.edsl.interpreter.AsyncScalaTestSuite AsyncScalaTestSuite]] соответственно. - * - * Описание набора примеров может выглядеть так: - * - * {{{ - * package ru.tinkoff.tcb.mockingbird.examples - * - * import ru.tinkoff.tcb.mockingbird.edsl.ExampleSet - * import ru.tinkoff.tcb.mockingbird.edsl.model.* - * import ru.tinkoff.tcb.mockingbird.edsl.model.Check.* - * import ru.tinkoff.tcb.mockingbird.edsl.model.HttpMethod.* - * import ru.tinkoff.tcb.mockingbird.edsl.model.ValueMatcher.syntax.* - * - * class CatsFacts[HttpResponseR] extends ExampleSet[HttpResponseR] { - * - * override val name = "Примеры использования ExampleSet" - * - * example("Получение случайного факта о котиках")( - * for { - * _ <- describe("Отправить GET запрос") - * resp <- sendHttp( - * method = Get, - * path = "/fact", - * headers = Seq("X-CSRF-TOKEN" -> "unEENxJqSLS02rji2GjcKzNLc0C0ySlWih9hSxwn") - * ) - * _ <- describe("Ответ содержит случайный факт полученный с сервера") - * _ <- checkHttp( - * resp, - * HttpResponseExpected( - * code = Some(CheckInteger(200)), - * body = Some( - * CheckJsonObject( - * "fact" -> CheckJsonString("There are approximately 100 breeds of cat.".sample), - * "length" -> CheckJsonNumber(42.sample) - * ) - * ), - * headers = Seq("Content-Type" -> CheckString("application/json")) - * ) - * ) - * } yield () - * ) - * } - * }}} - * - * Дженерик параметр `HttpResponseR` нужен так результат выполнения HTTP запроса зависит от интерпретатора DSL. - * - * Переменная `name` - общий заголовок для примеров внутри набора, при генерации Markdown файла будет добавлен в самое - * начало как заголовок первого уровня. - * - * Метод `example` позволяет добавить пример к набору. Вначале указывается название примера, как первый набор - * аргументов. При генерации тестов это будет именем теста, а при генерации Markdown будет добавлено как заголовок - * второго уровня, затем описывается сам пример. Последовательность действий описывается при помощи монады - * [[ru.tinkoff.tcb.mockingbird.edsl.model.Example Example]]. - * - * `ExampleSet` предоставляет следующие действия: - * - [[describe]] - добавить текстовое описание. - * - [[sendHttp]] - исполнить HTTP запрос с указанными параметрами, возвращает результат запроса. - * - [[checkHttp]] - проверить, что результат запроса отвечает указанным ожиданиям, возвращает извлеченные из ответа - * данные на основании проверок. ''Если предполагается использовать какие-то части ответа по ходу описания примера, - * то необходимо для них задать ожидания, иначе они будут отсутствовать в возвращаемом объекте.'' - * - * Для описания ожиданий используются проверки [[model.Check$]]. Некоторые проверки принимают как параметр - * [[model.ValueMatcher ValueMatcher]]. Данный трейт тип представлен двумя реализациями - * [[model.ValueMatcher.AnyValue AnyValue]] и [[model.ValueMatcher.FixedValue FixedValue]]. Первая описывает - * произвольное значение определенного типа, т.е. проверки значения не производится. Вторая задает конкретное ожидаемое - * значение. - * - * Для упрощения создания значений типа [[model.ValueMatcher ValueMatcher]] добавлены имплиситы в объекте - * [[model.ValueMatcher.syntax ValueMatcher.syntax]]. Они добавляют неявную конвертацию значений в тип - * [[model.ValueMatcher.FixedValue FixedValue]], а так же методы `sample` и `fixed` для создания - * [[model.ValueMatcher.AnyValue AnyValue]] и [[model.ValueMatcher.FixedValue FixedValue]] соответственно. Благодаря - * этому можно писать: - * {{{ - * CheckString("some sample".sample) // вместо CheckString(AnyValue("some sample")) - * CheckString("some fixed string") // вместо CheckString(FixedValue("some fixed string")) - * }}} - * - * ==Генерации markdown документа из набора примеров== - * - * {{{ - * package ru.tinkoff.tcb.mockingbird.examples - * - * import sttp.client3.* - * - * import ru.tinkoff.tcb.mockingbird.edsl.interpreter.MarkdownGenerator - * - * object CatsFactsMd { - * def main(args: Array[String]): Unit = { - * val mdg = MarkdownGenerator(baseUri = uri"https://catfact.ninja") - * val set = new CatsFacts[MarkdownGenerator.HttpResponseR]() - * println(mdg.generate(set)) - * } - * } - * }}} - * - * Здесь создается интерпретатор [[ru.tinkoff.tcb.mockingbird.edsl.interpreter.MarkdownGenerator MarkdownGenerator]] для - * генерации markdown документа из инстанса `ExampleSet`. Как параметр, конструктору передается хост со схемой который - * будет подставлен в качестве примера в документ. - * - * Как упоминалось ранее, тип ответа от HTTP сервера зависит от интерпретатора DSL, поэтому при создании `CatsFacts` - * параметром передается тип `MarkdownGenerator.HttpResponseR`. - * - * ==Генерация тестов из набора примеров== - * {{{ - * package ru.tinkoff.tcb.mockingbird.examples - * - * import sttp.client3.* - * - * import ru.tinkoff.tcb.mockingbird.edsl.interpreter.AsyncScalaTestSuite - * - * class CatsFactsSuite extends AsyncScalaTestSuite { - * override val baseUri = uri"https://catfact.ninja" - * val set = new CatsFacts[HttpResponseR]() - * generateTests(set) - * } - * }}} - * - * Для генерации тестов нужно создать класс и унаследовать его от - * [[ru.tinkoff.tcb.mockingbird.edsl.interpreter.AsyncScalaTestSuite AsyncScalaTestSuite]]. После чего в переопределить - * значение `baseUri` и в конструкторе вызвать метод `generateTests` передав в него набор примеров. В качестве дженерик - * параметра для типа HTTP ответа, в создаваемый инстанс набора примеров надо передать тип - * [[ru.tinkoff.tcb.mockingbird.edsl.interpreter.AsyncScalaTestSuite.HttpResponseR AsyncScalaTestSuite.HttpResponseR]] - * - * Пример запуска тестов: - * {{{ - * [info] CatsFactsSuite: - * [info] - Получение случайного факта о котиках - * [info] + Отправить GET запрос - * [info] + Ответ содержит случайный факт полученный с сервера - * [info] Run completed in 563 milliseconds. - * [info] Total number of tests run: 1 - * [info] Suites: completed 1, aborted 0 - * [info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0 - * [info] All tests passed. - * }}} - */ trait ExampleSet[HttpResponseR] { private var examples_ : Vector[ExampleDescription] = Vector.empty final private[edsl] def examples: Vector[ExampleDescription] = examples_ /** - * Заглавие набора примеров. + * Title of the example set. */ def name: String @@ -162,30 +19,30 @@ trait ExampleSet[HttpResponseR] { examples_ = examples_ :+ ExampleDescription(name, body, pos) /** - * Выводит сообщение при помощи `info` при генерации тестов или добавляет текстовый блок при генерации Markdown. + * Prints a message using info during test generation or adds a text block during Markdown generation. * @param text - * текст сообщения + * The message text */ final def describe(text: String)(implicit pos: source.Position): Example[Unit] = liftF[Step, Unit](Describe(text, pos)) /** - * В тестах, выполняет HTTP запрос с указанными параметрами или добавляет в Markdown пример запроса, который можно - * исполнить командой `curl`. + * In tests, makes an HTTP request with the specified parameters or adds a sample request to the Markdown, which can + * be executed with the curl command. * * @param method - * используемый HTTP метод. + * HTTP method used. * @param path - * путь до ресурса без схемы и хоста. + * path to the resource without the scheme and host. * @param body - * тело запроса как текст. + * request body as text.. * @param headers - * заголовки, который будут переданы вместе с запросом. + * request headers to send. * @param query - * URL параметры запроса + * URL query parameters. * @return - * возвращает объект представляющий собой результат исполнения запроса, конкретный тип зависит от интерпретатора - * DSL. Использовать возвращаемое значение можно только передав в метод [[checkHttp]]. + * Returns an object representing the result of the request execution; the specific type depends on the DSL + * interpreter. The return value can only be used by passing it to the [[checkHttp]] method. */ final def sendHttp( method: HttpMethod, @@ -199,18 +56,18 @@ trait ExampleSet[HttpResponseR] { liftF[Step, HttpResponseR](SendHttp[HttpResponseR](HttpRequest(method, path, body, headers, query), pos)) /** - * В тестах, проверяет, что полученный HTTP ответ соответствует ожиданиям. При генерации Markdown вставляет ожидаемый - * ответ опираясь на указанные ожидания. Если никакие ожидания не указана, то ничего добавлено не будет. + * In tests, verifies that the received HTTP response matches the expectations. When generating Markdown, inserts the + * expected response based on the specified expectations. If no expectations are specified, nothing will be added. * * @param response - * результат исполнения [[sendHttp]], тип зависит от интерпретатора DSL. + * the result of executing [[sendHttp]], the type depends on the DSL interpreter. * @param expects - * ожидания предъявляемые к результату HTTP запроса. Ожидания касаются кода ответа, тела запроса и заголовков - * полеченных от сервера. + * expectations placed on the result of the HTTP request. Expectations concern the response code, request body, and + * headers received from the server. * @return - * возвращает разобранный ответ от сервера. При генерации Markdown, так как реального ответа от сервера нет, то - * формирует ответ на основании переданных ожиданий от ответа. В Markdown добавляется информация только от том, для - * чего была указана проверка. + * returns the parsed response from the server. When generating Markdown, since there is no actual response from the + * server, it constructs a response based on the provided response expectations. Only information relevant to the + * specified checks is added to the Markdown. */ final def checkHttp(response: HttpResponseR, expects: HttpResponseExpected)(implicit pos: source.Position diff --git a/backend/edsl/src/main/scala/ru/tinkoff/tcb/mockingbird/edsl/interpreter/AsyncScalaTestSuite.scala b/backend/edsl/src/main/scala/ru/tinkoff/tcb/mockingbird/edsl/interpreter/AsyncScalaTestSuite.scala index 61d9938b..c46397d1 100644 --- a/backend/edsl/src/main/scala/ru/tinkoff/tcb/mockingbird/edsl/interpreter/AsyncScalaTestSuite.scala +++ b/backend/edsl/src/main/scala/ru/tinkoff/tcb/mockingbird/edsl/interpreter/AsyncScalaTestSuite.scala @@ -21,14 +21,13 @@ import ru.tinkoff.tcb.mockingbird.edsl.model.Check.* import ru.tinkoff.tcb.mockingbird.edsl.model.ValueMatcher.* /** - * Базовый трейт для генерации набора тестов по набору примеров - * [[ru.tinkoff.tcb.mockingbird.edsl.ExampleSet ExampleSet]]. + * Base trait for generating a set of tests from an [[ru.tinkoff.tcb.mockingbird.edsl.ExampleSet ExampleSet]]. * - * Трейт наследуется от `AsyncFunSuiteLike` из фреймоврка [[https://www.scalatest.org/ ScalaTest]], поэтому внутри можно - * как дописать дополнительные тесты, так и использовать - * [[https://www.scalatest.org/user_guide/sharing_fixtures#beforeAndAfter BeforeAndAfter]] и/или - * [[https://www.scalatest.org/user_guide/sharing_fixtures#composingFixtures BeforeAndAfterEach]] для управления - * поднятием необходимого для исполнения тестов окружения, в том числе используя + * This trait inherits from AsyncFunSuiteLike in the [[https://www.scalatest.org/ ScalaTest]] framework, so you can add + * additional tests inside it or use + * [[https://www.scalatest.org/user_guide/sharing_fixtures#beforeAndAfter BeforeAndAfter]] and/or + * [[https://www.scalatest.org/user_guide/sharing_fixtures#composingFixtures BeforeAndAfterEach]] to manage the setup of + * the necessary environment for executing tests, including using * [[https://github.com/testcontainers/testcontainers-scala testcontainers-scala]]. */ trait AsyncScalaTestSuite extends AsyncFunSuiteLike { @@ -40,12 +39,12 @@ trait AsyncScalaTestSuite extends AsyncFunSuiteLike { private[interpreter] def sttpbackend: SttpBackend[Future] = sttpbackend_ /** - * URI относительно которого будут разрешаться пути используемые в примерах + * URI relative to which paths used in examples will be resolved */ def baseUri: Uri /** - * Сгенерировать тесты из набора примеров. + * Generate tests from the set of examples. */ protected def generateTests(es: ExampleSet[HttpResponseR]): Unit = es.examples.foreach { desc => diff --git a/backend/edsl/src/main/scala/ru/tinkoff/tcb/mockingbird/edsl/interpreter/MarkdownGenerator.scala b/backend/edsl/src/main/scala/ru/tinkoff/tcb/mockingbird/edsl/interpreter/MarkdownGenerator.scala index e1a8fb53..4b3f49d8 100644 --- a/backend/edsl/src/main/scala/ru/tinkoff/tcb/mockingbird/edsl/interpreter/MarkdownGenerator.scala +++ b/backend/edsl/src/main/scala/ru/tinkoff/tcb/mockingbird/edsl/interpreter/MarkdownGenerator.scala @@ -68,10 +68,10 @@ object MarkdownGenerator { } /** - * Интерпретатор DSL создающий markdown документ с описанием примера. + * DSL interpreter creating a markdown document with an example description. * * @param baseUri - * URI относительно которого будут разрешаться пути используемые в примерах + * URI relative to which paths used in examples will be resolved. */ final class MarkdownGenerator(baseUri: Uri) { import MarkdownGenerator.HttpResponseR @@ -82,12 +82,12 @@ final class MarkdownGenerator(baseUri: Uri) { private[interpreter] type W[A] = Writer[Vector[Markdown], A] /** - * Сгенерировать markdown документ из переданного набора примеров. + * Generate a markdown document from the provided set of examples. * * @param set - * набор примеров + * examples set * @return - * строка содержащая markdown документ. + * A string containing a markdown document. */ def generate(set: ExampleSet[HttpResponseR]): String = { val tags = for { @@ -125,16 +125,16 @@ final class MarkdownGenerator(baseUri: Uri) { case CheckHttp(_, HttpResponseExpected(code, body, headers), _) => val bodyStr = body.map(_.show) val cb = Vector( - code.map(c => s"Код ответа: ${c.matcher.show}\n"), + code.map(c => s"Response code: ${c.matcher.show}\n"), headers.nonEmpty.option { - headers.map { case (k, v) => s"$k: '${v.matcher.show}'" }.mkString("Заголовки ответа:\n", "\n", "\n") + headers.map { case (k, v) => s"$k: '${v.matcher.show}'" }.mkString("Response headers:\n", "\n", "\n") }, - bodyStr.map("Тело ответа:\n" ++ _ ++ "\n"), + bodyStr.map("Response body:\n" ++ _ ++ "\n"), ).flatten.mkString("\n") Writer( Vector( - p("Ответ:"), + p("Response:"), codeBlock(cb) ), HttpResponse( diff --git a/backend/edsl/src/main/scala/ru/tinkoff/tcb/mockingbird/edsl/model/Check.scala b/backend/edsl/src/main/scala/ru/tinkoff/tcb/mockingbird/edsl/model/Check.scala index faf1b9d3..fe6078ac 100644 --- a/backend/edsl/src/main/scala/ru/tinkoff/tcb/mockingbird/edsl/model/Check.scala +++ b/backend/edsl/src/main/scala/ru/tinkoff/tcb/mockingbird/edsl/model/Check.scala @@ -1,23 +1,25 @@ package ru.tinkoff.tcb.mockingbird.edsl.model + import io.circe.Json sealed trait ValueMatcher[T] extends Product with Serializable object ValueMatcher { /** - * Показывает, что ожидается конкретное значение типа `T`, в случае несовпадения сгенерированный тест упадет с - * ошибкой. + * Indicates that a specific value of type 'T' is expected, and if there is a mismatch, the generated test will fail + * with an error. * * @param value - * значение используемое для сравнения и отображения при генерации примера ответа от сервера в markdown. + * The value used for comparison and display when generating an example server response in markdown. */ final case class FixedValue[T](value: T) extends ValueMatcher[T] /** - * Показывает, что ожидается любое значение типа `T`. + * Indicates that any value of type 'T' is expected. * * @param example - * Это значение будет отображено в markdown документе при генерации в описании примера ответа от сервера. + * This value will be displayed in the markdown document when generated in the description of the example server + * response. */ final case class AnyValue[T](example: T) extends ValueMatcher[T] @@ -41,10 +43,10 @@ sealed trait Check extends Product with Serializable object Check { /** - * Соответствует любому значению. + * Corresponds to any value * * @param example - * значение, которое будет использоваться как пример при генерации Markdown. + * The value that will be used as an example when generating Markdown. * @group CheckCommon */ final case class CheckAny(example: String) extends Check @@ -60,35 +62,37 @@ object Check { final case class CheckInteger(matcher: ValueMatcher[Long]) extends Check /** - * Показывает, что ожидается JSON, реализации этого трейта позволяют детальнее описать ожидания. + * Indicates that JSON is expected. Implementations of this trait allow for a more detailed description of + * expectations. * @group CheckJson */ sealed trait CheckJson extends Check /** - * Значение null + * null value * @group CheckJson */ final case object CheckJsonNull extends CheckJson /** - * Любой валидный JSON. + * Any valid JSON. * * @constructor * @param example - * значение, которое будет использоваться как пример при генерации Markdown. + * The value that will be used as an example when generating Markdown. * @group CheckJson */ final case class CheckJsonAny(example: Json) extends CheckJson /** - * JSON объект с указанными полями, объект с которым производится сравнение может содержать дополнительные поля. + * A JSON object with the specified fields. The object being compared with may contain additional fields. * @group CheckJson */ final case class CheckJsonObject(fields: (String, CheckJson)*) extends CheckJson /** - * Массив с указанными элементами, важен порядок. Проверяемы массив может содержать в конце дополнительные элементы. + * An array with the specified elements, the order is important. The array being checked may contain additional + * elements at the end. * @group CheckJson */ final case class CheckJsonArray(items: CheckJson*) extends CheckJson diff --git a/backend/edsl/src/test/scala/ru/tinkoff/tcb/mockingbird/edsl/interpreter/AsyncScalaTestSuiteWholeTest.scala b/backend/edsl/src/test/scala/ru/tinkoff/tcb/mockingbird/edsl/interpreter/AsyncScalaTestSuiteWholeTest.scala index 25395c94..c68c140f 100644 --- a/backend/edsl/src/test/scala/ru/tinkoff/tcb/mockingbird/edsl/interpreter/AsyncScalaTestSuiteWholeTest.scala +++ b/backend/edsl/src/test/scala/ru/tinkoff/tcb/mockingbird/edsl/interpreter/AsyncScalaTestSuiteWholeTest.scala @@ -44,12 +44,12 @@ class AsyncScalaTestSuiteWholeTest (mockI .apply(_: String, _: Option[Any])(_: source.Position)) - .expects("Отправить GET запрос", *, *) + .expects("Send a GET request", *, *) .once() (mockI .apply(_: String, _: Option[Any])(_: source.Position)) - .expects("Ответ содержит случайный факт полученный с сервера", *, *) + .expects("The response contains a random fact obtained from the server", *, *) .twice() mockInformer = mockI.some @@ -74,7 +74,7 @@ class AsyncScalaTestSuiteWholeTest } override protected def afterAll(): Unit = { - calledTests shouldBe Vector("fake", "Получение случайного факта о котиках") + calledTests shouldBe Vector("fake", "Getting a random fact about kittens") super.afterAll() } diff --git a/backend/edsl/src/test/scala/ru/tinkoff/tcb/mockingbird/edsl/interpreter/MarkdownGeneratorSuite.scala b/backend/edsl/src/test/scala/ru/tinkoff/tcb/mockingbird/edsl/interpreter/MarkdownGeneratorSuite.scala index 50c0ce28..e53d3e00 100644 --- a/backend/edsl/src/test/scala/ru/tinkoff/tcb/mockingbird/edsl/interpreter/MarkdownGeneratorSuite.scala +++ b/backend/edsl/src/test/scala/ru/tinkoff/tcb/mockingbird/edsl/interpreter/MarkdownGeneratorSuite.scala @@ -114,15 +114,15 @@ class MarkdownGeneratorSuite extends AnyFunSuite with Matchers { val obtains = markdown(mds).md val expected = raw""" - |Ответ: + |Response: |``` - |Код ответа: 418 + |Response code: 418 | - |Заголовки ответа: + |Response headers: |Content-Type: 'application/json' |token: 'token-example' | - |Тело ответа: + |Response body: |{ | "foo" : [ | ], @@ -142,10 +142,10 @@ class MarkdownGeneratorSuite extends AnyFunSuite with Matchers { val mdg = MarkdownGenerator(uri"https://catfact.ninja") val set = new CatsFacts[MarkdownGenerator.HttpResponseR]() mdg.generate(set) shouldBe - """# Примеры использования ExampleSet - |## Получение случайного факта о котиках + """# Examples of using ExampleSet + |## Getting a random fact about kittens | - |Отправить GET запрос + |Send a GET request |``` |curl \ | --request GET \ @@ -154,16 +154,16 @@ class MarkdownGeneratorSuite extends AnyFunSuite with Matchers { | |``` | - |Ответ содержит случайный факт полученный с сервера + |The response contains a random fact obtained from the server | - |Ответ: + |Response: |``` - |Код ответа: 200 + |Response code: 200 | - |Заголовки ответа: + |Response headers: |Content-Type: 'application/json' | - |Тело ответа: + |Response body: |{ | "fact" : "There are approximately 100 breeds of cat.", | "length" : 42.0 diff --git a/backend/edsl/src/test/scala/ru/tinkoff/tcb/mockingbird/examples/CatsFacts.scala b/backend/edsl/src/test/scala/ru/tinkoff/tcb/mockingbird/examples/CatsFacts.scala index 6a4a1e09..7dd81ca2 100644 --- a/backend/edsl/src/test/scala/ru/tinkoff/tcb/mockingbird/examples/CatsFacts.scala +++ b/backend/edsl/src/test/scala/ru/tinkoff/tcb/mockingbird/examples/CatsFacts.scala @@ -8,17 +8,17 @@ import ru.tinkoff.tcb.mockingbird.edsl.model.ValueMatcher.syntax.* class CatsFacts[HttpResponseR] extends ExampleSet[HttpResponseR] { - override val name = "Примеры использования ExampleSet" + override val name = "Examples of using ExampleSet" - example("Получение случайного факта о котиках")( + example("Getting a random fact about kittens")( for { - _ <- describe("Отправить GET запрос") + _ <- describe("Send a GET request") resp <- sendHttp( method = Get, path = "/fact", headers = Seq("X-CSRF-TOKEN" -> "unEENxJqSLS02rji2GjcKzNLc0C0ySlWih9hSxwn") ) - _ <- describe("Ответ содержит случайный факт полученный с сервера") + _ <- describe("The response contains a random fact obtained from the server") _ <- checkHttp( resp, HttpResponseExpected( diff --git a/backend/examples/src/main/scala/ru/tinkoff/tcb/mockingbird/examples/BasicHttpStub.scala b/backend/examples/src/main/scala/ru/tinkoff/tcb/mockingbird/examples/BasicHttpStub.scala index 1a4d935f..11be8a99 100644 --- a/backend/examples/src/main/scala/ru/tinkoff/tcb/mockingbird/examples/BasicHttpStub.scala +++ b/backend/examples/src/main/scala/ru/tinkoff/tcb/mockingbird/examples/BasicHttpStub.scala @@ -10,17 +10,13 @@ import ru.tinkoff.tcb.utils.circe.optics.* class BasicHttpStub[HttpResponseR] extends ExampleSet[HttpResponseR] { import ValueMatcher.syntax.* - val name = "Базовые примеры работы с HTTP заглушками" + val name = "Basic examples of working with HTTP stubs" - example("Persistent, ephemeral и countdown HTTP заглушки") { + example("Persistent, ephemeral, and countdown HTTP stubs") { for { - // TODO: подумать можно ли описать какие-то пререквизиты, чтобы в тестах - // автоматически их проверять и возможно исполнять. Например, проверить - // и создать сервис, если его нет, запустить сервис до которого будет - // mockingbird проксировать запрос и т.п. - _ <- describe("Предполагается, что в mockingbird есть сервис `alpha`.") + _ <- describe("It is assumed that in mockingbird there is a service `alpha`.") - _ <- describe("Создаем заглушку в скоупе `persistent`.") + _ <- describe("Creating a stub in the persistent `scope`.") resp <- sendHttp( method = HttpMethod.Post, path = "/api/internal/mockingbird/v2/stub", @@ -57,7 +53,7 @@ class BasicHttpStub[HttpResponseR] extends ExampleSet[HttpResponseR] { ) ) - _ <- describe("Проверяем созданную заглушку.") + _ <- describe("Checking the created stub.") resp <- sendHttp( method = HttpMethod.Get, path = "/api/mockingbird/exec/alpha/handler1", @@ -73,7 +69,7 @@ class BasicHttpStub[HttpResponseR] extends ExampleSet[HttpResponseR] { ) ) - _ <- describe("Для этого же пути, создаем заглушку в скоупе `ephemeral`.") + _ <- describe("For the same path, creating a stub in the `ephemeral` scope.") resp <- sendHttp( method = HttpMethod.Post, path = "/api/internal/mockingbird/v2/stub", @@ -111,7 +107,7 @@ class BasicHttpStub[HttpResponseR] extends ExampleSet[HttpResponseR] { ) idEphemeral = parser.parse(r.body.get).toOption.flatMap((JLens \ "id").getOpt).flatMap(_.asString).get - _ <- describe("И создаем заглушку в скоупе `countdown` с `times` равным 2.") + _ <- describe("And creating a stub in the `countdown` scope with `times` equal to 2.") resp <- sendHttp( method = HttpMethod.Post, path = "/api/internal/mockingbird/v2/stub", @@ -150,18 +146,15 @@ class BasicHttpStub[HttpResponseR] extends ExampleSet[HttpResponseR] { ) _ <- describe( - """Заданные заглушки отличаются возвращаемыми ответами, а именно содержимым `body` и `code`, - | в целом они могут быть как и полностью одинаковыми так и иметь больше различий. - | Скоупы заглушек в порядке убывания приоритета: Countdown, Ephemeral, Persistent""".stripMargin + """The specified stubs differ in the responses they return, namely the contents of `body` and `code`, + | in general, they can be either completely identical or have more differences. + | The scopes of stubs in descending order of priority: Countdown, Ephemeral, Persistent""".stripMargin ) _ <- describe( - """Так как заглушка `countdown` была создана с `times` равным двум, то следующие два - |запроса вернут указанное в ней содержимое.""".stripMargin + """Since the countdown stub was created with `times` equal to two, the next two + |requests will return the specified content.""".stripMargin ) - // TODO: при генерации Markdown будет дважды добавлен запрос и ожидаемый ответ, - // может стоит добавить действие Repeat, чтобы при генерации Markdown в документе - // указывалось бы, что данное действие повторяется N раз. _ <- Seq .fill(2)( for { @@ -184,8 +177,8 @@ class BasicHttpStub[HttpResponseR] extends ExampleSet[HttpResponseR] { .sequence _ <- describe( - """Последующие запросы будут возвращать содержимое заглушки `ephemeral`. Если бы её не было, - |то вернулся бы ответ от заглушки `persistent`.""".stripMargin + """Subsequent requests will return the content of the `ephemeral` stub. If it didn't exist, + |the response from the `persistent` stub would be returned..""".stripMargin ) resp <- sendHttp( method = HttpMethod.Get, @@ -202,8 +195,8 @@ class BasicHttpStub[HttpResponseR] extends ExampleSet[HttpResponseR] { ) ) - _ <- describe("""Чтобы получить теперь ответ от `persistent` заглушки нужно или дождаться, когда истекут - |сутки с момента её создания или просто удалить `ephemeral` заглушку.""".stripMargin) + _ <- describe("""Now to get a response from the `persistent` stub, one must either wait until a day has passed + |since its creation or simply delete the `ephemeral` stub.""".stripMargin) resp <- sendHttp( method = HttpMethod.Delete, path = s"/api/internal/mockingbird/v2/stub/$idEphemeral", @@ -222,7 +215,7 @@ class BasicHttpStub[HttpResponseR] extends ExampleSet[HttpResponseR] { ) ) - _ <- describe("После удаления `ephemeral` заглушки, при запросе вернется результат заглушки `persistent`") + _ <- describe("After deleting the `ephemeral` stub, a request will return the result of the `persistent` stub.") resp <- sendHttp( method = HttpMethod.Get, path = "/api/mockingbird/exec/alpha/handler1", @@ -240,22 +233,22 @@ class BasicHttpStub[HttpResponseR] extends ExampleSet[HttpResponseR] { } yield () } - example("Использование параметров пути в HTTP заглушках") { + example("Using path parameters in HTTP stubs") { for { _ <- describe( - """Заглушка может выбираться в том числе и на основании регулярного выражения - |в пути, это может быть не очень эффективно с точки зрения поиска такой заглушки. - |Поэтому без необходимости, лучше не использовать этот механизм.""".stripMargin + """A stub can also be selected based on a regular expression in the path, + |which can be inefficient in terms of searching for such a stub. + |Therefore, without necessity, it's better not to use this mechanism.""".stripMargin ) - _ <- describe("Предполагается, что в mockingbird есть сервис `alpha`.") + _ <- describe("It is assumed that in mockingbird there is a service `alpha`.") _ <- describe( - """Скоуп в котором создаются заглушки не важен. В целом скоуп влияет только - |на приоритет заглушек. В данном случае заглушка создается в скоупе `countdown`. - |В отличие от предыдущих примеров, здесь для указания пути для срабатывания - |заглушки используется поле `pathPattern`, вместо `path`. Так же, ответ который - |формирует заглушка не статичный, а зависит от параметров пути.""".stripMargin + """The scope in which stubs are created does not matter. In general, the scope only affects + |the priority of the stubs. In this case, the stub is created in the `countdown` scope. + |Unlike previous examples, here the `pathPattern` field is used to specify the path for triggering + |the stub, instead of `path`. Also, the response that + |the stub generates is not static but depends on the path parameters.""".stripMargin ) resp <- sendHttp( @@ -300,8 +293,8 @@ class BasicHttpStub[HttpResponseR] extends ExampleSet[HttpResponseR] { ) _ <- describe( - """Теперь сделаем несколько запросов, который приведут к срабатыванию этой заглшки, - |чтобы увидеть, что результат действительно зависит от пути.""".stripMargin + """Now let's make several requests that will trigger this stub, + |to see that the result really depends on the path.""".stripMargin ) resp <- sendHttp( method = HttpMethod.Get, diff --git a/backend/examples/src/main/scala/ru/tinkoff/tcb/mockingbird/examples/HttpStubWithState.scala b/backend/examples/src/main/scala/ru/tinkoff/tcb/mockingbird/examples/HttpStubWithState.scala index 023b7bf4..54bc40a0 100644 --- a/backend/examples/src/main/scala/ru/tinkoff/tcb/mockingbird/examples/HttpStubWithState.scala +++ b/backend/examples/src/main/scala/ru/tinkoff/tcb/mockingbird/examples/HttpStubWithState.scala @@ -11,18 +11,18 @@ import ru.tinkoff.tcb.utils.circe.optics.* class HttpStubWithState[HttpResponseR] extends ExampleSet[HttpResponseR] { - val name = "Использование хранимого состояние в HTTP заглушках" + val name = "Utilizing persistent state in HTTP stubs" - example("Создать, получить и обновить хранимое состояние") { + example("Create, retrieve, and update stored state") { for { - _ <- describe("Предполагается, что в mockingbird есть сервис `alpha`.") - _ <- describe("""Для работы с состоянием у HTTP заглушки есть две секции: `persist` и `state`. - |Секция `persist` отвечает за сохранение состояния для последующего доступа к - |нему. А секция `state` содержит предикаты для поиска состояния. Если указана - |только секция `persist`, то каждый раз при срабатывании заглушки в БД будет - |записываться новое состояние. А если указаны обе секции, то найденное состояние - |будет перезаписано. Состояние - это JSON объект.""".stripMargin) - _ <- describe("""В качестве примера, будем хранить как состояние JSON объект вида: + _ <- describe("It is assumed that in mockingbird there is a service `alpha`.") + _ <- describe("""For working with state in the HTTP stub, there are two sections: `persist` and `state`. + |The `persist` section is responsible for saving the state for subsequent access to + |it. The `state` section contains predicates for searching for the state. If only + |the `persist` section is specified, then each time the stub is triggered, a new state will be + |recorded in the database. If both sections are specified, the found state + |will be overwritten. The state is a JSON object.""".stripMargin) + _ <- describe(""" As an example, we will store as the state a JSON object of the form: |```json |{ | "id": "o1", @@ -30,8 +30,8 @@ class HttpStubWithState[HttpResponseR] extends ExampleSet[HttpResponseR] { | "version": 1 |} |``` - |И дополнительно сохранять время создания и модификации.""".stripMargin) - _ <- describe("Для первоначального создания состояния создадим следующую заглушку.") + |And additionally save the creation and modification time.""".stripMargin) + _ <- describe("To initially create the state, we create the following stub.") resp <- sendHttp( method = Post, path = "/api/internal/mockingbird/v2/stub", @@ -84,18 +84,18 @@ class HttpStubWithState[HttpResponseR] extends ExampleSet[HttpResponseR] { ) ) _ <- describe( - """Данная заглушка делает следующее: - | * Проверяет, что тело запроса - это JSON объект содержащий как минимум одно - | поле `id`. - | * В секции `seed` создается переменная `timestamp` в которую записывается - | текущее время. - | * Секция `persist` описывает объект, который будет сохранен как состояние. - | Данные, которые пришли в теле запроса записываются в поле `_data`, в добавок, - | в поле `created` записывает текущее время. - | * В ответе возвращаются полученные данные и временная метка.""".stripMargin + """This stub does the following: + | * Checks that the request body is a JSON object containing at least one + | field `id`. + | * In the `seed` section, a `timestamp` variable is created in which + | the current time is recorded. + | * The `persist` section describes the object that will be saved as the state. + | The data that came in the request body are recorded in the `_data` field, in addition, + | the `created` field records the current time. + | * The response returns the received data and the timestamp.""".stripMargin ) _ <- describe( - """В итоге в Mockingbird состояние будет записано как: + """As a result, in Mockingbird the state will be recorded as: |```json |{ | "_data": { @@ -109,9 +109,9 @@ class HttpStubWithState[HttpResponseR] extends ExampleSet[HttpResponseR] { |""".stripMargin ) _ <- describe( - """Добавим заглушку для модификации состояния, она будет похожей на предыдущую, - |но будет иметь секцию `state` для поиска уже существующего состояния, а в секции - |`persist` будет поле `modified` вместо `created`.""".stripMargin + """We add a stub for modifying the state, similar to the previous one, + |but it has a state section for searching for an existing state, + |and in the `persist` section, it has a `modified` field instead of `created`.""".stripMargin ) resp <- sendHttp( method = Post, @@ -168,30 +168,30 @@ class HttpStubWithState[HttpResponseR] extends ExampleSet[HttpResponseR] { ) ) _ <- describe( - """Для обновления состояния принимаем такие же данные, как и для создания нового. - |В секции `state` поля из тела запроса доступны сразу, без дополнительных, - |поэтому просто пишем, имя поля `${id}`, в отличии от секций `response` - |и `persist`, где доступ к данным запроса осуществляется через переменную `req`. - |В случае, если используются именованные параметры пути в `pathPattern`, - |то доступ к ним из секции `state` осуществляется через переменную `__segments`.""".stripMargin + """To update the state, we accept the same data as for creating a new one. + |In the `state` section, fields from the request body are available immediately, + |without additional, so we just write the field name `${id}`, unlike in the `response` + |and `persist` sections, where access to request data is through the `req` variable. + |If named path parameters are used in `pathPattern`, + |then access to them from the `state` section is through the `__segments` variable.""".stripMargin ) _ <- describe( - """При обновлении состояния, поля перечисленные в секции `persist` дописываются - |к тем, что уже есть в найденном состоянии. В случае если поле уже существует, то - |оно будет перезаписано. Стоит обратить внимание каким образом дописывается - |временная метка `modified`. Она указана как `meta.modified`, такой синтаксис - |позволяет перезаписывать не весь объект, а только его часть или добавлять - |в него новые поля.""".stripMargin + """When updating the state, the fields listed in the `persist` section are appended + |to those already in the found state. In case a field already exists, it + |will be overwritten. Pay attention to how the modified `timestamp` is appended. + |It is indicated as `meta.modified`, this syntax + |allows overwriting not the entire object, but only part of it or adding + |new fields to it.""".stripMargin ) _ <- describe( - """При выборе между двух заглушек, заглушка для которой выполнилось условие поиска - |хранимого состояние, т.е. существует состояние удовлетворяющее критериям - |указанным в секции `state`, имеет больший приоритет, чем заглушка без условий - |выбора состояний. Поэтому первая заглушка будет срабатывать когда в БД ещё нет - |хранимого состояния с указанным `id`, а вторая когда такое состояние уже есть. """.stripMargin - ) - _ <- describe("""Теперь создадим заглушку для получения хранимого состояния. Получать состояние - |будем отправляя POST запрос с JSON содержащим поле `id`: + """When choosing between two stubs, the stub for which the search condition + |for the stored state is met, i.e., there exists a state meeting the criteria + |specified in the `state` section, has a higher priority than a stub without conditions + |for selecting states. Therefore, the first stub will be triggered when there is no + |stored state with the specified `id` in the database, and the second when such a state already exists.""".stripMargin + ) + _ <- describe("""Now we create a stub for retrieving the stored state. We will retrieve the state + |by sending a POST request with JSON containing the `id` field: |```json |{ | "id": "o1" @@ -241,7 +241,7 @@ class HttpStubWithState[HttpResponseR] extends ExampleSet[HttpResponseR] { ).some ) ) - _ <- describe("Теперь попробуем вызвать заглушку, записывающую новое состояние.") + _ <- describe("Now let's try to invoke the stub that writes a new state.") resp <- sendHttp( method = Post, path = "/api/mockingbird/exec/alpha/state1", @@ -270,7 +270,7 @@ class HttpStubWithState[HttpResponseR] extends ExampleSet[HttpResponseR] { ) o1v1 = checked.body.flatMap(b => parser.parse(b).toOption).get o1v1created = (JLens \ "meta" \ "created").getOpt(o1v1).flatMap(_.asString).get - _ <- describe("А теперь получить состояние") + _ <- describe("And now retrieve the state") resp <- sendHttp( method = Post, path = "/api/mockingbird/exec/alpha/state1/get", @@ -293,8 +293,8 @@ class HttpStubWithState[HttpResponseR] extends ExampleSet[HttpResponseR] { ) ) _ <- describe( - """Теперь модифицируем состояние, изменив значение поля `version` и добавив новое - |поле `description`. Поле `name` опустим.""".stripMargin + """Now we modify the state, changing the value of the `version` field + |and adding a new field `description`. We will omit the `name` field.""".stripMargin ) resp <- sendHttp( method = Post, @@ -330,7 +330,7 @@ class HttpStubWithState[HttpResponseR] extends ExampleSet[HttpResponseR] { ) o1v2 = checked.body.flatMap(b => parser.parse(b).toOption).get o1v2modified = (JLens \ "meta" \ "modified").getOpt(o1v2).flatMap(_.asString).get - _ <- describe("И снова запросим состояние объекта `o1`") + _ <- describe("And again, we request the state of object `o1`") resp <- sendHttp( method = Post, path = "/api/mockingbird/exec/alpha/state1/get", @@ -354,15 +354,15 @@ class HttpStubWithState[HttpResponseR] extends ExampleSet[HttpResponseR] { ) ) _ <- describe( - """Ответ изменился, мы видим новые поля. Так как поле `data` перезаписывалось - |целиком, то поле `name` исчезло, в то время как в объекте `meta` - |модифицировалось только поле `modified`, поэтому, хотя поле `created` не указано - |в секции `persist` заглушки обновляющей сосотояние, оно отсталось.""".stripMargin + """The response changed, we see new fields. Since the `data` field was completely overwritten, + |the `name` field disappeared, while in the `meta` object, + |only the modified field was `modified`, so although the `created` field is not mentioned + |in the `persist` section of the stub updating the state, it remained.""".stripMargin ) _ <- describe( - """Если попробовать вызвать заглушку читающую состояние объекта которого нет, - |то Mockingbird вернет ошибку, в котрой будет сказано, что не найдено подходящие - |заглушки.""".stripMargin + """If we try to invoke the stub for reading the state of an object that does not exist, + |Mockingbird will return an error, stating that no suitable + |stubs were found.""".stripMargin ) resp <- sendHttp( method = Post, @@ -374,35 +374,35 @@ class HttpStubWithState[HttpResponseR] extends ExampleSet[HttpResponseR] { HttpResponseExpected( code = CheckInteger(400).some, body = CheckString( - "ru.tinkoff.tcb.mockingbird.error.StubSearchError: Не удалось подобрать заглушку для [Post] /alpha/state1/get" + "ru.tinkoff.tcb.mockingbird.error.StubSearchError: Can't find any stub for [Post] /alpha/state1/get" ).some ) ) _ <- describe( - """Для решения подобной проблемы, надо создать вторую заглушку с таким же `path`, - |но с незаполненным `state`. Тогда, в случае отсутствия искомого состояния, будет - |отрабатывать она. Это аналогично тому как мы создали заглушку для записи нового - |состояния и заглушку для его обновления.""".stripMargin + """To solve such a problem, one should create a second stub with the same `path`, + |but with an empty `state`. Then, in the absence of the searched state, it + |will be triggered. This is similar to how we created a stub for writing a new + |state and a stub for its update.""".stripMargin ) } yield () } - example("Несколько состояний подходящих под условие поиска") { + example("Multiple states matching the search condition") { for { _ <- describe( - """В предыдущем примере было рассмотрено создание и модификация состояния, - |для этого было создано две соответствующие заглушки. Важно помнить, что если - |секция `state` не указана, а указана только секция `persist`, то в БД **всегда** - |создается новый объект состояния. При это заглушка с заполненным полем `state` - |будет выбрана только в том случае, если в результате поиска по заданным - |параметрам из БД вернулся строго один объект с состоянием.""".stripMargin + """In the previous example, creating and modifying a state was discussed, + |for which two corresponding stubs were created. It is important to remember that if + |the `state` section is not specified, and only the `persist` section is, then in the database **always** + |a new state object is created. Meanwhile, a stub with a filled `state` + |field will be selected only in the case that, as a result of the search by specified + |parameters, exactly one state object is returned from the database.""".stripMargin ) _ <- describe( - """**ВНИМАНИЕ!** Функции удаления состояний в Mockingbird нет. Неосторожная работа - |с состояниями может привести к неработоспособности заглушек и придется удалять - |данные напрямую из БД.""".stripMargin + """**ATTENTION!** There is no function to delete states in Mockingbird. Careless work + |with states can lead to the inoperability of stubs, and it will be necessary to delete + |data directly from the database.""".stripMargin ) - _ <- describe("Для демонстрации этого создадим новые заглушки для записи и чтения состояния.") + _ <- describe("To demonstrate this, we will create new stubs for writing and reading a state.") resp <- sendHttp( method = Post, path = "/api/internal/mockingbird/v2/stub", @@ -471,7 +471,7 @@ class HttpStubWithState[HttpResponseR] extends ExampleSet[HttpResponseR] { resp, HttpResponseExpected(code = CheckInteger(200).some) ) - _ <- describe("Вызовем заглушку для записи состояния") + _ <- describe("We call the stub for writing a state") resp <- sendHttp( method = Post, path = "/api/mockingbird/exec/alpha/state2", @@ -488,7 +488,7 @@ class HttpStubWithState[HttpResponseR] extends ExampleSet[HttpResponseR] { body = CheckString("OK").some, ) ) - _ <- describe("Теперь попробуем его получить.") + _ <- describe("Now let's try to retrieve it.") resp <- sendHttp( method = Post, path = "/api/mockingbird/exec/alpha/state2/get", @@ -497,7 +497,7 @@ class HttpStubWithState[HttpResponseR] extends ExampleSet[HttpResponseR] { |}""".stripMargin.some, headers = Seq("Content-Type" -> "application/json"), ) - _ <- describe("Тут всё хорошо и мы получили то, что записали.") + _ <- describe("Here everything is fine, and we got what we wrote.") _ <- checkHttp( resp, HttpResponseExpected( @@ -508,7 +508,7 @@ class HttpStubWithState[HttpResponseR] extends ExampleSet[HttpResponseR] { ).some, ) ) - _ <- describe("Теперь еще раз отправим объект с таким же `bad_id`") + _ <- describe("Now we send the object with the same `bad_id` again") resp <- sendHttp( method = Post, path = "/api/mockingbird/exec/alpha/state2", @@ -525,7 +525,7 @@ class HttpStubWithState[HttpResponseR] extends ExampleSet[HttpResponseR] { body = CheckString("OK").some, ) ) - _ <- describe("И снова попробуем его получить.") + _ <- describe("And try to retrieve it again.") resp <- sendHttp( method = Post, path = "/api/mockingbird/exec/alpha/state2/get", @@ -534,17 +534,17 @@ class HttpStubWithState[HttpResponseR] extends ExampleSet[HttpResponseR] { |}""".stripMargin.some, headers = Seq("Content-Type" -> "application/json"), ) - _ <- describe("А вот тут уже ошибка") + _ <- describe("And here we encounter an error") _ <- checkHttp( resp, HttpResponseExpected( code = CheckInteger(400).some, body = CheckString( - "ru.tinkoff.tcb.mockingbird.error.StubSearchError: Для одной или нескольких заглушек найдено более одного подходящего состояния" + "ru.tinkoff.tcb.mockingbird.error.StubSearchError: For one or more stubs, multiple suitable states were found" ).some, ) ) - _ <- describe("Для проверки состояний подходящих для под заданное условие, можно выполнить следующий запрос.") + _ <- describe("To check for states that fit the given condition, one can perform the following request.") resp <- sendHttp( method = Post, path = "/api/internal/mockingbird/fetchStates", @@ -555,7 +555,7 @@ class HttpStubWithState[HttpResponseR] extends ExampleSet[HttpResponseR] { |}""".stripMargin.some, headers = Seq("Content-Type" -> "application/json"), ) - _ <- describe("В результате будет два объекта") + _ <- describe("As a result, there will be two objects") _ <- checkHttp( resp, HttpResponseExpected( @@ -585,9 +585,9 @@ class HttpStubWithState[HttpResponseR] extends ExampleSet[HttpResponseR] { ) ) _ <- describe( - """Ручка `/api/internal/mockingbird/fetchStates` возвращает состояния в том виде - |как они хранятся в БД, присутствуют поля `id`, `created`, а записанное состояние - |хранится в поле `data`.""".stripMargin + """The `/api/internal/mockingbird/fetchStates` endpoint returns states as + |they are stored in the database, with fields `id`, `created`, and the recorded state + |stored in the `data` field.""".stripMargin ) } yield () } diff --git a/backend/mockingbird-api/src/main/scala/ru/tinkoff/tcb/mockingbird/api/admin/package.scala b/backend/mockingbird-api/src/main/scala/ru/tinkoff/tcb/mockingbird/api/admin/package.scala index e127e0cf..cbbe1af9 100644 --- a/backend/mockingbird-api/src/main/scala/ru/tinkoff/tcb/mockingbird/api/admin/package.scala +++ b/backend/mockingbird-api/src/main/scala/ru/tinkoff/tcb/mockingbird/api/admin/package.scala @@ -49,21 +49,21 @@ package object admin { .in("fetchStates") .in(jsonBody[SearchRequest]) .out(jsonBody[Vector[PersistentState]]) - .summary("Выборка состояний по предикату") + .summary("Fetch states by predicate") val testXPath: Endpoint[Unit, XPathTestRequest, Throwable, String, Any] = basicTest.post .in("testXpath") .in(jsonBody[XPathTestRequest]) .out(stringBody) - .summary("Тестирование работоспособности XPath-выражения") + .summary("Test XPath expression") private val tryStub: PublicEndpoint[ExecInputB, Throwable, SID[ HttpStub ], Any] = basicTest .in("tryStub") - .summary("Проверка резолвинга HTTP заглушки") + .summary("Test HTTP stub resolution") .in(execInput) .in( binaryBody(RawBodyType.ByteArrayBody)[Option[String]] @@ -93,7 +93,7 @@ package object admin { .in("tryScenario") .in(jsonBody[ScenarioResolveRequest]) .out(jsonBody[SID[Scenario]]) - .summary("Проверка резолвинга сценария") + .summary("Test scenario resolution") private val basicV2 = basic.in("v2").tag("setup v2") @@ -102,19 +102,19 @@ package object admin { val fetchServices: Endpoint[Unit, Unit, Throwable, Vector[Service], Any] = serviceBase.get .out(jsonBody[Vector[Service]]) - .summary("Получение списка сервисов") + .summary("Get service list") val createService: Endpoint[Unit, CreateServiceRequest, Throwable, OperationResult[String], Any] = serviceBase.post .in(jsonBody[CreateServiceRequest]) .out(jsonBody[OperationResult[String]]) - .summary("Создание сервиса") + .summary("Create service") val getService: Endpoint[Unit, String, Throwable, Option[Service], Any] = serviceBase.get .in(path[String].name("suffix")) .out(jsonBody[Option[Service]]) - .summary("Получение сервиса по суффиксу") + .summary("Get service by suffix") private val stubBase = basicV2.in("stub") @@ -126,32 +126,32 @@ package object admin { .in(query[Option[String]]("service")) .in(query[List[String]]("labels")) .out(jsonBody[Vector[HttpStub]]) - .summary("Получение списка заглушек") + .summary("Get stub list") val createHttpStub: Endpoint[Unit, CreateStubRequest, Throwable, OperationResult[SID[HttpStub]], Any] = stubBase.post .in(jsonBody[CreateStubRequest]) .out(jsonBody[OperationResult[SID[HttpStub]]]) - .summary("Создание HTTP мока") + .summary("Create HTTP mock") val getStub: Endpoint[Unit, SID[HttpStub], Throwable, Option[HttpStub], Any] = stubBase.get .in(path[SID[HttpStub]].name("id")) .out(jsonBody[Option[HttpStub]]) - .summary("Получение заглушки по id") + .summary("Get stub by id") val updateStub: Endpoint[Unit, (SID[HttpStub], UpdateStubRequest), Throwable, OperationResult[SID[HttpStub]], Any] = stubBase.patch .in(path[SID[HttpStub]].name("id")) .in(jsonBody[UpdateStubRequest]) .out(jsonBody[OperationResult[SID[HttpStub]]]) - .summary("Обновление заглушки по id") + .summary("Update stub by id") val deleteStub: Endpoint[Unit, SID[HttpStub], Throwable, OperationResult[String], Any] = stubBase.delete .in(path[SID[HttpStub]].name("id")) .out(jsonBody[OperationResult[String]]) - .summary("Удаление HTTP мока") + .summary("Delete HTTP mock") private val scenarioBase = basicV2.in("scenario") @@ -163,19 +163,19 @@ package object admin { .in(query[Option[String]]("service")) .in(query[List[String]]("labels")) .out(jsonBody[Vector[Scenario]]) - .summary("Получение списка сценариев") + .summary("Get scenario list") val createScenario: Endpoint[Unit, CreateScenarioRequest, Throwable, OperationResult[SID[Scenario]], Any] = scenarioBase.post .in(jsonBody[CreateScenarioRequest]) .out(jsonBody[OperationResult[SID[Scenario]]]) - .summary("Создание событийного мока") + .summary("Create scenario") val getScenario: Endpoint[Unit, SID[Scenario], Throwable, Option[Scenario], Any] = scenarioBase.get .in(path[SID[Scenario]].name("id")) .out(jsonBody[Option[Scenario]]) - .summary("Получение сценария по id") + .summary("Get scenario by id") val updateScenario : Endpoint[Unit, (SID[Scenario], UpdateScenarioRequest), Throwable, OperationResult[SID[Scenario]], Any] = @@ -183,13 +183,13 @@ package object admin { .in(path[SID[Scenario]].name("id")) .in(jsonBody[UpdateScenarioRequest]) .out(jsonBody[OperationResult[SID[Scenario]]]) - .summary("Обновление сценария по id") + .summary("Update scenario id") val deleteScenario: Endpoint[Unit, SID[Scenario], Throwable, OperationResult[String], Any] = scenarioBase.delete .in(path[SID[Scenario]].name("id")) .out(jsonBody[OperationResult[String]]) - .summary("Удаление сценария") + .summary("Delete scenario") private val labelBase = basicV2.in("label") @@ -232,20 +232,20 @@ package object admin { sourceConfBase.get .in(query[Option[String Refined NonEmpty]]("service")) .out(jsonBody[Vector[SourceDTO]]) - .summary("Получение списка конфигураций источников") + .summary("Get source configurations") val getSourceConfiguration = sourceConfBase.get .in(path[SID[SourceConfiguration]].name("name")) .out(jsonBody[Option[SourceConfiguration]]) - .summary("Получение конфигурации источника по имени") + .summary("Get source by name") val createSourceConf : Endpoint[Unit, CreateSourceConfigurationRequest, Throwable, OperationResult[SID[SourceConfiguration]], Any] = sourceConfBase.post .in(jsonBody[CreateSourceConfigurationRequest]) .out(jsonBody[OperationResult[SID[SourceConfiguration]]]) - .summary("Создание конфигурации источника") + .summary("Create source") val updateSourceConf : Endpoint[Unit, (SID[SourceConfiguration], UpdateSourceConfigurationRequest), Throwable, OperationResult[ @@ -255,13 +255,13 @@ package object admin { .in(path[SID[SourceConfiguration]].name("name")) .in(jsonBody[UpdateSourceConfigurationRequest]) .out(jsonBody[OperationResult[SID[SourceConfiguration]]]) - .summary("Обновление конфигурации по name") + .summary("Update source by name name") val deleteSourceConf: Endpoint[Unit, SID[SourceConfiguration], Throwable, OperationResult[String], Any] = sourceConfBase.delete .in(path[SID[SourceConfiguration]].name("name")) .out(jsonBody[OperationResult[String]]) - .summary("Удаление конфигурации") + .summary("Delete source") private val destinationConfBase = basicV3.in("destination") @@ -270,13 +270,13 @@ package object admin { destinationConfBase.get .in(query[Option[String Refined NonEmpty]]("service")) .out(jsonBody[Vector[DestinationDTO]]) - .summary("Получение списка конфигураций назначений") + .summary("Get destinations list") val getDestinationConfiguration = destinationConfBase.get .in(path[SID[DestinationConfiguration]].name("name")) .out(jsonBody[Option[DestinationConfiguration]]) - .summary("Получение конфигурации назначения по имени") + .summary("Get destination by name") val createDestinationConf: Endpoint[Unit, CreateDestinationConfigurationRequest, Throwable, OperationResult[ SID[DestinationConfiguration] @@ -284,7 +284,7 @@ package object admin { destinationConfBase.post .in(jsonBody[CreateDestinationConfigurationRequest]) .out(jsonBody[OperationResult[SID[DestinationConfiguration]]]) - .summary("Создание конфигурации источника") + .summary("Create destination") val updateDestinationConf: Endpoint[ Unit, @@ -299,5 +299,5 @@ package object admin { .in(path[SID[DestinationConfiguration]].name("name")) .in(jsonBody[UpdateDestinationConfigurationRequest]) .out(jsonBody[OperationResult[SID[DestinationConfiguration]]]) - .summary("Обновление конфигурации по name") + .summary("Update destination by name") } diff --git a/backend/mockingbird-api/src/main/scala/ru/tinkoff/tcb/mockingbird/codec/package.scala b/backend/mockingbird-api/src/main/scala/ru/tinkoff/tcb/mockingbird/codec/package.scala index e48d95ea..ec70dc1c 100644 --- a/backend/mockingbird-api/src/main/scala/ru/tinkoff/tcb/mockingbird/codec/package.scala +++ b/backend/mockingbird-api/src/main/scala/ru/tinkoff/tcb/mockingbird/codec/package.scala @@ -38,7 +38,7 @@ package object codec { case BinaryResponse(_, _, body, _) => body.asArray case EmptyResponse(_, _, _) => Array.empty /** - * все *ProxyResponse преобразуются в RawResponse внутри [[PublicApiHandler]] (методы *proxyRequest) + * all *ProxyResponse are converted into RawResponse inside [[PublicApiHandler]] (see *proxyRequest methods) */ case ProxyResponse(_, _, _) => throw new UnsupportedOperationException() case JsonProxyResponse(_, _, _, _) => throw new UnsupportedOperationException() diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/AdminApiHandler.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/AdminApiHandler.scala index 4f86a040..18dbc013 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/AdminApiHandler.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/AdminApiHandler.scala @@ -77,7 +77,7 @@ final class AdminApiHandler( _ <- ZIO.when(service1.isEmpty && service2.isEmpty)( ZIO.fail( ValidationError( - Vector(s"Не удалось подобрать сервис для ${body.path.orElse(body.pathPattern.map(_.regex)).getOrElse("")}") + Vector(s"Can't find service for ${body.path.orElse(body.pathPattern.map(_.regex)).getOrElse("")}") ) ) ) @@ -95,7 +95,10 @@ final class AdminApiHandler( candidates2 = candidates1.filter(_.state == body.state) _ <- ZIO.when(candidates2.nonEmpty)( ZIO.fail( - DuplicationError("Существует заглушка(-ки), полностью совпадающая по условиям и типу", candidates2.map(_.id)) + DuplicationError( + "There exists a stub or stubs that match completely in terms of conditions and type", + candidates2.map(_.id) + ) ) ) now <- ZIO.clockWith(_.instant) @@ -119,7 +122,7 @@ final class AdminApiHandler( _ <- ZIO.when(service.isEmpty)( ZIO.fail( ValidationError( - Vector(s"Сервис ${body.service} не существует") + Vector(s"Service ${body.service} does not exist") ) ) ) @@ -135,7 +138,7 @@ final class AdminApiHandler( _ <- ZIO.when(candidates2.nonEmpty)( ZIO.fail( DuplicationError( - "Существует сценарий(и), полностью совпадающий по источнику, условиям и типу", + "There exist scenario(s) that match completely in terms of source, conditions, and type", candidates2.map(_.id) ) ) @@ -162,7 +165,7 @@ final class AdminApiHandler( _ <- ZIO.when(candidates.nonEmpty)( ZIO.fail( DuplicationError( - s"Существует сервис(ы), имеющий суффикс ${body.suffix.value}", + s"There exist service(s) that have a suffix ${body.suffix.value}", candidates.map(_.name) ) ) @@ -198,7 +201,7 @@ final class AdminApiHandler( _ <- Tracing.update(_.addToPayload("path" -> path, "method" -> method.entryName)) (stub, _) <- f(Scope.Countdown) .filterOrElse(_.isDefined)(f(Scope.Ephemeral).filterOrElse(_.isDefined)(f(Scope.Persistent))) - .someOrFail(StubSearchError(s"Не удалось подобрать заглушку для [$method] $path")) + .someOrFail(StubSearchError(s"Cant find a stub for [$method] $path")) } yield stub.id } @@ -208,7 +211,7 @@ final class AdminApiHandler( for { (scenario, _) <- f(Scope.Countdown) .filterOrElse(_.isDefined)(f(Scope.Ephemeral).filterOrElse(_.isDefined)(f(Scope.Persistent))) - .someOrFail(ScenarioSearchError(s"Не удалось подобрать сценарий для сообщения из ${body.source}")) + .someOrFail(ScenarioSearchError(s"Can't find any scenario for the message from ${body.source}")) } yield scenario.id } @@ -298,7 +301,7 @@ final class AdminApiHandler( _ <- ZIO.when(service1.isEmpty && service2.isEmpty)( ZIO.fail( ValidationError( - Vector(s"Не удалось подобрать сервис для ${body.path.orElse(body.pathPattern.map(_.regex)).getOrElse("")}") + Vector(s"Can't find a service for ${body.path.orElse(body.pathPattern.map(_.regex)).getOrElse("")}") ) ) ) @@ -317,7 +320,10 @@ final class AdminApiHandler( candidates2 = candidates1.filter(_.state == body.state) _ <- ZIO.when(candidates2.nonEmpty)( ZIO.fail( - DuplicationError("Существует заглушка(-ки), полностью совпадающая по условиям и типу", candidates2.map(_.id)) + DuplicationError( + "There exists a stub or stubs that match completely in terms of conditions and type", + candidates2.map(_.id) + ) ) ) now <- ZIO.clockWith(_.instant) @@ -344,7 +350,7 @@ final class AdminApiHandler( _ <- ZIO.when(service.isEmpty)( ZIO.fail( ValidationError( - Vector(s"Сервис ${body.service} не существует") + Vector(s"Service ${body.service} does not exist") ) ) ) @@ -361,7 +367,7 @@ final class AdminApiHandler( _ <- ZIO.when(candidates2.nonEmpty)( ZIO.fail( DuplicationError( - "Существует сценарий(и), полностью совпадающий по источнику, условиям и типу", + "There exist scenario(s) that match completely in terms of source, conditions, and type", candidates2.map(_.id) ) ) @@ -422,7 +428,7 @@ final class AdminApiHandler( _ <- ZIO.when(service.isEmpty)( ZIO.fail( ValidationError( - Vector(s"Сервис ${body.service} не существует") + Vector(s"Service ${body.service} does not exist") ) ) ) @@ -443,7 +449,10 @@ final class AdminApiHandler( .filter(_.state == body.state) _ <- ZIO.when(candidates.nonEmpty)( ZIO.fail( - DuplicationError("Существует заглушка(-ки), полностью совпадающая по условиям и типу", candidates.map(_.id)) + DuplicationError( + "There exists a stub or stubs that match completely in terms of conditions and type", + candidates.map(_.id) + ) ) ) responseSchema <- protobufSchemaResolver.parseDefinitionFrom(responseSchemaBytes) @@ -494,7 +503,7 @@ final class AdminApiHandler( _ <- ZIO.when(service.isEmpty)( ZIO.fail( ValidationError( - Vector(s"Сервис ${body.service} не существует") + Vector(s"Service ${body.service} does not exist") ) ) ) @@ -502,7 +511,7 @@ final class AdminApiHandler( _ <- ZIO.when(candidate.nonEmpty)( ZIO.fail( DuplicationError( - "Существует конфигурация, полностью совпадающая по имени", + "There exists a configuration that matches completely by name", candidate.map(_.name).toVector ) ) @@ -519,7 +528,7 @@ final class AdminApiHandler( ZIO.fail(ex) } .catchSome { case NonFatal(ex) => - log.errorCause("Ошибка при инициализации", ex) + log.errorCause("Initialization error", ex) } .forkDaemon } yield if (res > 0) OperationResult("success", sourceConf.name) else OperationResult("nothing inserted") @@ -533,7 +542,7 @@ final class AdminApiHandler( _ <- ZIO.when(service.isEmpty)( ZIO.fail( ValidationError( - Vector(s"Сервис ${body.service} не существует") + Vector(s"Service ${body.service} does not exist") ) ) ) @@ -550,14 +559,14 @@ final class AdminApiHandler( ZIO.fail(ex) } .catchSome { case NonFatal(ex) => - log.errorCause("Ошибка при деинициализации", ex) + log.errorCause("Error during deinitialization", ex) } *> ZIO .foreachDiscard(confPatch.init.map(_.toVector).getOrElse(Vector.empty))(rm.execute) .catchSomeDefect { case NonFatal(ex) => ZIO.fail(ex) } .catchSome { case NonFatal(ex) => - log.errorCause("Ошибка при инициализации", ex) + log.errorCause("Initialization error", ex) }).forkDaemon } yield if (res.successful) OperationResult("success", confPatch.name) else OperationResult("nothing changed") @@ -571,7 +580,7 @@ final class AdminApiHandler( _ <- ZIO.when(scenarios.nonEmpty)( ZIO.fail( ValidationError( - Vector(s"Сценарии ${scenarios.mkString(",")} используют источник ${name}") + Vector(s"Scenarios ${scenarios.mkString(",")} are using source ${name}") ) ) ) @@ -579,18 +588,18 @@ final class AdminApiHandler( _ <- ZIO.when(source.isEmpty)( ZIO.fail( ValidationError( - Vector(s"Конфигурация $name не существует") + Vector(s"Configuration of name $name does not exist") ) ) ) - _ <- sourceDAO.deleteById(name) // TODO: удалять в транзакции + _ <- sourceDAO.deleteById(name) // TODO: delete in transaction _ <- ZIO .foreachDiscard(source.get.shutdown.map(_.toVector).getOrElse(Vector.empty))(rm.execute) .catchSomeDefect { case NonFatal(ex) => ZIO.fail(ex) } .catchSome { case NonFatal(ex) => - log.errorCause("Ошибка при деинициализации", ex) + log.errorCause("Error during deinitialization", ex) } .forkDaemon } yield OperationResult("success", None) @@ -621,7 +630,7 @@ final class AdminApiHandler( _ <- ZIO.when(service.isEmpty)( ZIO.fail( ValidationError( - Vector(s"Сервис ${body.service} не существует") + Vector(s"Service ${body.service} does not exist") ) ) ) @@ -629,7 +638,7 @@ final class AdminApiHandler( _ <- ZIO.when(candidate.nonEmpty)( ZIO.fail( DuplicationError( - "Существует конфигурация, полностью совпадающая по имени", + "There exists a configuration that matches completely by name", candidate.map(_.name).toVector ) ) @@ -646,7 +655,7 @@ final class AdminApiHandler( ZIO.fail(ex) } .catchSome { case NonFatal(ex) => - log.errorCause("Ошибка при инициализации", ex) + log.errorCause("Initialization error", ex) } .forkDaemon } yield if (res > 0) OperationResult("success", destinationConf.name) else OperationResult("nothing inserted") @@ -660,7 +669,7 @@ final class AdminApiHandler( _ <- ZIO.when(service.isEmpty)( ZIO.fail( ValidationError( - Vector(s"Сервис ${body.service} не существует") + Vector(s"Service ${body.service} does not exist") ) ) ) @@ -677,14 +686,14 @@ final class AdminApiHandler( ZIO.fail(ex) } .catchSome { case NonFatal(ex) => - log.errorCause("Ошибка при деинициализации", ex) + log.errorCause("Error during deinitialization", ex) } *> ZIO .foreachDiscard(confPatch.init.map(_.toVector).getOrElse(Vector.empty))(rm.execute) .catchSomeDefect { case NonFatal(ex) => ZIO.fail(ex) } .catchSome { case NonFatal(ex) => - log.errorCause("Ошибка при инициализации", ex) + log.errorCause("Initialization error", ex) }).forkDaemon } yield if (res.successful) OperationResult("success", confPatch.name) else OperationResult("nothing changed") } diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/PublicApiHandler.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/PublicApiHandler.scala index 1c619c98..a1feb3e2 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/PublicApiHandler.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/PublicApiHandler.scala @@ -80,7 +80,7 @@ final class PublicApiHandler( _ <- Tracing.update(_.addToPayload("path" -> path, "method" -> method.entryName)) (stub, stateOp) <- f(Scope.Countdown) .filterOrElse(_.isDefined)(f(Scope.Ephemeral).filterOrElse(_.isDefined)(f(Scope.Persistent))) - .someOrFail(StubSearchError(s"Не удалось подобрать заглушку для [$method] $path")) + .someOrFail(StubSearchError(s"Can't find any stub for [$method] $path")) _ <- Tracing.update(_.addToPayload("name" -> stub.name)) seed = stub.seed.map(_.eval) srb = SimpleRequestBody.subset.getOption(body).map(_.value) @@ -148,7 +148,7 @@ final class PublicApiHandler( ZIO.fail(ex) } .catchSome { case NonFatal(ex) => - log.errorCause("Ошибка при выполнении колбэка", ex) + log.errorCause("Error during callback execution", ex) } .forkDaemon ) diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/StubResolver.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/StubResolver.scala index d8edaf87..9b63c892 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/StubResolver.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/StubResolver.scala @@ -50,7 +50,7 @@ final class StubResolver( ): RIO[WLD, Option[(HttpStub, Option[PersistentState])]] = ( for { - _ <- log.info("Поиск заглушек для запроса {} типа {}", path, scope) + _ <- log.info("Searching stubs for request to {} of type {}", path, scope) pathPatternExpr = Expression[HttpStub]( None, "$expr" -> BsonDocument( @@ -71,29 +71,32 @@ final class StubResolver( ) candidates0 <- stubDAO.findChunk(condition, 0, Int.MaxValue) _ <- ZIO.when(candidates0.isEmpty)( - log.info("Не найдены обработчики для запроса {} типа {}", path, scope) *> ZIO.fail(EarlyReturn) + log.info("Can't find any handler for {} of type {}", path, scope) *> ZIO.fail(EarlyReturn) ) - _ <- log.info("Кандидаты: {}", candidates0.map(_.id)) + _ <- log.info("Candidates are: {}", candidates0.map(_.id)) candidates1 = candidates0.filter(_.request.checkQueryParams(queryObject)) _ <- ZIO.when(candidates1.isEmpty)( log.warn( - "После проверки query параметров не осталось кандидатов, проверьте параметры: {}", + "After checking query parameters, there are no candidates left. Please verify the parameters: {}", queryObject.noSpaces ) *> ZIO.fail(EarlyReturn) ) - _ <- log.info("После проверки query параметров: {}", candidates1.map(_.id)) + _ <- log.info("After query parameters check: {}", candidates1.map(_.id)) candidates2 = candidates1.filter(_.request.checkHeaders(headers)) _ <- ZIO.when(candidates2.isEmpty)( - log.warn("После проверки заголовков не осталось кандидатов, проверьте заголовки: {}", headers) *> + log.warn("After checking headers, there are no candidates left. Please verify the headers: {}", headers) *> ZIO.fail(EarlyReturn) ) - _ <- log.info("После проверки заголовков: {}", candidates2.map(_.id)) + _ <- log.info("After headers check: {}", candidates2.map(_.id)) candidates3 = candidates2.filter(_.request.checkBody(body)) _ <- ZIO.when(candidates3.isEmpty)( - log.warn("После проверки тела запроса не осталось кандидатов, проверьте тело запроса: {}", body) *> + log.warn( + "After checking the request body, there are no candidates left. Please verify the request body: {}", + body + ) *> ZIO.fail(EarlyReturn) ) - _ <- log.info("После валидации тела: {}", candidates3.map(_.id)) + _ <- log.info("After body check: {}", candidates3.map(_.id)) candidates4 <- candidates3.traverse { stubc => val bodyJson = stubc.request.extractJson(body) val bodyXml = stubc.request.extractXML(body) @@ -116,20 +119,20 @@ final class StubResolver( ) } _ <- ZIO.when(candidates4.exists(_._2.size > 1))( - log.error("Для одной или нескольких заглушек найдено более одного подходящего состояния") *> - ZIO.fail(StubSearchError("Для одной или нескольких заглушек найдено более одного подходящего состояния")) + log.error("For one or more stubs, multiple suitable states were found") *> + ZIO.fail(StubSearchError("For one or more stubs, multiple suitable states were found")) ) _ <- ZIO.when(candidates4.count(_._2.nonEmpty) > 1)( - log.error("Для более чем одной заглушки нашлось подходящее состояние") *> - ZIO.fail(StubSearchError("Для более чем одной заглушки нашлось подходящее состояние")) + log.error("For more than one stub, suitable states were found") *> + ZIO.fail(StubSearchError("For more than one stub, suitable states were found")) ) _ <- ZIO.when(candidates4.size > 1 && candidates4.forall(c => c._1.state.isDefined && c._2.isEmpty))( - log.error("Ни для одной заглушки не найдено подходящего состояния") *> - ZIO.fail(StubSearchError("Ни для одной заглушки не найдено подходящего состояния")) + log.error("No suitable state found for any stub") *> + ZIO.fail(StubSearchError("No suitable state found for any stub")) ) _ <- ZIO.when(candidates4.size > 1 && candidates4.forall(_._1.state.isEmpty))( - log.error("Найдено более одной не требующей состояния заглушки") *> - ZIO.fail(StubSearchError("Найдено более одной не требующей состояния заглушки")) + log.error("More than one stateless stub found") *> + ZIO.fail(StubSearchError("More than one stateless stub found")) ) res = candidates4.find(_._2.size == 1) orElse candidates4.find(_._1.state.isEmpty) } yield res.map { case (stub, states) => stub -> states.headOption } @@ -149,11 +152,11 @@ final class StubResolver( private def findStates(id: SID[?], spec: StateSpec): RIO[WLD, Vector[PersistentState]] = for { - _ <- log.info("Поиск state для {} по условию {}", id, spec.renderJson.noSpaces) + _ <- log.info("Searching state for {} by condition {}", id, spec.renderJson.noSpaces) states <- stateDAO.findBySpec(spec) _ <- - if (states.nonEmpty) log.info("Найдены состояния для {}: {}", id, states.map(_.id)) - else log.info("Не найдено подходящих состояний для {}", id) + if (states.nonEmpty) log.info("Found states for {}: {}", id, states.map(_.id)) + else log.info("Unable to find suitable states for {}", id) } yield states } diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/CreateDestinationConfigurationRequest.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/CreateDestinationConfigurationRequest.scala index 3b8e6a9f..5babc310 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/CreateDestinationConfigurationRequest.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/CreateDestinationConfigurationRequest.scala @@ -16,16 +16,16 @@ import ru.tinkoff.tcb.utils.id.SID @derive(decoder, encoder, schema) final case class CreateDestinationConfigurationRequest( - @description("Уникальное название конфигурации") + @description("Unique configuration name") name: SID[DestinationConfiguration], - @description("Описание конфигурации") + @description("Configuration description") description: String, service: String, - @description("Спецификация запроса") + @description("Request specification") request: EventDestinationRequest, - @description("Спецификация инициализатора") + @description("Initializer specification") init: Option[NonEmptyVector[ResourceRequest]], - @description("Спецификация деинициализатора") + @description("Finalizer specification") shutdown: Option[NonEmptyVector[ResourceRequest]] ) diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/CreateScenarioRequest.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/CreateScenarioRequest.scala index 89121820..d6a2058b 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/CreateScenarioRequest.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/CreateScenarioRequest.scala @@ -29,29 +29,29 @@ import ru.tinkoff.tcb.utils.id.SID @derive(decoder, encoder, schema) final case class CreateScenarioRequest( - @description("Тип конфигурации") + @description("Scope") scope: Scope, - @description("Количество возможных срабатываний. Имеет смысл только для scope=countdown") + @description("The number of possible triggers. Only relevant for scope=countdown") times: Option[Int Refined NonNegative] = Some(refineMV(1)), service: String Refined NonEmpty, - @description("Имя сценария, отображается в логах, полезно для отладки") + @description("Scenario name (shown in logs, handy for debugging)") name: String Refined NonEmpty, - @description("Имя источника событий") + @description("Event source name") source: SID[SourceConfiguration], seed: Option[Json], - @description("Спецификация события") + @description("Event specification") input: ScenarioInput, - @description("Предикат для поиска состояния") + @description("State search predicate") state: Option[Map[JsonOptic, Map[Keyword.Json, Json]]], - @description("Данные, записываемые в базу") + @description("Persisted data") persist: Option[Map[JsonOptic, Json]], - @description("Имя назначения ответа") + @description("Destination name") destination: Option[SID[DestinationConfiguration]], - @description("Спецификация ответа") + @description("Response specification") output: Option[ScenarioOutput], - @description("Спецификация колбека") + @description("Callback specification") callback: Option[Callback], - @description("Тэги") + @description("Tags") labels: Seq[String] = Seq.empty ) object CreateScenarioRequest { diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/CreateSourceConfigurationRequest.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/CreateSourceConfigurationRequest.scala index 26ef473c..1421dcd7 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/CreateSourceConfigurationRequest.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/CreateSourceConfigurationRequest.scala @@ -17,18 +17,18 @@ import ru.tinkoff.tcb.utils.id.SID @derive(decoder, encoder, schema) final case class CreateSourceConfigurationRequest( - @description("Уникальное название конфигурации") + @description("Unique configuration name") name: SID[SourceConfiguration], - @description("Описание конфигурации") + @description("Configuration description") description: String, service: String, - @description("Спецификация запроса") + @description("Request specification") request: EventSourceRequest, - @description("Спецификация инициализатора") + @description("Initializer specification") init: Option[NonEmptyVector[ResourceRequest]], - @description("Спецификация деинициализатора") + @description("Finalizer specification") shutdown: Option[NonEmptyVector[ResourceRequest]], - @description("Спецификации триггеров реинициализации") + @description("Reinitialization triggers specification") reInitTriggers: Option[NonEmptyVector[ResponseSpec]] ) diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/CreateStubRequest.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/CreateStubRequest.scala index 6fff036d..4280ca0c 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/CreateStubRequest.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/CreateStubRequest.scala @@ -29,29 +29,29 @@ import ru.tinkoff.tcb.utils.circe.optics.JsonOptic @derive(encoder, decoder, schema) final case class CreateStubRequest( - @description("Тип конфигурации") + @description("Scope") scope: Scope, - @description("Количество возможных срабатываний. Имеет смысл только для scope=countdown") + @description("The number of possible triggers. Only relevant for scope=countdown") times: Option[Int Refined NonNegative] = Some(refineMV(1)), - @description("Название мока") + @description("Mock name") name: String Refined NonEmpty, - @description("HTTP метод") + @description("HTTP method") method: HttpMethod, - @description("Суффикс пути, по которому срабатывает мок") + @description("The path suffix where the mock triggers") path: Option[String Refined NonEmpty], pathPattern: Option[Regex], seed: Option[Json], - @description("Предикат для поиска состояния") + @description("State search predicate") state: Option[Map[JsonOptic, Map[Keyword.Json, Json]]], - @description("Спецификация запроса") + @description("Request specification") request: HttpStubRequest, - @description("Данные, записываемые в базу") + @description("Persisted data") persist: Option[Map[JsonOptic, Json]], - @description("Спецификация ответа") + @description("Response specification") response: HttpStubResponse, - @description("Спецификация колбека") + @description("Callback specification") callback: Option[Callback], - @description("Тэги") + @description("Tags") labels: Seq[String] = Seq.empty ) object CreateStubRequest { diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/UpdateDestinationConfigurationRequest.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/UpdateDestinationConfigurationRequest.scala index 380394cd..d111ca05 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/UpdateDestinationConfigurationRequest.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/UpdateDestinationConfigurationRequest.scala @@ -15,14 +15,14 @@ import ru.tinkoff.tcb.protocol.schema.* @derive(decoder, encoder, schema) final case class UpdateDestinationConfigurationRequest( - @description("Описание конфигурации") + @description("Configuration description") description: String, service: String, - @description("Спецификация запроса") + @description("Request specification") request: EventDestinationRequest, - @description("Спецификация инициализатора") + @description("Initializer specification") init: Option[NonEmptyVector[ResourceRequest]], - @description("Спецификация деинициализатора") + @description("Finalizer specification") shutdown: Option[NonEmptyVector[ResourceRequest]], ) diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/UpdateScenarioRequest.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/UpdateScenarioRequest.scala index c80ba9c6..363f8fa7 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/UpdateScenarioRequest.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/UpdateScenarioRequest.scala @@ -33,29 +33,29 @@ import ru.tinkoff.tcb.utils.id.SID @derive(decoder, encoder, schema) final case class UpdateScenarioRequest( - @description("Тип конфигурации") + @description("Scope") scope: Scope, - @description("Количество возможных срабатываний. Имеет смысл только для scope=countdown") + @description("The number of possible triggers. Only relevant for scope=countdown") times: Option[Int Refined NonNegative] = Some(refineMV(1)), service: String Refined NonEmpty, - @description("Имя сценария, отображается в логах, полезно для отладки") + @description("Scenario name (shown in logs, handy for debugging)") name: String Refined NonEmpty, - @description("Имя источника событий") + @description("Event source name") source: SID[SourceConfiguration], seed: Option[Json], - @description("Спецификация события") + @description("Event specification") input: ScenarioInput, - @description("Предикат для поиска состояния") + @description("State search predicate") state: Option[Map[JsonOptic, Map[Keyword.Json, Json]]], - @description("Данные, записываемые в базу") + @description("Persisted data") persist: Option[Map[JsonOptic, Json]], - @description("Имя назначения ответа") + @description("Destination name") destination: Option[SID[DestinationConfiguration]], - @description("Спецификация ответа") + @description("Response specification") output: Option[ScenarioOutput], - @description("Спецификация колбека") + @description("Callback specification") callback: Option[Callback], - @description("Тэги") + @description("Tags") labels: Seq[String] ) object UpdateScenarioRequest { diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/UpdateSourceConfigurationRequest.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/UpdateSourceConfigurationRequest.scala index 349a89bb..e3812325 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/UpdateSourceConfigurationRequest.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/UpdateSourceConfigurationRequest.scala @@ -16,16 +16,16 @@ import ru.tinkoff.tcb.protocol.schema.* @derive(decoder, encoder, schema) final case class UpdateSourceConfigurationRequest( - @description("Описание конфигурации") + @description("Configuration description") description: String, service: String, - @description("Спецификация запроса") + @description("Request specification") request: EventSourceRequest, - @description("Спецификация инициализатора") + @description("Initializer specification") init: Option[NonEmptyVector[ResourceRequest]], - @description("Спецификация деинициализатора") + @description("Finalizer specification") shutdown: Option[NonEmptyVector[ResourceRequest]], - @description("Спецификации триггеров реинициализации") + @description("Reinitialization triggers specification") reInitTriggers: Option[NonEmptyVector[ResponseSpec]] ) diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/UpdateStubRequest.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/UpdateStubRequest.scala index da8639ad..ffa5c3dc 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/UpdateStubRequest.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/UpdateStubRequest.scala @@ -34,29 +34,29 @@ import ru.tinkoff.tcb.utils.id.SID @derive(decoder, encoder, schema) final case class UpdateStubRequest( - @description("Тип конфигурации") + @description("Scope") scope: Scope, - @description("Количество возможных срабатываний. Имеет смысл только для scope=countdown") + @description("The number of possible triggers. Only relevant for scope=countdown") times: Option[Int Refined NonNegative] = Some(refineMV(1)), - @description("Название мока") + @description("Mock name") name: String Refined NonEmpty, - @description("HTTP метод") + @description("HTTP method") method: HttpMethod, - @description("Суффикс пути, по которому срабатывает мок") + @description("The path suffix where the mock triggers") path: Option[String Refined NonEmpty], pathPattern: Option[Regex], seed: Option[Json], - @description("Предикат для поиска состояния") + @description("State search predicate") state: Option[Map[JsonOptic, Map[Keyword.Json, Json]]], - @description("Спецификация запроса") + @description("Request specification") request: HttpStubRequest, - @description("Данные, записываемые в базу") + @description("Persisted data") persist: Option[Map[JsonOptic, Json]], - @description("Спецификация ответа") + @description("Response specification") response: HttpStubResponse, - @description("Спецификация колбека") + @description("Callback specification") callback: Option[Callback], - @description("Тэги") + @description("Tags") labels: Seq[String] ) object UpdateStubRequest { diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/error/EarlyReturn.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/error/EarlyReturn.scala index 36d5ae6e..69980163 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/error/EarlyReturn.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/error/EarlyReturn.scala @@ -1,4 +1,4 @@ package ru.tinkoff.tcb.mockingbird.error -//TODO: заменить все использования на Selective функторы +//TODO: replace all occurrences with selective functors case object EarlyReturn extends Exception diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/error/ValidationError.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/error/ValidationError.scala index c76fe9c4..1a5f57dc 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/error/ValidationError.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/error/ValidationError.scala @@ -1,3 +1,3 @@ package ru.tinkoff.tcb.mockingbird.error -final case class ValidationError(fails: Vector[String]) extends Exception(s"Ошибки валидации: ${fails.mkString(",")}") +final case class ValidationError(fails: Vector[String]) extends Exception(s"Validation error: ${fails.mkString(",")}") diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/grpc/GrpcRequestHandler.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/grpc/GrpcRequestHandler.scala index 5767bc14..1b3faaab 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/grpc/GrpcRequestHandler.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/grpc/GrpcRequestHandler.scala @@ -42,7 +42,7 @@ class GrpcRequestHandlerImpl( _ <- Tracing.update(_.addToPayload("service" -> grpcServiceName)) (stub, req, stateOp) <- f(Scope.Countdown) .filterOrElse(_.isDefined)(f(Scope.Ephemeral).filterOrElse(_.isDefined)(f(Scope.Persistent))) - .someOrFail(StubSearchError(s"Не удалось подобрать заглушку для $grpcServiceName")) + .someOrFail(StubSearchError(s"Can't find any stub for $grpcServiceName")) _ <- Tracing.update(_.addToPayload("name" -> stub.name)) seed = stub.seed.map(_.eval) state <- ZIO.fromOption(stateOp).orElse(PersistentState.fresh) diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/grpc/GrpcStubresolver.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/grpc/GrpcStubresolver.scala index 79444134..31d72498 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/grpc/GrpcStubresolver.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/grpc/GrpcStubresolver.scala @@ -73,20 +73,20 @@ class GrpcStubResolverImpl( ) } _ <- ZIO.when(candidates.exists(_._2.size > 1))( - log.error("Для одной или нескольких заглушек найдено более одного подходящего состояния") *> - ZIO.fail(StubSearchError("Для одной или нескольких заглушек найдено более одного подходящего состояния")) + log.error("For one or more stubs, multiple suitable states were found") *> + ZIO.fail(StubSearchError("For one or more stubs, multiple suitable states were found")) ) _ <- ZIO.when(candidates.count(_._2.nonEmpty) > 1)( - log.error("Для более чем одной заглушки нашлось подходящее состояние") *> - ZIO.fail(StubSearchError("Для более чем одной заглушки нашлось подходящее состояние")) + log.error("For more than one stub, suitable states were found") *> + ZIO.fail(StubSearchError("For more than one stub, suitable states were found")) ) _ <- ZIO.when(candidates.size > 1 && candidates.forall(c => c._1.state.isDefined && c._2.isEmpty))( - log.error("Ни для одной заглушки не найдено подходящего состояния") *> - ZIO.fail(StubSearchError("Ни для одной заглушки не найдено подходящего состояния")) + log.error("No suitable state found for any stub") *> + ZIO.fail(StubSearchError("No suitable state found for any stub")) ) _ <- ZIO.when(candidates.size > 1 && candidates.forall(_._1.state.isEmpty))( - log.error("Найдено более одной не требующей состояния заглушки") *> - ZIO.fail(StubSearchError("Найдено более одной не требующей состояния заглушки")) + log.error("More than one stateless stub found") *> + ZIO.fail(StubSearchError("More than one stateless stub found")) ) res = candidates.find(_._2.size == 1) orElse candidates.find(_._1.state.isEmpty) } yield res.map { case (stub, states) => @@ -100,7 +100,7 @@ class GrpcStubResolverImpl( ) .catchSome { case e @ (_: InvalidProtocolBufferException | ParsingFailure(_, _)) => log.infoCause( - "Ошибка разбора gRPC запроса {} для заглушки {}", + "Failed to parse gRPC request {} for stub {}", e, stub.requestClass, stub.id @@ -109,7 +109,7 @@ class GrpcStubResolverImpl( } .tapError(e => log.errorCause( - "Ошибка разбора gRPC запроса {} для заглушки {}", + "Failed to parse gRPC request {} for stub {}", e, stub.requestClass, stub.id @@ -119,11 +119,11 @@ class GrpcStubResolverImpl( private def findStates(id: SID[GrpcStub], spec: StateSpec): RIO[WLD, Vector[PersistentState]] = for { - _ <- log.info("Поиск state для {} по условию {}", id, spec.renderJson.noSpaces) + _ <- log.info("Searching for state for {} based on condition {}", id, spec.renderJson.noSpaces) states <- stateDAO.findBySpec(spec) _ <- - if (states.nonEmpty) log.info("Найдены состояния для {}: {}", id, states.map(_.id)) - else log.info("Не найдено подходящих состояний для {}", id) + if (states.nonEmpty) log.info("States found for {}: {}", id, states.map(_.id)) + else log.info("No suitable states found for {}", id) } yield states } diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/grpc/Method.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/grpc/Method.scala index 50fbc39d..63447745 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/grpc/Method.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/grpc/Method.scala @@ -9,7 +9,7 @@ import io.grpc.MethodDescriptor.Marshaller object Method { /* - Универсальный маршаллер, который не меняет поток байтов + Universal marshaller that does not alter byte stream */ final case class ByteMarshaller() extends Marshaller[Array[Byte]] { override def stream(value: Array[Byte]): InputStream = new ByteArrayInputStream(value) diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/misc/Substitute.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/misc/Substitute.scala index 3c5c63cb..cb159a34 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/misc/Substitute.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/misc/Substitute.scala @@ -11,7 +11,7 @@ import ru.tinkoff.tcb.utils.transformation.json.* import ru.tinkoff.tcb.utils.transformation.xml.* /** - * Свидетельство того, что B можно подставить в A + * "Proof that B can be substituted into A */ @finalAlg trait Substitute[A, B] { def substitute(a: A, b: B): A diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/model/CallbackChecker.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/model/CallbackChecker.scala index ca6f8e0a..f991f48f 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/model/CallbackChecker.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/model/CallbackChecker.scala @@ -17,16 +17,18 @@ trait CallbackChecker { case Some(value) => value match { case MessageCallback(dest, _, mcallback, _) => - (destinations(dest) !? Vector(s"$dest не настроен")) ++ checkCallback(mcallback, destinations) + (destinations(dest) !? Vector( + s"The field ${nameOf[MessageCallback](_.destination)} must be filled" + )) ++ checkCallback(mcallback, destinations) case HttpCallback(_, rm, p, hcallback, _) => (rm, p) match { case Some(_) <*> None => - s"Поле ${nameOf[HttpCallback](_.responseMode)} должно быть заполнено ТОЛЬКО при наличии ${nameOf[HttpCallback](_.persist)}" +: checkCallback( + s"The field ${nameOf[HttpCallback](_.responseMode)} must be filled in ONLY if ${nameOf[HttpCallback](_.persist)} is present" +: checkCallback( hcallback, destinations ) case None <*> Some(_) => - s"Поле ${nameOf[HttpCallback](_.responseMode)} должно быть заполнено при наличии ${nameOf[HttpCallback](_.persist)}" +: checkCallback( + s"The field ${nameOf[HttpCallback](_.responseMode)} must be filled in if ${nameOf[HttpCallback](_.persist)} is present" +: checkCallback( hcallback, destinations ) diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/model/Extractor.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/model/Extractor.scala index 5fd38845..5d4698f3 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/model/Extractor.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/model/Extractor.scala @@ -43,9 +43,9 @@ object XmlExtractor { /** * @param prefix - * Путь до CDATA + * Path to CDATA * @param path - * Путь внутри CDATA + * Path inside CDATA */ @derive(decoder, encoder) final case class XMLCDataExtractor(prefix: Xpath, path: Xpath) extends XmlExtractor { @@ -59,9 +59,9 @@ final case class XMLCDataExtractor(prefix: Xpath, path: Xpath) extends XmlExtrac /** * @param prefix - * Путь до CDATA + * Path to CDATA * @param path - * Путь внутри CDATA + * Path inside CDATA */ @derive(decoder, encoder) final case class JsonCDataExtractor(prefix: Xpath, path: JsonOptic) extends XmlExtractor { diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/model/GrpcStub.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/model/GrpcStub.scala index 42514065..a281e239 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/model/GrpcStub.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/model/GrpcStub.scala @@ -108,7 +108,7 @@ object GrpcStub { } yield () private val stateNonEmpty: Rule[GrpcStub] = - _.state.exists(_.isEmpty).valueOrZero(Vector("Предикат state не может быть пустым")) + _.state.exists(_.isEmpty).valueOrZero(Vector("The state predicate cannot be empty")) val validationRules: Rule[GrpcStub] = Vector(stateNonEmpty).reduce(_ |+| _) } diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/model/HttpStub.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/model/HttpStub.scala index dae1e61e..4dcd5782 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/model/HttpStub.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/model/HttpStub.scala @@ -35,34 +35,34 @@ import ru.tinkoff.tcb.validation.Rule @derive(bsonDecoder, bsonEncoder, encoder, decoder, schema) final case class HttpStub( @BsonKey("_id") - @description("id мока") + @description("Mock id") id: SID[HttpStub], - @description("Время создания мока") + @description("Mock creation time") created: Instant, - @description("Тип конфигурации") + @description("Scope") scope: Scope, - @description("Количество возможных срабатываний. Имеет смысл только для scope=countdown") + @description("The number of possible triggers. Only relevant for scope=countdown") times: Option[Int Refined NonNegative], serviceSuffix: String, - @description("Название мока") + @description("Mock name") name: String Refined NonEmpty, - @description("HTTP метод") + @description("HTTP method") method: HttpMethod, - @description("Суффикс пути, по которому срабатывает мок") + @description("The path suffix where the mock triggers") path: Option[String Refined NonEmpty], pathPattern: Option[Regex], seed: Option[Json], - @description("Предикат для поиска состояния") + @description("State search predicate") state: Option[Map[JsonOptic, Map[Keyword.Json, Json]]], - @description("Спецификация запроса") + @description("Request specification") request: HttpStubRequest, - @description("Данные, записываемые в базу") + @description("Persisted data") persist: Option[Map[JsonOptic, Json]], - @description("Спецификация ответа") + @description("Response specification") response: HttpStubResponse, - @description("Спецификация колбека") + @description("Callback specification") callback: Option[Callback], - @description("Тэги") + @description("tags") labels: Seq[String] ) @@ -70,24 +70,24 @@ object HttpStub extends CallbackChecker { private val pathOrPattern: Rule[HttpStub] = stub => (stub.path, stub.pathPattern) match { case Some(_) <*> None | None <*> Some(_) => Vector.empty - case Some(_) <*> Some(_) => Vector("Может быть указан путь либо шаблон пути") - case None <*> None => Vector("Должен быть указан путь либо шаблон пути") + case Some(_) <*> Some(_) => Vector("A path or path pattern may be specified") + case None <*> None => Vector("A path or path pattern must be specified") } private val stateNonEmpty: Rule[HttpStub] = - _.state.exists(_.isEmpty).valueOrZero(Vector("Предикат state не может быть пустым")) + _.state.exists(_.isEmpty).valueOrZero(Vector("The state predicate cannot be empty")) private val persistNonEmpty: Rule[HttpStub] = - _.persist.exists(_.isEmpty).valueOrZero(Vector("Спецификация persist не может быть пустой")) + _.persist.exists(_.isEmpty).valueOrZero(Vector("The persist specification cannot be empty")) private val jsonProxyReq: Rule[HttpStub] = stub => (stub.request, stub.response) match { case (JsonRequest(_, _, _) | JLensRequest(_, _, _), JsonProxyResponse(_, _, _, _)) => Vector.empty case (_, JsonProxyResponse(_, _, _, _)) => - Vector(s"${nameOfType[JsonProxyResponse]} может использоваться только совместно с ${nameOfType[JsonRequest]} или ${nameOfType[JLensRequest]}") + Vector(s"${nameOfType[JsonProxyResponse]} can only be used together with ${nameOfType[JsonRequest]} or ${nameOfType[JLensRequest]}") case (XmlRequest(_, _, _, _, _) | XPathRequest(_, _, _, _, _), XmlProxyResponse(_, _, _, _)) => Vector.empty case (_, XmlProxyResponse(_, _, _, _)) => - Vector(s"${nameOfType[XmlProxyResponse]} может использоваться только совместно с ${nameOfType[XmlRequest]} или ${nameOfType[XPathRequest]}") + Vector(s"${nameOfType[XmlProxyResponse]} can only be used together with ${nameOfType[XmlRequest]} or ${nameOfType[XPathRequest]}") case _ => Vector.empty } @@ -97,7 +97,7 @@ object HttpStub extends CallbackChecker { stub.response match { case EmptyResponse(_, _, _) => Vector.empty case _ => - Vector(s"Коды ответов 204 и 304 могут использоваться только с mode '${HttpStubResponse.modes(nameOfType[EmptyResponse])}'") + Vector(s"Response codes 204 and 304 can only be used with mode '${HttpStubResponse.modes(nameOfType[EmptyResponse])}'") } case _ => Vector.empty } diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/model/Scenario.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/model/Scenario.scala index 1ac74e59..9c742774 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/model/Scenario.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/model/Scenario.scala @@ -32,33 +32,33 @@ import ru.tinkoff.tcb.validation.Rule @derive(bsonDecoder, bsonEncoder, encoder, decoder, schema) final case class Scenario( @BsonKey("_id") - @description("id мока") + @description("Scenario id") id: SID[Scenario], - @description("Время создания мока") + @description("Scenario creation time") created: Instant, - @description("Тип конфигурации") + @description("Scope") scope: Scope, - @description("Количество возможных срабатываний. Имеет смысл только для scope=countdown") + @description("The number of possible triggers. Only relevant for scope=countdown") times: Option[Int Refined NonNegative], service: String Refined NonEmpty, - @description("Имя сценария, отображается в логах, полезно для отладки") + @description("Scenario name (shown in logs, handy for debugging)") name: String Refined NonEmpty, - @description("Имя источника событий") + @description("Event source name") source: SID[SourceConfiguration], seed: Option[Json], - @description("Спецификация события") + @description("Event specification") input: ScenarioInput, - @description("Предикат для поиска состояния") + @description("State search predicate") state: Option[Map[JsonOptic, Map[Keyword.Json, Json]]], - @description("Данные, записываемые в базу") + @description("Persisted data") persist: Option[Map[JsonOptic, Json]], - @description("Имя назначения ответа") + @description("Destination name") destination: Option[SID[DestinationConfiguration]], - @description("Спецификация ответа") + @description("Response specification") output: Option[ScenarioOutput], - @description("Спецификация колбека") + @description("Callback specification") callback: Option[Callback], - @description("Тэги") + @description("Tags") labels: Seq[String] ) @@ -68,26 +68,26 @@ object Scenario extends CallbackChecker { case Some(_) <*> Some(_) | None <*> None => Vector.empty case None <*> Some(_) => Vector( - s"Поле ${nameOf[Scenario](_.destination)} должно быть заполнено при наличии ${nameOf[Scenario](_.output)}" + s"The field ${nameOf[Scenario](_.destination)} must be filled in if ${nameOf[Scenario](_.output)} is present" ) case Some(_) <*> None => Vector( - s"Поле ${nameOf[Scenario](_.destination)} должно быть заполнено ТОЛЬКО при наличии ${nameOf[Scenario](_.output)}" + s"The field ${nameOf[Scenario](_.destination)} must be filled in ONLY if ${nameOf[Scenario](_.output)} is present" ) } private def checkSourceId(sources: Set[SID[SourceConfiguration]]): Rule[Scenario] = - (s: Scenario) => sources(s.source) !? Vector(s"${s.source} не настроен") + (s: Scenario) => sources(s.source) !? Vector(s"${s.source} is not configured") private def checkDestinationId(destinations: Set[SID[DestinationConfiguration]]): Rule[Scenario] = (s: Scenario) => - s.destination.map(destinations).getOrElse(true) !? Vector(s"${s.destination.getOrElse("")} не настроен") + s.destination.map(destinations).getOrElse(true) !? Vector(s"${s.destination.getOrElse("")} is not configured") private val stateNonEmpty: Rule[Scenario] = - _.state.exists(_.isEmpty).valueOrZero(Vector("Предикат state не может быть пустым")) + _.state.exists(_.isEmpty).valueOrZero(Vector("The state predicate cannot be empty.")) private val persistNonEmpty: Rule[Scenario] = - _.persist.exists(_.isEmpty).valueOrZero(Vector("Спецификация persist не может быть пустой")) + _.persist.exists(_.isEmpty).valueOrZero(Vector("The persist specification cannot be empty")) def validationRules( sources: Set[SID[SourceConfiguration]], diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/resource/ResourceManager.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/resource/ResourceManager.scala index 27b843b4..86ea2c09 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/resource/ResourceManager.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/resource/ResourceManager.scala @@ -82,7 +82,7 @@ final class ResourceManager( .mapError { case Right(err) => err case Left(err) => - ResourceManagementError(s"Запрос на ${req.url.asString} завершился ошибкой ($err)") + ResourceManagementError(s"The request to ${req.url.asString} ended with an error ($err)") } def reinitialize(sourceId: SID[SourceConfiguration]): URIO[WLD, Unit] = diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/scenario/ScenarioEngine.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/scenario/ScenarioEngine.scala index 4be212b7..1db58341 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/scenario/ScenarioEngine.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/scenario/ScenarioEngine.scala @@ -74,11 +74,11 @@ final class ScenarioEngine( for { _ <- Tracing.update(_.addToPayload(("source" -> source))) - _ <- log.info("Получено сообщение из {}", source) + _ <- log.info("Got message from {}", source) (scenario, stateOp) <- f(Scope.Countdown) .filterOrElse(_.isDefined)(f(Scope.Ephemeral).filterOrElse(_.isDefined)(f(Scope.Persistent))) - .someOrFail(ScenarioSearchError(s"Не удалось подобрать сценарий для сообщения из $source")) - _ <- log.info("Выполнение сценария '{}'", scenario.name) + .someOrFail(ScenarioSearchError(s"Failed to match a scenario for the message from $source")) + _ <- log.info("Executing scenario '{}'", scenario.name) seed = scenario.seed.map(_.eval) bodyJson = scenario.input.extractJson(message) bodyXml = scenario.input.extractXML(message) @@ -97,7 +97,7 @@ final class ScenarioEngine( .fold(ZIO.attempt(()))(_.traverse_(stateDAO.createIndexForDataField)) dests <- fetcher.getDestinations _ <- ZIO.when(scenario.destination.isDefined && !dests.exists(_.name == scenario.destination.get))( - ZIO.fail(ScenarioExecError(s"Не сконфигурирован destination с именем ${scenario.destination.get}")) + ZIO.fail(ScenarioExecError(s"Destination with the name ${scenario.destination.get} is not configured")) ) destOut = scenario.destination.flatMap(dn => dests.find(_.name == dn)) zip scenario.output _ <- ZIO.when(destOut.isDefined) { @@ -121,10 +121,10 @@ final class ScenarioEngine( case MessageCallback(destinationId, output, callback, delay) => for { _ <- ZIO.when(delay.isDefined)(ZIO.sleep(Duration.fromScala(delay.get))) - _ <- log.info("Выполняется MessageCallback с destinationId={}", destinationId) + _ <- log.info("Executing MessageCallback with destinationId={}", destinationId) dests <- fetcher.getDestinations _ <- ZIO.when(!dests.exists(_.name == destinationId))( - ZIO.fail(CallbackError(s"Не сконфигурирован destination с именем ${destinationId}")) + ZIO.fail(CallbackError(s"Destination with the name $destinationId is not configured")) ) destination = dests.find(_.name == destinationId).get _ <- sendTo(destination, output, data, xdata) @@ -134,7 +134,7 @@ final class ScenarioEngine( for { _ <- ZIO.when(delay.isDefined)(ZIO.sleep(Duration.fromScala(delay.get))) requestUrl = request.url.value.substitute(data, xdata) - _ <- log.info("Выполняется HttpCallback на {}", requestUrl) + _ <- log.info("Executing HttpCallback to {}", requestUrl) res <- basicRequest .headers(request.headers) @@ -149,8 +149,8 @@ final class ScenarioEngine( ) .response(asString) .send(httpBackend) - .filterOrElseWith(_.isSuccess)(r => ZIO.fail(CallbackError(s"$requestUrl ответил ошибкой: $r"))) - bodyStr = res.body.getOrElse(throw new UnsupportedOperationException("Не может быть")) + .filterOrElseWith(_.isSuccess)(r => ZIO.fail(CallbackError(s"$requestUrl responded with error: $r"))) + bodyStr = res.body.getOrElse(throw new UnsupportedOperationException("Can't happen")) jsonBody = responseMode .contains(CallbackResponseMode.Json) @@ -210,9 +210,9 @@ final class ScenarioEngine( .response(asString) .send(httpBackend) .filterOrElseWith(_.isSuccess)(r => - ZIO.fail(ScenarioExecError(s"Destination ${dest.name} ответил ошибкой: $r")) + ZIO.fail(ScenarioExecError(s"Destination ${dest.name} responded with error: $r")) ) *> - log.info("Отправлен ответ в {}", dest.name) + log.info("Response sent to {}", dest.name) private def b64Enc(s: String): String = new String(Base64.getEncoder.encode(s.getBytes(Charset.defaultCharset())), Charset.defaultCharset()) diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/scenario/ScenarioResolver.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/scenario/ScenarioResolver.scala index dcf2cee1..f151cc00 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/scenario/ScenarioResolver.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/scenario/ScenarioResolver.scala @@ -39,7 +39,7 @@ class ScenarioResolver( scope: Scope ): RIO[WLD, Option[(Scenario, Option[PersistentState])]] = (for { - _ <- log.info("Поиск сценариев для источника {} типа {}", source, scope) + _ <- log.info("Searching for scenarios for source {} of type {}", source, scope) condition0 = prop[Scenario](_.source) === source && prop[Scenario](_.scope) === scope condition = (scope == Scope.Countdown).fold( condition0 && prop[Scenario](_.times) > Option(refineMV[NonNegative](0)), @@ -47,16 +47,17 @@ class ScenarioResolver( ) scenarios0 <- scenarioDAO.findChunk(condition, 0, Int.MaxValue) _ <- ZIO.when(scenarios0.isEmpty)( - log.info("Не найдены обработчики для источника {} типа {}", source, scope) *> + log.info("No handlers found for source {} of type {}", source, scope) *> ZIO.fail(EarlyReturn) ) - _ <- log.info("Кандидаты: {}", scenarios0.map(_.id)) + _ <- log.info("Candidates are: {}", scenarios0.map(_.id)) scenarios1 = scenarios0.filter(_.input.checkMessage(message)) _ <- ZIO.when(scenarios1.isEmpty)( - log.warn("После валидации сообщения не осталось кандидатов, проверьте сообщение: {}", message) *> + log + .warn("After validating the message, there are no candidates left. Please verify the message: {}", message) *> ZIO.fail(EarlyReturn) ) - _ <- log.info("После валидации сообщения: {}", scenarios1.map(_.id)) + _ <- log.info("After message validation: {}", scenarios1.map(_.id)) scenarios2 <- scenarios1.traverse { scenc => val bodyJson = scenc.input.extractJson(message) val bodyXml = scenc.input.extractXML(message) @@ -67,22 +68,22 @@ class ScenarioResolver( ) } _ <- ZIO.when(scenarios2.exists(_._2.size > 1))( - log.error("Для одного или нескольких сценариев найдено более одного подходящего состояния") *> + log.error("For one or more scenarios, multiple suitable states were found") *> ZIO.fail( - ScenarioSearchError("Для одного или нескольких сценариев найдено более одного подходящего состояния") + ScenarioSearchError("For one or more scenarios, multiple suitable states were found") ) ) _ <- ZIO.when(scenarios2.count(_._2.nonEmpty) > 1)( - log.error("Для более чем одного сценария нашлось подходящее состояние") *> - ZIO.fail(ScenarioSearchError("Для более чем одного сценария нашлось подходящее состояние")) + log.error("For more than one scenario, suitable states were found") *> + ZIO.fail(ScenarioSearchError("For more than one scenario, suitable states were found")) ) _ <- ZIO.when(scenarios2.size > 1 && scenarios2.forall(c => c._1.state.isDefined && c._2.isEmpty))( - log.error("Ни для одного сценария не найдено подходящего состояния") *> - ZIO.fail(ScenarioSearchError("Ни для одного сценария не найдено подходящего состояния")) + log.error("No suitable states found for any scenario") *> + ZIO.fail(ScenarioSearchError("No suitable states found for any scenario")) ) _ <- ZIO.when(scenarios2.size > 1 && scenarios2.forall(_._1.state.isEmpty))( - log.error("Найдено более одного не требующего состояния сценария") *> - ZIO.fail(ScenarioSearchError("Найдено более одного не требующего состояния сценария")) + log.error("More than one stateless scenario found") *> + ZIO.fail(ScenarioSearchError("More than one stateless scenario found")) ) res = scenarios2.find(_._2.size == 1) orElse scenarios2.find(_._1.state.isEmpty) } yield res.map { case (scenario, states) => scenario -> states.headOption }).catchSome { case EarlyReturn => @@ -98,11 +99,11 @@ class ScenarioResolver( private def findStates(id: SID[?], spec: StateSpec): RIO[WLD, Vector[PersistentState]] = for { - _ <- log.info("Поиск state для {} по условию {}", id, spec.renderJson.noSpaces) + _ <- log.info("Searching for state for {} based on condition {}", id, spec.renderJson.noSpaces) states <- stateDAO.findBySpec(spec) _ <- - if (states.nonEmpty) log.info("Найдены состояния для {}: {}", id, states.map(_.id)) - else log.info("Не найдено подходящих состояний для {}", id) + if (states.nonEmpty) log.info("States found for {}: {}", id, states.map(_.id)) + else log.info("No suitable states found for {}", id) } yield states } diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/stream/EventSpawner.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/stream/EventSpawner.scala index ff4f6f89..fa503d80 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/stream/EventSpawner.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/stream/EventSpawner.scala @@ -87,7 +87,7 @@ final class EventSpawner( .fromEither(response.body) .mapError[Exception](err => (if (reInit) SourceFault(_) else EventProcessingError(_))( - s"Запрос на ${req.url.asString} завершился ошибкой ($err)" + s"The request to ${req.url.asString} ended with an error ($err)" ) ) processed <- ZIO.fromEither { @@ -112,7 +112,7 @@ final class EventSpawner( res <- fetch(sourceConf.request, sourceConf.reInitTriggers.map(_.toVector).orEmpty) .mapError[Exception](SpawnError(sourceConf.name, _)) neRes = res.filter(_.nonEmpty) - _ <- ZIO.when(neRes.nonEmpty)(log.info(s"Отправлено в обработку: ${neRes.length}")) + _ <- ZIO.when(neRes.nonEmpty)(log.info(s"Sent for processing: ${neRes.length}")) _ <- ZIO .validateDiscard(neRes) { engine.perform(sourceConf.name, _) @@ -145,17 +145,17 @@ final class EventSpawner( case SpawnError(sid, SourceFault(_)) => rm.reinitialize(sid.asInstanceOf[SID[SourceConfiguration]]) case EventProcessingError(err) => - log.warn(s"Ошибка при обработке события: $err") + log.warn(s"Error processing the event: $err") case ScenarioExecError(err) => - log.warn(s"Ошибка при выполнении сценария: $err") + log.warn(s"Error executing the scenario: $err") case ScenarioSearchError(err) => - log.warn(s"Ошибка при поиске сценария: $err") + log.warn(s"Error searching for the scenario: $err") case CallbackError(err) => - log.warn(s"Ошибка при выполнении колбэка: $err") + log.warn(s"Error executing the callback: $err") case SpawnError(sid, err) => - log.errorCause(s"Ошибка при получении события из {}", err, sid) + log.errorCause(s"Error retrieving event from {}", err, sid) case NonFatal(t) => - log.errorCause("Ошибка при получении события", t) + log.errorCause("Error retrieving event", t) } } diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/stream/SDFetcher.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/stream/SDFetcher.scala index b81b624d..d93021f2 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/stream/SDFetcher.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/stream/SDFetcher.scala @@ -30,9 +30,9 @@ final class SDFetcher( .awakeEvery[RIO[WLD, *]](eventConfig.reloadInterval) .evalMap(_ => sourceDAO.getAll) .evalTap(sourceCache.set) - .evalMap(srcs => log.info("Получены источники: {}", srcs.map(_.name))) + .evalMap(srcs => log.info("Sources received: {}", srcs.map(_.name))) .handleErrorWith { case NonFatal(t) => - Stream.eval(log.errorCause("Ошибка при загрузке источников", t)) ++ + Stream.eval(log.errorCause("Error loading sources", t)) ++ Stream.sleep[RIO[WLD, *]](eventConfig.reloadInterval) ++ reloadSrc } @@ -42,9 +42,9 @@ final class SDFetcher( .awakeEvery[RIO[WLD, *]](eventConfig.reloadInterval) .evalMap(_ => destinationDAO.getAll) .evalTap(destionationCache.set) - .evalMap(dsts => log.info("Получены приёмники: {}", dsts.map(_.name))) + .evalMap(dsts => log.info("Destinations received: {}", dsts.map(_.name))) .handleErrorWith { case NonFatal(t) => - Stream.eval(log.errorCause("Ошибка при загрузке приёмников", t)) ++ + Stream.eval(log.errorCause("Error loading destinations", t)) ++ Stream.sleep[RIO[WLD, *]](eventConfig.reloadInterval) ++ reloadDest } diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/predicatedsl/xml/XmlPredicate.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/predicatedsl/xml/XmlPredicate.scala index 297ac11a..dc2ded7d 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/predicatedsl/xml/XmlPredicate.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/predicatedsl/xml/XmlPredicate.scala @@ -68,7 +68,7 @@ object XmlPredicate { /** * @param description - * Имеет вид: {"/xpath": ] + * Looks like: {"/xpath": ] * @return */ def apply( diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/xml/SafeXML.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/xml/SafeXML.scala index 6d78b076..1e8f5494 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/xml/SafeXML.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/xml/SafeXML.scala @@ -11,7 +11,7 @@ import org.xml.sax.ext.DefaultHandler2 import org.xml.sax.ext.LexicalHandler /* - Хак с обработкой CDATA принадлежит перу славного индуса Kolmar + CDATA processing hack by Kolmar https://stackoverflow.com/a/35483778 */ object SafeXML extends XMLLoader[Elem] { diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/xml/package.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/xml/package.scala index 6fd4234f..b07b5e71 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/xml/package.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/xml/package.scala @@ -41,8 +41,7 @@ package object xml { Encoder.encodeString.coerce /* - Для ускорения чтения здесь не производится валидация, - предполагается, что данные записаны уже провалидированными + Validation is not performed here to speed up reading */ implicit val xmlStringBsonDecoder: BsonDecoder[XMLString] = BsonDecoder[String].coerce diff --git a/backend/project/Settings.scala b/backend/project/Settings.scala index 47c27907..37297469 100644 --- a/backend/project/Settings.scala +++ b/backend/project/Settings.scala @@ -46,21 +46,21 @@ object Settings { java.lang.Runtime.getRuntime.availableProcessors().toString, "-Ycache-plugin-class-loader:last-modified", "-Ycache-macro-class-loader:last-modified", - prelude(), // стандартные импорты + zio + prelude(), // standart imports + zio "-Ymacro-annotations", "-Xsource:3", "-Vimplicits", "-Vtype-diffs", - // далее настройки предупреждений - "-Wconf:any:wv", // отображает категория warning'а для nowarn (https://www.scala-lang.org/2021/01/12/configuring-and-suppressing-warnings.html) + // warning settings + "-Wconf:any:wv", // shows warning categories for nowarn (https://www.scala-lang.org/2021/01/12/configuring-and-suppressing-warnings.html) "-Wunused:imports", "-Wunused:privates", "-Wunused:synthetics", "-Xlint:_", - "-Xlint:-byname-implicit", // выкинут из-за бага в скале https://github.com/scala/bug/issues/12072 - "-Xlint:-unused", // частично включается через Wunused иначе ругается на макросы - "-Xlint:-missing-interpolator", // ругается на строки для mongo запросов - "-Xlint:-type-parameter-shadow", // слишком много случаев + "-Xlint:-byname-implicit", // disabled due to scala bug https://github.com/scala/bug/issues/12072 + "-Xlint:-unused", // partially enabled via Wunused otherwise is false-positive in macros + "-Xlint:-missing-interpolator", // false-positive on mongo request + "-Xlint:-type-parameter-shadow", // too many occurrences "-Ywarn-unused:imports", "-Ywarn-value-discard", "-Ywarn-dead-code", @@ -80,17 +80,17 @@ object Settings { ), missinglinkExcludedDependencies ++= Seq( moduleFilter(organization = "ch.qos.logback", name = "logback-core" | "logback-classic"), - // missinglink некорректно обрабатывает scope optional + // missinglink does not correctly handle optional scope moduleFilter(organization = "org.mongodb", name = "mongodb-driver-core" | "mongodb-driver-reactivestreams"), moduleFilter(organization = "io.netty"), - // там что-то ужасное, артефакт использует классы из зависимостей, которых нет в pom.xml + // something awful here, artifact uses classes from dependencies, which are absent in pom.xml moduleFilter(organization = "io.projectreactor", name = "reactor-core") ), missinglinkIgnoreDestinationPackages ++= Seq( - // optional зависимость в bson + // optional dependency in bson IgnoredPackage("org.slf4j"), IgnoredPackage("ch.qos.logback"), - // optional в vertx-core + // optional dependency in vertx-core IgnoredPackage("com.fasterxml.jackson.databind"), IgnoredPackage("io.vertx.core.json.jackson"), IgnoredPackage("io.netty.handler.codec.haproxy"), diff --git a/backend/utils/src/main/scala/ru/tinkoff/tcb/utils/map/package.scala b/backend/utils/src/main/scala/ru/tinkoff/tcb/utils/map/package.scala index 6b5aa715..e016950a 100644 --- a/backend/utils/src/main/scala/ru/tinkoff/tcb/utils/map/package.scala +++ b/backend/utils/src/main/scala/ru/tinkoff/tcb/utils/map/package.scala @@ -4,7 +4,7 @@ package object map { implicit class MapOps[K, V](map: Map[K, V]) { /** - * Добавляет ключ если значение Some(..) + * Adds a key is the value is Some(..) */ @inline def +?(kv: (K, Option[V])): Map[K, V] = kv._2.fold(map)(v => map + (kv._1 -> v)) diff --git a/backend/utils/src/main/scala/ru/tinkoff/tcb/utils/string/package.scala b/backend/utils/src/main/scala/ru/tinkoff/tcb/utils/string/package.scala index 62e20b46..e2a69514 100644 --- a/backend/utils/src/main/scala/ru/tinkoff/tcb/utils/string/package.scala +++ b/backend/utils/src/main/scala/ru/tinkoff/tcb/utils/string/package.scala @@ -6,7 +6,7 @@ package object string { implicit class ExtStringOps(private val text: String) extends AnyVal { /** - * Конвертирует camelCase в camel_case + * Converts camelCase into camel_case */ def camel2Underscore: String = text.drop(1).foldLeft(text.headOption.map(ch => s"${ch.toLower}") getOrElse "") { @@ -15,13 +15,13 @@ package object string { } /** - * Конвертирует snake_case в snakeCase + * Converts snake_case into snakeCase */ def underscore2Camel: String = camelize(text) /** - * Конвертирует snake_case в SnakeCase + * Converts snake_case into SnakeCase */ def underscore2UpperCamel: String = pascalize(text) diff --git a/configuration.md b/configuration.md index ffb91e30..6af73373 100644 --- a/configuration.md +++ b/configuration.md @@ -1,199 +1,197 @@ -# Конфигурация mockingbird - -Mockingbird конфигурируется посредством файла secrets.conf, имеющего следующий вид: - -``` -{ - "secrets": { - "server": { - "allowedOrigins": [ - "http://localhost", - "http://localhost:3000", - ... - ], - "healthCheckRoute": "/ready" - }, - "security": { - "secret": ".." - }, - "mongodb": { - "uri": "mongodb://.." - }, - "proxy": { - "excludedRequestHeaders": [..], - "excludedResponseHeaders": [..], - "insecureHosts": [..], - "logOutgoingRequests": false, - "proxyServer": { - "type": "http" | "socks", - "type": "..", - "port": "..", - "nonProxy": ["..", ...], - "onlyProxy": ["..", ...], - "auth": { - "user": "..", - "password": ".." - } - } - }, - "tracing": { - "required": [..], - "incomingHeaders": {}, - "outcomingHeaders": {} - } - } -} -``` - -### Секция server - -Здесь указыватся ориджены для CORS. Эти настройки влияют на работоспособность UI Mockingbird, а также swagger-ui - -healthCheckRoute - необязательный параметр, позволяет настроить эндпоинт, всегда отдающий 200 OK, полезно для healthcheck - -### Секция security - -Обязательная секция. Здесь указывается secret - ключ шифрования для конфигураций source и destination. -Рекомендуется использовать достаточно длинный ключ (от 40 символов) - -### Секция mongodb - -Обязательная секция. Здесь указывается uri для подключения к mongodb, которую будет использовать mockingbird. -Здесь же можно переопределить названия коллекций, которые будет создавать mockingbird (в примере перечислены все возможные поля со значениями по-умолчанию, не обязательно указывать все): - -``` -{ - "secrets": { - "mongodb": { - "uri": "mongodb://..", - "collections": { - "stub": "mockingbirdStubs", - "state": "mockingbirdStates", - "scenario": "mockingbirdScenarios", - "service": "mockingbirdServices", - "label": "mockingbirdLabels", - "grpcStub": "mockingbirdGrpcStubs" - } - } - } -} -``` - -### Секция proxy - -В данной секции можно указать заголовки, которые mockingbird будет отбрасывать при работе в режимах proxy и json-proxy - -Пример типовой конфигурации: - -``` -{ - "secrets": { - "proxy": { - "excludedRequestHeaders": ["Host", "HOST", "User-Agent", "user-agent"], - "excludedResponseHeaders": ["transfer-encoding"], - "insecureHosts": [ - "some.host" - ], - "logOutgoingRequests": false - } - } -} -``` - -В поле insecureHosts можно указать список хостов, для которых не будет выполняться проверка сертификатов. Это может быть полезно -для случаев развёртывания во внутренней инфраструктуре. - -Флаг logOutgoingRequests позволяет включить логирование запросов к удаленному серверу, когда http заглушка работет в режиме прокси. Запрос пишется в лог в виде команды curl с заголовками и телом запроса. - -Так-же в этой секции можно указать настройки прокси сервера. Эти настройки влияют на ВСЕ http запросы, которые делаем mockingbird, т.е.: -- запросы к внешнему серверу с proxy моках -- запросы в source и destination (включая init/shutdown) - -Назначения полей: -- type - тип прокси сервера -- host - хост -- port - порт -- nonProxy - (опционально) перечень доменов (масок доменов), запросы к которым НЕ НУЖНО проксировать -- onlyProxy - (опционально) перечень доменов (масок доменов), запросы к которым НУЖНО проксировать. -Если указать одновременно nonProxy и onlyProxy, то nonProxy будет иметь приоритет -- auth - (опционально) параметры авторизации - -Можно указывать как домены, так и маски: "localhost", "*.local", "127.*" - -### Секция tracing - -Данная секция описывает какие поля будут фигурировать в логах и в заголовках ответа. - -Пример конфигурации: - -``` -{ - "secrets": { - "tracing": { - "required": ["correlationId", "traceId"], - "incomingHeaders": { - "X-Trace-ID": "traceId", - "X-Request-ID": "traceId" - }, - "outcomingHeaders": { - "correlationId": "X-Correlation-ID", - "traceId": "X-Trace-ID" - } - } - } -} -``` - -Поле `required` - массив строковых значений, ключи которые будут добавлены к логам, в качестве значений будут сгенерированы UUID. - -Поле `incomingHeaders` - указывает какие заголовки будут извлечены из входящих запросов и в какие поля логов будут записаны значения. Извлечение заголовков делается без учета регистра, т.е. `X-Trace-Id` и `x-trace-id` эквиваленты. - -Поле `outcomingHeaders` задает значения каких полей трасировки из будут возвращены в заголовках ответа. - -В приведенном выше примере, для полей указанных в поле `required` будут сгенерированы UUID. Из запросов извлекаются значения заголовков `X-Trace-ID` и `X-Request-ID`. Значения записываются в поле с именем `traceId`. Значение поля `traceId` будет или сгенерировано, или взято из соотвествующего заголовка запроса. В случае, если оба заголовка присуствуют в запросе, то одно значение перепишет другое, порядок обработки заголовков неопределен. К ответам мокингберда будут добавлены заголовки `X-Correlation-ID` и `X-Trace-ID`. Если какое-то поле трасировки только указано в секциях `incomingHeaders` и `outcomingHeaders`, то в заголовках ответа оно будет добавлено лишь в том случае, если было в запросе. Все значения полей трассировки добавляются к логам, при условии наличия значений. - -Конфигурация поле трасировки по-умолчанию следующая: -``` -{ - "tracing": { - "required": ["correlationId"], - "incomingHeaders": {}, - "outcomingHeaders": { - "correlationId": "X-Correlation-ID", - } - } -} -``` - - -### Custom Fields в JSON логах - -Необходимо описать свой `logback.xml` файл и передать его в приложение через VM Options как `-Dlogback.configurationFile=...`. - -Пример конфигурации со своими полями, в значении `customFields` можно использовать интерполяцию переменных окружения: - -```xml - - - - - - - - UTF-8 - - {"env":"${ENV}","inst":"${HOSTNAME}","system":"mockingbird"} - - - - - - - - - - - - - -``` \ No newline at end of file +# Mockingbird Configuration + +Mockingbird is configured via the secrets.conf file, which has the following structure: + +``` +{ + "secrets": { + "server": { + "allowedOrigins": [ + "http://localhost", + "http://localhost:3000", + ... + ], + "healthCheckRoute": "/ready" + }, + "security": { + "secret": ".." + }, + "mongodb": { + "uri": "mongodb://.." + }, + "proxy": { + "excludedRequestHeaders": [..], + "excludedResponseHeaders": [..], + "insecureHosts": [..], + "logOutgoingRequests": false, + "proxyServer": { + "type": "http" | "socks", + "type": "..", + "port": "..", + "nonProxy": ["..", ...], + "onlyProxy": ["..", ...], + "auth": { + "user": "..", + "password": ".." + } + } + }, + "tracing": { + "required": [..], + "incomingHeaders": {}, + "outcomingHeaders": {} + } + } +} +``` + +### Server Section + +This section specifies origins for CORS. These settings affect the functionality of UI Mockingbird as well as swagger-ui. + +healthCheckRoute - an optional parameter that allows configuring an endpoint always returning 200 OK, useful for health checks. + +### Security Section + +Mandatory section. Here the secret is specified - the encryption key for the configurations of source and destination. +It is recommended to use a sufficiently long key (at least 40 characters). + +### MongoDB Section + +Mandatory section. Here the URI for connecting to MongoDB is specified, which Mockingbird will use. +Here you can also override the names of collections that Mockingbird will create (all possible fields with default values are listed in the example, it is not necessary to specify all). + +``` +{ + "secrets": { + "mongodb": { + "uri": "mongodb://..", + "collections": { + "stub": "mockingbirdStubs", + "state": "mockingbirdStates", + "scenario": "mockingbirdScenarios", + "service": "mockingbirdServices", + "label": "mockingbirdLabels", + "grpcStub": "mockingbirdGrpcStubs" + } + } + } +} +``` + +### Proxy Section + +In this section, you can specify headers that Mockingbird will discard when operating in proxy and json-proxy modes. + +Example of a typical configuration: + +``` +{ + "secrets": { + "proxy": { + "excludedRequestHeaders": ["Host", "HOST", "User-Agent", "user-agent"], + "excludedResponseHeaders": ["transfer-encoding"], + "insecureHosts": [ + "some.host" + ], + "logOutgoingRequests": false + } + } +} +``` + +In the insecureHosts field, you can specify a list of hosts for which certificate validation will not be performed. This can be useful for deployments within internal infrastructure. + +The logOutgoingRequests flag allows enabling logging of requests to the remote server when the HTTP mock is operating in proxy mode. The request is logged in the form of a curl command with headers and request body. + +Also, in this section, you can specify proxy server settings. These settings affect ALL HTTP requests made by Mockingbird, including: + +- requests to external servers with proxy mocks +- requests to source and destination (including init/shutdown) + +Field purposes: +- type - proxy server type +- host - host +- port - port +- nonProxy - (optional) a list of domains (domain masks) to which requests DO NOT need to be proxied +- onlyProxy - (optional) a list of domains (domain masks) to which requests NEED to be proxied. + If both nonProxy and onlyProxy are specified simultaneously, nonProxy will take precedence. +- auth - (optional) authentication parameters + +Both domains and masks can be specified: "localhost", ".local", "127." + +### Tracing Section + +This section describes which fields will appear in the logs and in the response headers. + +Example configuration: + +``` +{ + "secrets": { + "tracing": { + "required": ["correlationId", "traceId"], + "incomingHeaders": { + "X-Trace-ID": "traceId", + "X-Request-ID": "traceId" + }, + "outcomingHeaders": { + "correlationId": "X-Correlation-ID", + "traceId": "X-Trace-ID" + } + } + } +} +``` + +The `required` field is an array of string values, the keys of which will be added to the logs, and UUIDs will be generated as values. + +The `incomingHeaders` field specifies which headers will be extracted from incoming requests and into which log fields the values will be written. Header extraction is case-insensitive, meaning `X-Trace-Id` and `x-trace-id` are equivalent. + +The `outcomingHeaders` field sets the values of which tracing fields will be returned in the response headers. + +In the example provided above, UUIDs will be generated for the fields specified in the `required` field. Values of the `X-Trace-ID` and `X-Request-ID` headers are extracted from requests and written into a field named `traceId`. The value of the `traceId` field will either be generated or taken from the corresponding request header. If both headers are present in the request, one value will overwrite the other; the order of header processing is undefined. Mockingbird responses will include the headers `X-Correlation-ID` and `X-Trace-ID`. If a tracing field is only specified in the `incomingHeaders` and `outcomingHeaders` sections, it will be added to the response headers only if it was present in the request. All tracing field values are added to the logs, provided there are values available. + +Default tracing configuration is as follows: +``` +{ + "tracing": { + "required": ["correlationId"], + "incomingHeaders": {}, + "outcomingHeaders": { + "correlationId": "X-Correlation-ID", + } + } +} +``` +### Custom Fields in JSON Logs + +To describe your `logback.xml` file and pass it to the application via VM Options, use `-Dlogback.configurationFile=...`. + +Below is an example configuration with custom fields. In the `customFields` value, you can use environment variable interpolation: + +```xml + + + + + + + + UTF-8 + + {"env":"${ENV}","inst":"${HOSTNAME}","system":"mockingbird"} + + + + + + + + + + + + + +``` diff --git a/configuration_ru.md b/configuration_ru.md new file mode 100644 index 00000000..ffb91e30 --- /dev/null +++ b/configuration_ru.md @@ -0,0 +1,199 @@ +# Конфигурация mockingbird + +Mockingbird конфигурируется посредством файла secrets.conf, имеющего следующий вид: + +``` +{ + "secrets": { + "server": { + "allowedOrigins": [ + "http://localhost", + "http://localhost:3000", + ... + ], + "healthCheckRoute": "/ready" + }, + "security": { + "secret": ".." + }, + "mongodb": { + "uri": "mongodb://.." + }, + "proxy": { + "excludedRequestHeaders": [..], + "excludedResponseHeaders": [..], + "insecureHosts": [..], + "logOutgoingRequests": false, + "proxyServer": { + "type": "http" | "socks", + "type": "..", + "port": "..", + "nonProxy": ["..", ...], + "onlyProxy": ["..", ...], + "auth": { + "user": "..", + "password": ".." + } + } + }, + "tracing": { + "required": [..], + "incomingHeaders": {}, + "outcomingHeaders": {} + } + } +} +``` + +### Секция server + +Здесь указыватся ориджены для CORS. Эти настройки влияют на работоспособность UI Mockingbird, а также swagger-ui + +healthCheckRoute - необязательный параметр, позволяет настроить эндпоинт, всегда отдающий 200 OK, полезно для healthcheck + +### Секция security + +Обязательная секция. Здесь указывается secret - ключ шифрования для конфигураций source и destination. +Рекомендуется использовать достаточно длинный ключ (от 40 символов) + +### Секция mongodb + +Обязательная секция. Здесь указывается uri для подключения к mongodb, которую будет использовать mockingbird. +Здесь же можно переопределить названия коллекций, которые будет создавать mockingbird (в примере перечислены все возможные поля со значениями по-умолчанию, не обязательно указывать все): + +``` +{ + "secrets": { + "mongodb": { + "uri": "mongodb://..", + "collections": { + "stub": "mockingbirdStubs", + "state": "mockingbirdStates", + "scenario": "mockingbirdScenarios", + "service": "mockingbirdServices", + "label": "mockingbirdLabels", + "grpcStub": "mockingbirdGrpcStubs" + } + } + } +} +``` + +### Секция proxy + +В данной секции можно указать заголовки, которые mockingbird будет отбрасывать при работе в режимах proxy и json-proxy + +Пример типовой конфигурации: + +``` +{ + "secrets": { + "proxy": { + "excludedRequestHeaders": ["Host", "HOST", "User-Agent", "user-agent"], + "excludedResponseHeaders": ["transfer-encoding"], + "insecureHosts": [ + "some.host" + ], + "logOutgoingRequests": false + } + } +} +``` + +В поле insecureHosts можно указать список хостов, для которых не будет выполняться проверка сертификатов. Это может быть полезно +для случаев развёртывания во внутренней инфраструктуре. + +Флаг logOutgoingRequests позволяет включить логирование запросов к удаленному серверу, когда http заглушка работет в режиме прокси. Запрос пишется в лог в виде команды curl с заголовками и телом запроса. + +Так-же в этой секции можно указать настройки прокси сервера. Эти настройки влияют на ВСЕ http запросы, которые делаем mockingbird, т.е.: +- запросы к внешнему серверу с proxy моках +- запросы в source и destination (включая init/shutdown) + +Назначения полей: +- type - тип прокси сервера +- host - хост +- port - порт +- nonProxy - (опционально) перечень доменов (масок доменов), запросы к которым НЕ НУЖНО проксировать +- onlyProxy - (опционально) перечень доменов (масок доменов), запросы к которым НУЖНО проксировать. +Если указать одновременно nonProxy и onlyProxy, то nonProxy будет иметь приоритет +- auth - (опционально) параметры авторизации + +Можно указывать как домены, так и маски: "localhost", "*.local", "127.*" + +### Секция tracing + +Данная секция описывает какие поля будут фигурировать в логах и в заголовках ответа. + +Пример конфигурации: + +``` +{ + "secrets": { + "tracing": { + "required": ["correlationId", "traceId"], + "incomingHeaders": { + "X-Trace-ID": "traceId", + "X-Request-ID": "traceId" + }, + "outcomingHeaders": { + "correlationId": "X-Correlation-ID", + "traceId": "X-Trace-ID" + } + } + } +} +``` + +Поле `required` - массив строковых значений, ключи которые будут добавлены к логам, в качестве значений будут сгенерированы UUID. + +Поле `incomingHeaders` - указывает какие заголовки будут извлечены из входящих запросов и в какие поля логов будут записаны значения. Извлечение заголовков делается без учета регистра, т.е. `X-Trace-Id` и `x-trace-id` эквиваленты. + +Поле `outcomingHeaders` задает значения каких полей трасировки из будут возвращены в заголовках ответа. + +В приведенном выше примере, для полей указанных в поле `required` будут сгенерированы UUID. Из запросов извлекаются значения заголовков `X-Trace-ID` и `X-Request-ID`. Значения записываются в поле с именем `traceId`. Значение поля `traceId` будет или сгенерировано, или взято из соотвествующего заголовка запроса. В случае, если оба заголовка присуствуют в запросе, то одно значение перепишет другое, порядок обработки заголовков неопределен. К ответам мокингберда будут добавлены заголовки `X-Correlation-ID` и `X-Trace-ID`. Если какое-то поле трасировки только указано в секциях `incomingHeaders` и `outcomingHeaders`, то в заголовках ответа оно будет добавлено лишь в том случае, если было в запросе. Все значения полей трассировки добавляются к логам, при условии наличия значений. + +Конфигурация поле трасировки по-умолчанию следующая: +``` +{ + "tracing": { + "required": ["correlationId"], + "incomingHeaders": {}, + "outcomingHeaders": { + "correlationId": "X-Correlation-ID", + } + } +} +``` + + +### Custom Fields в JSON логах + +Необходимо описать свой `logback.xml` файл и передать его в приложение через VM Options как `-Dlogback.configurationFile=...`. + +Пример конфигурации со своими полями, в значении `customFields` можно использовать интерполяцию переменных окружения: + +```xml + + + + + + + + UTF-8 + + {"env":"${ENV}","inst":"${HOSTNAME}","system":"mockingbird"} + + + + + + + + + + + + + +``` \ No newline at end of file diff --git a/deployment.md b/deployment.md index 33cf1416..3a6a370f 100644 --- a/deployment.md +++ b/deployment.md @@ -1,60 +1,60 @@ -# Руководство по инсталляции mockingbird - -Требования к окружению: -- mongodb версии 4.2 и выше. В целом mockingbird запустится и с 3.x, но как минимум не будут работать моки с pathPattern -- 512 MB памяти для контейнера (абсолютный минимум в районе 300 MB) - -mockingbird доступен в двух вариантах -- native приложение - -`ghcr.io/tinkoff/mockingbird:-native` - -Рекомендуемый образ для большинства случаев. Представляет собой скомпилированное в native-image с помощью GraalVM Scala приложение - -- образ на классической JVM - -`ghcr.io/tinkoff/mockingbird:` - -Для обоих вариантов: HTTP порт 8228, GRPC порт 9000 - -## mockingbird-native - -При запуске образа нужно передать в CMD параметры. Типовой набор, с которого можно начать: - -`-server -Xms256m -Xmx256m -XX:MaxDirectMemorySize=128m -Dconfig.file=/opt/mockingbird-native/qa.conf -Dlog.level=DEBUG -Dlog4j.formatMsgNoLookups=true` - -Так-же необходимо примонтировать по пути `/opt/mockingbird-native/conf/secrets.conf` файл конфигурации mockingbird. -Минимальная конфигурация выглядит следующим образом: - -``` -{ - "secrets": { - "server": { - "allowedOrigins": [ - "*" - ] - }, - "mongodb": { - "uri": "mongodb://.." - }, - "security": { - "secret": ".." - } - } -} -``` -Подробнее о secrets.conf можно узнать из [руководства по настройке](configuration.md) - -Логи приложение пишет в /opt/log/mockingbird-native - -## mockingbird - -Этот образ содержит приложение на классической JVM, поэтому параметры передаются через переменную окружения JAVA_OPTS. -Пример типовых настроек - -`-server -XX:+AlwaysActAsServerClassMachine -Xms256m -Xmx256m -XX:MaxMetaspaceSize=256m -XX:MaxDirectMemorySize=128m -XX:ReservedCodeCacheSize=128m -XX:+PerfDisableSharedMem -Dconfig.resource=qa.conf -Dlog.level=DEBUG -Dlog4j.formatMsgNoLookups=true` - -Так-же необходимо примонтировать по пути `/opt/mockingbird/conf/secrets.conf` файл конфигурации mockingbird. -Формат и содержимое `/opt/mockingbird/conf/secrets.conf` полностью идентичны оному для mockingbird-native - -Логи приложение пишет в /opt/log/mockingbird +# Mockingbird Installation Guide + +Environment requirements: +- MongoDB version 4.2 or higher. In general, mockingbird will run on 3.x, but at least mocks with pathPattern won't work. +- 512 MB of memory for the container (the absolute minimum is around 300 MB). + +mockingbird is available in two variants: +- native application + +`ghcr.io/tinkoff/mockingbird:-native` + +The recommended image for most cases. It is a Scala application compiled into a native image using GraalVM. + +- image on classic JVM + +`ghcr.io/tinkoff/mockingbird:` + +For both variants: HTTP port 8228, GRPC port 9000. + +## mockingbird-native + +When launching the image, parameters need to be passed in CMD. A typical set to start with is: + +`-server -Xms256m -Xmx256m -XX:MaxDirectMemorySize=128m -Dconfig.file=/opt/mockingbird-native/qa.conf -Dlog.level=DEBUG -Dlog4j.formatMsgNoLookups=true` + +Also, the mockingbird configuration file needs to be mounted at the path `/opt/mockingbird-native/conf/secrets.conf`. +The minimal configuration looks like this: + +``` +{ + "secrets": { + "server": { + "allowedOrigins": [ + "*" + ] + }, + "mongodb": { + "uri": "mongodb://.." + }, + "security": { + "secret": ".." + } + } +} +``` +More about `secrets.conf` can be learned from the [configuration guide](configuration.md). + +The application logs to /opt/log/mockingbird-native. + +## mockingbird + +This image contains the application on the classic JVM, so parameters are passed through the JAVA_OPTS environment variable. +Example of typical settings: + +`-server -XX:+AlwaysActAsServerClassMachine -Xms256m -Xmx256m -XX:MaxMetaspaceSize=256m -XX:MaxDirectMemorySize=128m -XX:ReservedCodeCacheSize=128m -XX:+PerfDisableSharedMem -Dconfig.resource=qa.conf -Dlog.level=DEBUG -Dlog4j.formatMsgNoLookups=true` + +Also, the mockingbird configuration file needs to be mounted at the path `/opt/mockingbird/conf/secrets.conf`. +The format and contents of `/opt/mockingbird/conf/secrets.conf` are fully identical to those for mockingbird-native. + +The application logs to /opt/log/mockingbird. \ No newline at end of file diff --git a/deployment_ru.md b/deployment_ru.md new file mode 100644 index 00000000..33cf1416 --- /dev/null +++ b/deployment_ru.md @@ -0,0 +1,60 @@ +# Руководство по инсталляции mockingbird + +Требования к окружению: +- mongodb версии 4.2 и выше. В целом mockingbird запустится и с 3.x, но как минимум не будут работать моки с pathPattern +- 512 MB памяти для контейнера (абсолютный минимум в районе 300 MB) + +mockingbird доступен в двух вариантах +- native приложение + +`ghcr.io/tinkoff/mockingbird:-native` + +Рекомендуемый образ для большинства случаев. Представляет собой скомпилированное в native-image с помощью GraalVM Scala приложение + +- образ на классической JVM + +`ghcr.io/tinkoff/mockingbird:` + +Для обоих вариантов: HTTP порт 8228, GRPC порт 9000 + +## mockingbird-native + +При запуске образа нужно передать в CMD параметры. Типовой набор, с которого можно начать: + +`-server -Xms256m -Xmx256m -XX:MaxDirectMemorySize=128m -Dconfig.file=/opt/mockingbird-native/qa.conf -Dlog.level=DEBUG -Dlog4j.formatMsgNoLookups=true` + +Так-же необходимо примонтировать по пути `/opt/mockingbird-native/conf/secrets.conf` файл конфигурации mockingbird. +Минимальная конфигурация выглядит следующим образом: + +``` +{ + "secrets": { + "server": { + "allowedOrigins": [ + "*" + ] + }, + "mongodb": { + "uri": "mongodb://.." + }, + "security": { + "secret": ".." + } + } +} +``` +Подробнее о secrets.conf можно узнать из [руководства по настройке](configuration.md) + +Логи приложение пишет в /opt/log/mockingbird-native + +## mockingbird + +Этот образ содержит приложение на классической JVM, поэтому параметры передаются через переменную окружения JAVA_OPTS. +Пример типовых настроек + +`-server -XX:+AlwaysActAsServerClassMachine -Xms256m -Xmx256m -XX:MaxMetaspaceSize=256m -XX:MaxDirectMemorySize=128m -XX:ReservedCodeCacheSize=128m -XX:+PerfDisableSharedMem -Dconfig.resource=qa.conf -Dlog.level=DEBUG -Dlog4j.formatMsgNoLookups=true` + +Так-же необходимо примонтировать по пути `/opt/mockingbird/conf/secrets.conf` файл конфигурации mockingbird. +Формат и содержимое `/opt/mockingbird/conf/secrets.conf` полностью идентичны оному для mockingbird-native + +Логи приложение пишет в /opt/log/mockingbird diff --git a/examples/basic_http_stub.md b/examples/basic_http_stub.md index 7233a396..9dea9c91 100644 --- a/examples/basic_http_stub.md +++ b/examples/basic_http_stub.md @@ -1,9 +1,9 @@ -# Базовые примеры работы с HTTP заглушками -## Persistent, ephemeral и countdown HTTP заглушки +# Basic examples of working with HTTP stubs +## Persistent, ephemeral, and countdown HTTP stubs -Предполагается, что в mockingbird есть сервис `alpha`. +It is assumed that in mockingbird there is a service `alpha`. -Создаем заглушку в скоупе `persistent`. +Creating a stub in the persistent `scope`. ``` curl \ --request POST \ @@ -30,11 +30,11 @@ curl \ ``` -Ответ: +Response: ``` -Код ответа: 200 +Response code: 200 -Тело ответа: +Response body: { "status" : "success", "id" : "29dfd29e-d684-462e-8676-94dbdd747e30" @@ -42,7 +42,7 @@ curl \ ``` -Проверяем созданную заглушку. +Checking the created stub. ``` curl \ --request GET \ @@ -50,19 +50,19 @@ curl \ ``` -Ответ: +Response: ``` -Код ответа: 451 +Response code: 451 -Заголовки ответа: +Response headers: Content-Type: 'text/plain' -Тело ответа: +Response body: persistent scope ``` -Для этого же пути, создаем заглушку в скоупе `ephemeral`. +For the same path, creating a stub in the `ephemeral` scope. ``` curl \ --request POST \ @@ -89,11 +89,11 @@ curl \ ``` -Ответ: +Response: ``` -Код ответа: 200 +Response code: 200 -Тело ответа: +Response body: { "status" : "success", "id" : "13da7ef2-650e-4a54-9dca-377a1b1ca8b9" @@ -101,7 +101,7 @@ curl \ ``` -И создаем заглушку в скоупе `countdown` с `times` равным 2. +And creating a stub in the `countdown` scope with `times` equal to 2. ``` curl \ --request POST \ @@ -129,11 +129,11 @@ curl \ ``` -Ответ: +Response: ``` -Код ответа: 200 +Response code: 200 -Тело ответа: +Response body: { "status" : "success", "id" : "09ec1cb9-4ca0-4142-b796-b94a24d9df29" @@ -141,12 +141,12 @@ curl \ ``` -Заданные заглушки отличаются возвращаемыми ответами, а именно содержимым `body` и `code`, - в целом они могут быть как и полностью одинаковыми так и иметь больше различий. - Скоупы заглушек в порядке убывания приоритета: Countdown, Ephemeral, Persistent +The specified stubs differ in the responses they return, namely the contents of `body` and `code`, + in general, they can be either completely identical or have more differences. + The scopes of stubs in descending order of priority: Countdown, Ephemeral, Persistent -Так как заглушка `countdown` была создана с `times` равным двум, то следующие два -запроса вернут указанное в ней содержимое. +Since the countdown stub was created with `times` equal to two, the next two +requests will return the specified content. ``` curl \ --request GET \ @@ -154,14 +154,14 @@ curl \ ``` -Ответ: +Response: ``` -Код ответа: 429 +Response code: 429 -Заголовки ответа: +Response headers: Content-Type: 'text/plain' -Тело ответа: +Response body: countdown scope ``` @@ -172,20 +172,20 @@ curl \ ``` -Ответ: +Response: ``` -Код ответа: 429 +Response code: 429 -Заголовки ответа: +Response headers: Content-Type: 'text/plain' -Тело ответа: +Response body: countdown scope ``` -Последующие запросы будут возвращать содержимое заглушки `ephemeral`. Если бы её не было, -то вернулся бы ответ от заглушки `persistent`. +Subsequent requests will return the content of the `ephemeral` stub. If it didn't exist, +the response from the `persistent` stub would be returned.. ``` curl \ --request GET \ @@ -193,20 +193,20 @@ curl \ ``` -Ответ: +Response: ``` -Код ответа: 200 +Response code: 200 -Заголовки ответа: +Response headers: Content-Type: 'text/plain' -Тело ответа: +Response body: ephemeral scope ``` -Чтобы получить теперь ответ от `persistent` заглушки нужно или дождаться, когда истекут -сутки с момента её создания или просто удалить `ephemeral` заглушку. +Now to get a response from the `persistent` stub, one must either wait until a day has passed +since its creation or simply delete the `ephemeral` stub. ``` curl \ --request DELETE \ @@ -215,11 +215,11 @@ curl \ ``` -Ответ: +Response: ``` -Код ответа: 200 +Response code: 200 -Тело ответа: +Response body: { "status" : "success", "id" : null @@ -227,7 +227,7 @@ curl \ ``` -После удаления `ephemeral` заглушки, при запросе вернется результат заглушки `persistent` +After deleting the `ephemeral` stub, a request will return the result of the `persistent` stub. ``` curl \ --request GET \ @@ -235,30 +235,30 @@ curl \ ``` -Ответ: +Response: ``` -Код ответа: 451 +Response code: 451 -Заголовки ответа: +Response headers: Content-Type: 'text/plain' -Тело ответа: +Response body: persistent scope ``` -## Использование параметров пути в HTTP заглушках +## Using path parameters in HTTP stubs -Заглушка может выбираться в том числе и на основании регулярного выражения -в пути, это может быть не очень эффективно с точки зрения поиска такой заглушки. -Поэтому без необходимости, лучше не использовать этот механизм. +A stub can also be selected based on a regular expression in the path, +which can be inefficient in terms of searching for such a stub. +Therefore, without necessity, it's better not to use this mechanism. -Предполагается, что в mockingbird есть сервис `alpha`. +It is assumed that in mockingbird there is a service `alpha`. -Скоуп в котором создаются заглушки не важен. В целом скоуп влияет только -на приоритет заглушек. В данном случае заглушка создается в скоупе `countdown`. -В отличие от предыдущих примеров, здесь для указания пути для срабатывания -заглушки используется поле `pathPattern`, вместо `path`. Так же, ответ который -формирует заглушка не статичный, а зависит от параметров пути. +The scope in which stubs are created does not matter. In general, the scope only affects +the priority of the stubs. In this case, the stub is created in the `countdown` scope. +Unlike previous examples, here the `pathPattern` field is used to specify the path for triggering +the stub, instead of `path`. Also, the response that +the stub generates is not static but depends on the path parameters. ``` curl \ --request POST \ @@ -290,11 +290,11 @@ curl \ ``` -Ответ: +Response: ``` -Код ответа: 200 +Response code: 200 -Тело ответа: +Response body: { "status" : "success", "id" : "c8c9d92f-192e-4fe3-8a09-4c9b69802603" @@ -302,8 +302,8 @@ curl \ ``` -Теперь сделаем несколько запросов, который приведут к срабатыванию этой заглшки, -чтобы увидеть, что результат действительно зависит от пути. +Now let's make several requests that will trigger this stub, +to see that the result really depends on the path. ``` curl \ --request GET \ @@ -311,14 +311,14 @@ curl \ ``` -Ответ: +Response: ``` -Код ответа: 200 +Response code: 200 -Заголовки ответа: +Response headers: Content-Type: 'application/json' -Тело ответа: +Response body: { "static_field" : "Fixed part of reponse", "obj" : "alpha", @@ -333,14 +333,14 @@ curl \ ``` -Ответ: +Response: ``` -Код ответа: 200 +Response code: 200 -Заголовки ответа: +Response headers: Content-Type: 'application/json' -Тело ответа: +Response body: { "static_field" : "Fixed part of reponse", "obj" : "beta", diff --git a/examples/http_stub_with_state.md b/examples/http_stub_with_state.md index d47a16fc..920694eb 100644 --- a/examples/http_stub_with_state.md +++ b/examples/http_stub_with_state.md @@ -1,16 +1,16 @@ -# Использование хранимого состояние в HTTP заглушках -## Создать, получить и обновить хранимое состояние +# Utilizing persistent state in HTTP stubs +## Create, retrieve, and update stored state -Предполагается, что в mockingbird есть сервис `alpha`. +It is assumed that in mockingbird there is a service `alpha`. -Для работы с состоянием у HTTP заглушки есть две секции: `persist` и `state`. -Секция `persist` отвечает за сохранение состояния для последующего доступа к -нему. А секция `state` содержит предикаты для поиска состояния. Если указана -только секция `persist`, то каждый раз при срабатывании заглушки в БД будет -записываться новое состояние. А если указаны обе секции, то найденное состояние -будет перезаписано. Состояние - это JSON объект. +For working with state in the HTTP stub, there are two sections: `persist` and `state`. +The `persist` section is responsible for saving the state for subsequent access to +it. The `state` section contains predicates for searching for the state. If only +the `persist` section is specified, then each time the stub is triggered, a new state will be +recorded in the database. If both sections are specified, the found state +will be overwritten. The state is a JSON object. -В качестве примера, будем хранить как состояние JSON объект вида: + As an example, we will store as the state a JSON object of the form: ```json { "id": "o1", @@ -18,9 +18,9 @@ "version": 1 } ``` -И дополнительно сохранять время создания и модификации. +And additionally save the creation and modification time. -Для первоначального создания состояния создадим следующую заглушку. +To initially create the state, we create the following stub. ``` curl \ --request POST \ @@ -59,17 +59,17 @@ curl \ } }, "seed": { - "timestamp": "%{now(yyyy-MM-dd\'T\'HH:mm:ss.nn\'Z\')}" + "timestamp": "%{now(\"yyyy-MM-dd\'T\'HH:mm:ss.nn\'Z\'\")}" } }' ``` -Ответ: +Response: ``` -Код ответа: 200 +Response code: 200 -Тело ответа: +Response body: { "status" : "success", "id" : "98f393d3-07f0-403e-9043-150bf5c5b4bc" @@ -77,17 +77,17 @@ curl \ ``` -Данная заглушка делает следующее: - * Проверяет, что тело запроса - это JSON объект содержащий как минимум одно - поле `id`. - * В секции `seed` создается переменная `timestamp` в которую записывается - текущее время. - * Секция `persist` описывает объект, который будет сохранен как состояние. - Данные, которые пришли в теле запроса записываются в поле `_data`, в добавок, - в поле `created` записывает текущее время. - * В ответе возвращаются полученные данные и временная метка. +This stub does the following: + * Checks that the request body is a JSON object containing at least one + field `id`. + * In the `seed` section, a `timestamp` variable is created in which + the current time is recorded. + * The `persist` section describes the object that will be saved as the state. + The data that came in the request body are recorded in the `_data` field, in addition, + the `created` field records the current time. + * The response returns the received data and the timestamp. -В итоге в Mockingbird состояние будет записано как: +As a result, in Mockingbird the state will be recorded as: ```json { "_data": { @@ -100,9 +100,9 @@ curl \ ``` -Добавим заглушку для модификации состояния, она будет похожей на предыдущую, -но будет иметь секцию `state` для поиска уже существующего состояния, а в секции -`persist` будет поле `modified` вместо `created`. +We add a stub for modifying the state, similar to the previous one, +but it has a state section for searching for an existing state, +and in the `persist` section, it has a `modified` field instead of `created`. ``` curl \ --request POST \ @@ -144,17 +144,17 @@ curl \ "_data.id": {"==": "${id}"} }, "seed": { - "timestamp": "%{now(yyyy-MM-dd\'T\'HH:mm:ss.nn\'Z\')}" + "timestamp": "%{now(\"yyyy-MM-dd\'T\'HH:mm:ss.nn\'Z\'\")}" } }' ``` -Ответ: +Response: ``` -Код ответа: 200 +Response code: 200 -Тело ответа: +Response body: { "status" : "success", "id" : "11b57bcc-ecee-445d-ad56-106b6ba706c7" @@ -162,28 +162,28 @@ curl \ ``` -Для обновления состояния принимаем такие же данные, как и для создания нового. -В секции `state` поля из тела запроса доступны сразу, без дополнительных, -поэтому просто пишем, имя поля `${id}`, в отличии от секций `response` -и `persist`, где доступ к данным запроса осуществляется через переменную `req`. -В случае, если используются именованные параметры пути в `pathPattern`, -то доступ к ним из секции `state` осуществляется через переменную `__segments`. +To update the state, we accept the same data as for creating a new one. +In the `state` section, fields from the request body are available immediately, +without additional, so we just write the field name `${id}`, unlike in the `response` +and `persist` sections, where access to request data is through the `req` variable. +If named path parameters are used in `pathPattern`, +then access to them from the `state` section is through the `__segments` variable. -При обновлении состояния, поля перечисленные в секции `persist` дописываются -к тем, что уже есть в найденном состоянии. В случае если поле уже существует, то -оно будет перезаписано. Стоит обратить внимание каким образом дописывается -временная метка `modified`. Она указана как `meta.modified`, такой синтаксис -позволяет перезаписывать не весь объект, а только его часть или добавлять -в него новые поля. +When updating the state, the fields listed in the `persist` section are appended +to those already in the found state. In case a field already exists, it +will be overwritten. Pay attention to how the modified `timestamp` is appended. +It is indicated as `meta.modified`, this syntax +allows overwriting not the entire object, but only part of it or adding +new fields to it. -При выборе между двух заглушек, заглушка для которой выполнилось условие поиска -хранимого состояние, т.е. существует состояние удовлетворяющее критериям -указанным в секции `state`, имеет больший приоритет, чем заглушка без условий -выбора состояний. Поэтому первая заглушка будет срабатывать когда в БД ещё нет -хранимого состояния с указанным `id`, а вторая когда такое состояние уже есть. +When choosing between two stubs, the stub for which the search condition +for the stored state is met, i.e., there exists a state meeting the criteria +specified in the `state` section, has a higher priority than a stub without conditions +for selecting states. Therefore, the first stub will be triggered when there is no +stored state with the specified `id` in the database, and the second when such a state already exists. -Теперь создадим заглушку для получения хранимого состояния. Получать состояние -будем отправляя POST запрос с JSON содержащим поле `id`: +Now we create a stub for retrieving the stored state. We will retrieve the state +by sending a POST request with JSON containing the `id` field: ```json { "id": "o1" @@ -226,11 +226,11 @@ curl \ ``` -Ответ: +Response: ``` -Код ответа: 200 +Response code: 200 -Тело ответа: +Response body: { "status" : "success", "id" : "da6b4458-596b-4db9-8943-0cea96bbba33" @@ -238,7 +238,7 @@ curl \ ``` -Теперь попробуем вызвать заглушку, записывающую новое состояние. +Now let's try to invoke the stub that writes a new state. ``` curl \ --request POST \ @@ -252,11 +252,11 @@ curl \ ``` -Ответ: +Response: ``` -Код ответа: 200 +Response code: 200 -Тело ответа: +Response body: { "new" : { "id" : "o1", @@ -270,7 +270,7 @@ curl \ ``` -А теперь получить состояние +And now retrieve the state ``` curl \ --request POST \ @@ -280,11 +280,11 @@ curl \ ``` -Ответ: +Response: ``` -Код ответа: 200 +Response code: 200 -Тело ответа: +Response body: { "data" : { "id" : "o1", @@ -298,8 +298,8 @@ curl \ ``` -Теперь модифицируем состояние, изменив значение поля `version` и добавив новое -поле `description`. Поле `name` опустим. +Now we modify the state, changing the value of the `version` field +and adding a new field `description`. We will omit the `name` field. ``` curl \ --request POST \ @@ -313,11 +313,11 @@ curl \ ``` -Ответ: +Response: ``` -Код ответа: 200 +Response code: 200 -Тело ответа: +Response body: { "old" : { "id" : "o1", @@ -337,7 +337,7 @@ curl \ ``` -И снова запросим состояние объекта `o1` +And again, we request the state of object `o1` ``` curl \ --request POST \ @@ -347,11 +347,11 @@ curl \ ``` -Ответ: +Response: ``` -Код ответа: 200 +Response code: 200 -Тело ответа: +Response body: { "data" : { "id" : "o1", @@ -366,14 +366,14 @@ curl \ ``` -Ответ изменился, мы видим новые поля. Так как поле `data` перезаписывалось -целиком, то поле `name` исчезло, в то время как в объекте `meta` -модифицировалось только поле `modified`, поэтому, хотя поле `created` не указано -в секции `persist` заглушки обновляющей сосотояние, оно отсталось. +The response changed, we see new fields. Since the `data` field was completely overwritten, +the `name` field disappeared, while in the `meta` object, +only the modified field was `modified`, so although the `created` field is not mentioned +in the `persist` section of the stub updating the state, it remained. -Если попробовать вызвать заглушку читающую состояние объекта которого нет, -то Mockingbird вернет ошибку, в котрой будет сказано, что не найдено подходящие -заглушки. +If we try to invoke the stub for reading the state of an object that does not exist, +Mockingbird will return an error, stating that no suitable +stubs were found. ``` curl \ --request POST \ @@ -383,33 +383,33 @@ curl \ ``` -Ответ: +Response: ``` -Код ответа: 400 +Response code: 400 -Тело ответа: -ru.tinkoff.tcb.mockingbird.error.StubSearchError: Не удалось подобрать заглушку для [Post] /alpha/state1/get +Response body: +ru.tinkoff.tcb.mockingbird.error.StubSearchError: Could not find stub for [Post] /alpha/state1/get ``` -Для решения подобной проблемы, надо создать вторую заглушку с таким же `path`, -но с незаполненным `state`. Тогда, в случае отсутствия искомого состояния, будет -отрабатывать она. Это аналогично тому как мы создали заглушку для записи нового -состояния и заглушку для его обновления. -## Несколько состояний подходящих под условие поиска +To solve such a problem, one should create a second stub with the same `path`, +but with an empty `state`. Then, in the absence of the searched state, it +will be triggered. This is similar to how we created a stub for writing a new +state and a stub for its update. +## Multiple states matching the search condition -В предыдущем примере было рассмотрено создание и модификация состояния, -для этого было создано две соответствующие заглушки. Важно помнить, что если -секция `state` не указана, а указана только секция `persist`, то в БД **всегда** -создается новый объект состояния. При это заглушка с заполненным полем `state` -будет выбрана только в том случае, если в результате поиска по заданным -параметрам из БД вернулся строго один объект с состоянием. +In the previous example, creating and modifying a state was discussed, +for which two corresponding stubs were created. It is important to remember that if +the `state` section is not specified, and only the `persist` section is, then in the database **always** +a new state object is created. Meanwhile, a stub with a filled `state` +field will be selected only in the case that, as a result of the search by specified +parameters, exactly one state object is returned from the database. -**ВНИМАНИЕ!** Функции удаления состояний в Mockingbird нет. Неосторожная работа -с состояниями может привести к неработоспособности заглушек и придется удалять -данные напрямую из БД. +**ATTENTION!** There is no function to delete states in Mockingbird. Careless work +with states can lead to the inoperability of stubs, and it will be necessary to delete +data directly from the database. -Для демонстрации этого создадим новые заглушки для записи и чтения состояния. +To demonstrate this, we will create new stubs for writing and reading a state. ``` curl \ --request POST \ @@ -443,9 +443,9 @@ curl \ ``` -Ответ: +Response: ``` -Код ответа: 200 +Response code: 200 ``` ``` @@ -481,13 +481,13 @@ curl \ ``` -Ответ: +Response: ``` -Код ответа: 200 +Response code: 200 ``` -Вызовем заглушку для записи состояния +We call the stub for writing a state ``` curl \ --request POST \ @@ -500,16 +500,16 @@ curl \ ``` -Ответ: +Response: ``` -Код ответа: 200 +Response code: 200 -Тело ответа: +Response body: OK ``` -Теперь попробуем его получить. +Now let's try to retrieve it. ``` curl \ --request POST \ @@ -521,13 +521,13 @@ curl \ ``` -Тут всё хорошо и мы получили то, что записали. +Here everything is fine, and we got what we wrote. -Ответ: +Response: ``` -Код ответа: 200 +Response code: 200 -Тело ответа: +Response body: { "bad_id" : "bad1", "version" : 1.0 @@ -535,7 +535,7 @@ curl \ ``` -Теперь еще раз отправим объект с таким же `bad_id` +Now we send the object with the same `bad_id` again ``` curl \ --request POST \ @@ -548,16 +548,16 @@ curl \ ``` -Ответ: +Response: ``` -Код ответа: 200 +Response code: 200 -Тело ответа: +Response body: OK ``` -И снова попробуем его получить. +And try to retrieve it again. ``` curl \ --request POST \ @@ -569,18 +569,18 @@ curl \ ``` -А вот тут уже ошибка +And here we encounter an error -Ответ: +Response: ``` -Код ответа: 400 +Response code: 400 -Тело ответа: -ru.tinkoff.tcb.mockingbird.error.StubSearchError: Для одной или нескольких заглушек найдено более одного подходящего состояния +Response body: +ru.tinkoff.tcb.mockingbird.error.StubSearchError: For one or more stubs, multiple suitable states were found ``` -Для проверки состояний подходящих для под заданное условие, можно выполнить следующий запрос. +To check for states that fit the given condition, one can perform the following request. ``` curl \ --request POST \ @@ -594,13 +594,13 @@ curl \ ``` -В результате будет два объекта +As a result, there will be two objects -Ответ: +Response: ``` -Код ответа: 200 +Response code: 200 -Тело ответа: +Response body: [ { "id" : "7d81f74b-968b-4737-8ebe-b0592d4fb89b", @@ -626,6 +626,6 @@ curl \ ``` -Ручка `/api/internal/mockingbird/fetchStates` возвращает состояния в там виде -как они хранятся в БД, присутствуют поля `id`, `created`, а записанное состояние -хранится в поле `data`. +The `/api/internal/mockingbird/fetchStates` endpoint returns states as +they are stored in the database, with fields `id`, `created`, and the recorded state +stored in the `data` field. diff --git a/message-brokers.md b/message-brokers.md index 23305685..8de53e81 100644 --- a/message-brokers.md +++ b/message-brokers.md @@ -1,273 +1,270 @@ -# Работа с очередями - -Mockingbird взаимодействует с брокерами сообщений через HTTP API, благодаря чему теоретически поддерживаются любые возможные MQ. -На практике некоторе брокеры требуют установки дополнительных sidecar-коробок, как, например, HTTP-Bridge для WebsphereMQ или rest-proxy для Kafka. -Здесь мы рассмотрим примеры настройки mockingbird для работы с различными MQ. -Примеры ниже являются шаблонами, по которым можно самостоятельно настроить Mockingbird через UI - -## RabbitMQ - -RabbitMQ имеет встроенный rest API, благодаря чему установка доплнительных решений не требуется. - -Официальная документация: https://www.rabbitmq.com/management.html#http-api - -Пример конфигурации source (mockingbird читает из очереди): -``` -Запрос: - -{ - "body": "{\"count\":1,\"ackmode\":\"ack_requeue_false\",\"encoding\":\"auto\"}", - "headers": { - "Authorization": "Basic xxxxxxx" - }, - "jenumerate": "$", - "jextract": "payload", - "jstringdecode": true, - "method": "POST", - "url": "http://:15672/api/queues///get" -} -``` - -Назначение большинства полей понятно из названий, но назначение некоторых поля стоит раскрыть подробно: -- `jenumerate` - наличие этого поля означает, что в ответе метода может быть несколько сообщений, значение представляет собой путь до поля с массивом. В данном случае массив находится непосредственно в корне ответа -- `jextract` - путь до содержимого сообщения в ответе. В данном случае это поле `payload` -- `jstringdecode` - признак того, что сообщение является json-строкой, в которой содержится экранированый JSON. При установке jstringdecode в true этот JSON будет распаршен - -Использование этих трёх полей имеет смысл только для API, возвращающих JSON и может в ином случае приводить к ошибкам - -Пример конфигурации destination (mockingbird пишет в очередь): -``` -Запрос: - -{ - "body": { - "payload": "${_message}", // сюда подставляется ответ, который сформировал мок - "payload_encoding": "string", - "properties": {}, - "routing_key": "" - }, - "headers": { - "Authorization": "Basic xxxxxxx" - }, - "method": "POST", - "stringifybody": true, - "url": "http://:15672/api/exchanges///publish" -} -``` - -Назначение большинства полей понятно из названий, но назначение некоторых поля стоит раскрыть подробно: -- `stringifybody` - означает, что ответ, сформированый моком, нужно заэкранировать и передать в шаблонизатор как JSON строку - -## WebsphereMQ - -Для работы с WebsphereMQ требуется установка [IBM MQ bridge for HTTP](https://www.ibm.com/docs/en/ibm-mq/8.0?topic=mq-bridge-http) - -Пример конфигурации source (mockingbird читает из очереди): -``` -Запрос: - -{ - "bypassCodes": [504], - "headers": { - "Authorization": "Basic xxxxxxx" - }, - "method": "DELETE", - "url": "http://:8080/WMQHTTP2/msg/queue//" -} -``` - -Назначение большинства полей понятно из названий, но назначение некоторых поля стоит раскрыть подробно: -- `bypassCodes` - коды ответа сервера, которые не следует считать ошибочными. В данном случае 504 это признак отсутсвия сообщений, что является нормой - -Пример конфигурации destination (mockingbird пишет в очередь): -``` -Запрос: - -{ - "headers": { - "Authorization": "Basic xxxxxxx", - "Content-Type": "text/xml", - "x-msg-class": "TEXT" - }, - "method": "POST", - "url": "http://:8080/WMQHTTP2/msg/queue//" -} -``` - -## Kafka - -Для работы с Kafka требуется установить и настроить [Kafka REST Proxy](https://github.com/confluentinc/kafka-rest) - -Чтение из топиков кафки с помощью Kafka REST Proxy требует дополнительного созданию (и удаления) консьюмеров и подписок, для этого предусмотрены блоки Init и Shutdown. - -`` и `` - произвольные уникальные в рамках конфига имена - -Пример конфигурации source (mockingbird читает JSON из топика): -``` -Запрос: - -{ - "headers": { - "Accept": "application/vnd.kafka.json.v2+json" - }, - "jenumerate": "$", - "jextract": "value", - "method": "GET", - "url": "http:///consumers//instances//records" -} - -Init: - -[ - { - "body": "{\"name\": \"\", \"format\": \"json\", \"auto.offset.reset\": \"earliest\"}", - "headers": { - "Content-Type": "application/vnd.kafka.v2+json" - }, - "method": "POST", - "url": "http:///consumers/" - }, - { - "body": "{\"topics\":[\"\"]}", - "headers": { - "Content-Type": "application/vnd.kafka.v2+json" - }, - "method": "POST", - "url": "http:///consumers//instances//subscription" - } -] - -Shutdown: - -[ - { - "method": "DELETE", - "url": "http:///consumers//instances/" - } -] - -ReInit triggers: - -[ - { - "mode": "json", - "code": 404, - "body": {"error_code":40403,"message":"Consumer instance not found."} - } -] -``` - -Пример конфигурации source (mockingbird читает Avro из топика): -``` -Запрос: - -{ - "headers": { - "Accept": "application/vnd.kafka.avro.v2+json" - }, - "jenumerate": "$", - "jextract": "value", - "method": "GET", - "url": "http:///consumers//instances//records" -} - -Init: - -[ - { - "body": "{\"name\": \"\", \"format\": \"avro\", \"auto.offset.reset\": \"earliest\"}", - "headers": { - "Content-Type": "application/vnd.kafka.v2+json" - }, - "method": "POST", - "url": "http:///consumers/" - }, - { - "body": "{\"topics\":[\"\"]}", - "headers": { - "Content-Type": "application/vnd.kafka.v2+json" - }, - "method": "POST", - "url": "http:///consumers//instances//subscription" - } -] - -Shutdown: - -[ - { - "method": "DELETE", - "url": "http:///consumers//instances/" - } -] - -ReInit triggers: - -[ - { - "mode": "json", - "code": 404, - "body": {"error_code":40403,"message":"Consumer instance not found."} - } -] -``` - -Назначение большинства полей понятно из названий, но назначение некоторых поля стоит раскрыть подробно: -- `jenumerate` - наличие этого поля означает, что в ответе метода может быть несколько сообщений, значение представляет собой путь до поля с массивом. В данном случае массив находится непосредственно в корне ответа -- `jextract` - путь до содержимого сообщения в ответе. В данном случае это поле `value` - -kafka-rest-proxy на данный момент (май 2022) [не поддерживает](https://github.com/confluentinc/kafka-rest/issues/620) топики, в которых сообщение сериализуется в Avro, а ключ - нет - -Пример конфигурации destination (mockingbird пишет JSON в топик): -``` -Запрос: - -{ - "body": { - "records": [ - { - "value": "${_message}" //сюда подставляется ответ, который сформировал мок - } - ] - }, - "headers": { - "Content-Type": "application/vnd.kafka.json.v2+json" - }, - "method": "POST", - "url": "http:///topics/" -} -``` - -Пример конфигурации destination (mockingbird пишет Avro в топик): -``` -Запрос: - -{ - "body": { - "key_schema_id": , - "records": [ - { - "key": "${_message.key}", - "value": "${_message.value}" - } - ], - "value_schema_id": - }, - "headers": { - "Content-Type": "application/vnd.kafka.avro.v2+json" - }, - "method": "POST", - "url": "http:///topics/" -} -``` - -Дополнительные пояснения: -данный пример предполагает, что ответ мока выглядит следующим образом: -``` -{ - "key": <содержимое ключа>, - "value": <содержимое сообщения> -} -``` - -kafka-rest-proxy на данный момент (май 2022) [не поддерживает](https://github.com/confluentinc/kafka-rest/issues/620) топики, в которых сообщение сериализуется в Avro, а ключ - нет \ No newline at end of file +# Working with Queues + +Mockingbird interacts with message brokers through an HTTP API, theoretically supporting any possible MQ. In practice, some brokers require the installation of additional sidecar solutions, such as an HTTP-Bridge for WebsphereMQ or a rest-proxy for Kafka. Here, we will look at examples of configuring Mockingbird to work with various MQs. The examples below are templates that you can use to configure Mockingbird through the UI. + +## RabbitMQ + +RabbitMQ has a built-in rest API, eliminating the need for additional solutions. + +Official documentation: https://www.rabbitmq.com/management.html#http-api + +Example configuration for source (mockingbird reads from a queue): +``` +Request: + +{ + "body": "{\"count\":1,\"ackmode\":\"ack_requeue_false\",\"encoding\":\"auto\"}", + "headers": { + "Authorization": "Basic xxxxxxx" + }, + "jenumerate": "$", + "jextract": "payload", + "jstringdecode": true, + "method": "POST", + "url": "http://:15672/api/queues///get" +} +``` + +The purpose of most fields is clear from their names, but some require detailed explanation: +- `jenumerate` - the presence of this field means that the method response can contain multiple messages, and the value represents the path to the array field. In this case, the array is directly at the root of the response. +- `jextract` - the path to the message content in the response. In this case, it is the `payload` field. +- `jstringdecode` - indicates that the message is a json-string containing escaped JSON. Setting `jstringdecode` to true will parse this JSON. + +Using these three fields makes sense only for APIs that return JSON and may otherwise lead to errors. + +Example configuration for destination (mockingbird writes to a queue): +``` +Request: + +{ + "body": { + "payload": "${_message}", // here, the mock's response is inserted + "payload_encoding": "string", + "properties": {}, + "routing_key": "" + }, + "headers": { + "Authorization": "Basic xxxxxxx" + }, + "method": "POST", + "stringifybody": true, + "url": "http://:15672/api/exchanges///publish" +} +``` + +The purpose of most fields is clear from their names, but some require detailed explanation: +- `stringifybody` - means that the mock's response should be escaped and passed to the templating engine as a JSON string. + +## WebsphereMQ + +Working with WebsphereMQ requires the installation of [IBM MQ bridge for HTTP](https://www.ibm.com/docs/en/ibm-mq/8.0?topic=mq-bridge-http). + +Example configuration for source (mockingbird reads from a queue): +``` +Request: + +{ + "bypassCodes": [504], + "headers": { + "Authorization": "Basic xxxxxxx" + }, + "method": "DELETE", + "url": "http://:8080/WMQHTTP2/msg/queue//" +} +``` + +The purpose of most fields is clear from their names, but some require detailed explanation: +- `bypassCodes` - server response codes that should not be considered errors. In this case, 504 indicates no messages, which is normal. + +Example configuration for destination (mockingbird writes to a queue): +``` +Request: + +{ + "headers": { + "Authorization": "Basic xxxxxxx", + "Content-Type": "text/xml", + "x-msg-class": "TEXT" + }, + "method": "POST", + "url": "http://:8080/WMQHTTP2/msg/queue//" +} +``` + +## Kafka + +Working with Kafka requires the installation and configuration of the [Kafka REST Proxy](https://github.com/confluentinc/kafka-rest). + +Reading from Kafka topics via the Kafka REST Proxy requires additional creation (and deletion) of consumers and subscriptions, for which the Init and Shutdown blocks are provided. + +`` and `` are arbitrary unique names within the config. + +Example configuration for source (mockingbird reads JSON from a topic): +``` +Request: + +{ + "headers": { + "Accept": "application/vnd.kafka.json.v2+json" + }, + "jenumerate": "$", + "jextract": "value", + "method": "GET", + "url": "http:///consumers//instances//records" +} + +Init: + +[ + { + "body": "{\"name\": \"\", \"format\": \"json\", \"auto.offset.reset\": \"earliest\"}", + "headers": { + "Content-Type": "application/vnd.kafka.v2+json" + }, + "method": "POST", + "url": "http:///consumers/" + }, + { + "body": "{\"topics\":[\"\"]}", + "headers": { + "Content-Type": "application/vnd.kafka.v2+json" + }, + "method": "POST", + "url": "http:///consumers//instances//subscription" + } +] + +Shutdown: + +[ + { + "method": "DELETE", + "url": "http:///consumers//instances/" + } +] + +ReInit triggers: + +[ + { + "mode": "json", + "code": 404, + "body": {"error_code":40403,"message":"Consumer instance not found."} + } +] +``` + +Example configuration for source (mockingbird reads Avro from a topic): +``` +Request: + +{ + "headers": { + "Accept": "application/vnd.kafka.avro.v2+json" + }, + "jenumerate": "$", + "jextract": "value", + "method": "GET", + "url": "http:///consumers//instances//records" +} + +Init: + +[ + { + "body": "{\"name\": \"\", \"format\": \"avro\", \"auto.offset.reset\": \"earliest\"}", + "headers": { + "Content-Type": "application/vnd.kafka.v2+json" + }, + "method": "POST", + "url": "http:///consumers/" + }, + { + "body": "{\"topics\":[\"\"]}", + "headers": { + "Content-Type": "application/vnd.kafka.v2+json" + }, + "method": "POST", + "url": "http:///consumers//instances//subscription" + } +] + +Shutdown: + +[ + { + "method": "DELETE", + "url": "http:///consumers//instances/" + } +] + +ReInit triggers: + +[ + { + "mode": "json", + "code": 404, + "body": {"error_code":40403,"message":"Consumer instance not found."} + } +] +``` + +The purpose of most fields is clear from their names, but the purpose of some fields is worth detailing: +- `jenumerate` - the presence of this field means that the method response can contain multiple messages, with the value representing the path to the array field. In this case, the array is directly at the root of the response. +- `jextract` - the path to the content of the message in the response. In this case, it is the `value` field. + +As of May 2022, kafka-rest-proxy [does not support](https://github.com/confluentinc/kafka-rest/issues/620) topics in which the message is serialized in Avro but the key is not. + +Example configuration for destination (mockingbird writes JSON to a topic): +``` +Request: + +{ + "body": { + "records": [ + { + "value": "${_message}" // here, the mock's response is inserted + } + ] + }, + "headers": { + "Content-Type": "application/vnd.kafka.json.v2+json" + }, + "method": "POST", + "url": "http:///topics/" +} +``` + +Example configuration for destination (mockingbird writes Avro to a topic): +``` +Request: + +{ + "body": { + "key_schema_id": , + "records": [ + { + "key": "${_message.key}", + "value": "${_message.value}" + } + ], + "value_schema_id": + }, + "headers": { + "Content-Type": "application/vnd.kafka.avro.v2+json" + }, + "method": "POST", + "url": "http:///topics/" +} +``` + +Additional explanations: +This example assumes that the mock's response looks as follows: +``` +{ + "key": , + "value": +} +``` + +As of May 2022, kafka-rest-proxy [does not support](https://github.com/confluentinc/kafka-rest/issues/620) topics in which the message is serialized in Avro but the key is not. diff --git a/message-brokers_ru.md b/message-brokers_ru.md new file mode 100644 index 00000000..23305685 --- /dev/null +++ b/message-brokers_ru.md @@ -0,0 +1,273 @@ +# Работа с очередями + +Mockingbird взаимодействует с брокерами сообщений через HTTP API, благодаря чему теоретически поддерживаются любые возможные MQ. +На практике некоторе брокеры требуют установки дополнительных sidecar-коробок, как, например, HTTP-Bridge для WebsphereMQ или rest-proxy для Kafka. +Здесь мы рассмотрим примеры настройки mockingbird для работы с различными MQ. +Примеры ниже являются шаблонами, по которым можно самостоятельно настроить Mockingbird через UI + +## RabbitMQ + +RabbitMQ имеет встроенный rest API, благодаря чему установка доплнительных решений не требуется. + +Официальная документация: https://www.rabbitmq.com/management.html#http-api + +Пример конфигурации source (mockingbird читает из очереди): +``` +Запрос: + +{ + "body": "{\"count\":1,\"ackmode\":\"ack_requeue_false\",\"encoding\":\"auto\"}", + "headers": { + "Authorization": "Basic xxxxxxx" + }, + "jenumerate": "$", + "jextract": "payload", + "jstringdecode": true, + "method": "POST", + "url": "http://:15672/api/queues///get" +} +``` + +Назначение большинства полей понятно из названий, но назначение некоторых поля стоит раскрыть подробно: +- `jenumerate` - наличие этого поля означает, что в ответе метода может быть несколько сообщений, значение представляет собой путь до поля с массивом. В данном случае массив находится непосредственно в корне ответа +- `jextract` - путь до содержимого сообщения в ответе. В данном случае это поле `payload` +- `jstringdecode` - признак того, что сообщение является json-строкой, в которой содержится экранированый JSON. При установке jstringdecode в true этот JSON будет распаршен + +Использование этих трёх полей имеет смысл только для API, возвращающих JSON и может в ином случае приводить к ошибкам + +Пример конфигурации destination (mockingbird пишет в очередь): +``` +Запрос: + +{ + "body": { + "payload": "${_message}", // сюда подставляется ответ, который сформировал мок + "payload_encoding": "string", + "properties": {}, + "routing_key": "" + }, + "headers": { + "Authorization": "Basic xxxxxxx" + }, + "method": "POST", + "stringifybody": true, + "url": "http://:15672/api/exchanges///publish" +} +``` + +Назначение большинства полей понятно из названий, но назначение некоторых поля стоит раскрыть подробно: +- `stringifybody` - означает, что ответ, сформированый моком, нужно заэкранировать и передать в шаблонизатор как JSON строку + +## WebsphereMQ + +Для работы с WebsphereMQ требуется установка [IBM MQ bridge for HTTP](https://www.ibm.com/docs/en/ibm-mq/8.0?topic=mq-bridge-http) + +Пример конфигурации source (mockingbird читает из очереди): +``` +Запрос: + +{ + "bypassCodes": [504], + "headers": { + "Authorization": "Basic xxxxxxx" + }, + "method": "DELETE", + "url": "http://:8080/WMQHTTP2/msg/queue//" +} +``` + +Назначение большинства полей понятно из названий, но назначение некоторых поля стоит раскрыть подробно: +- `bypassCodes` - коды ответа сервера, которые не следует считать ошибочными. В данном случае 504 это признак отсутсвия сообщений, что является нормой + +Пример конфигурации destination (mockingbird пишет в очередь): +``` +Запрос: + +{ + "headers": { + "Authorization": "Basic xxxxxxx", + "Content-Type": "text/xml", + "x-msg-class": "TEXT" + }, + "method": "POST", + "url": "http://:8080/WMQHTTP2/msg/queue//" +} +``` + +## Kafka + +Для работы с Kafka требуется установить и настроить [Kafka REST Proxy](https://github.com/confluentinc/kafka-rest) + +Чтение из топиков кафки с помощью Kafka REST Proxy требует дополнительного созданию (и удаления) консьюмеров и подписок, для этого предусмотрены блоки Init и Shutdown. + +`` и `` - произвольные уникальные в рамках конфига имена + +Пример конфигурации source (mockingbird читает JSON из топика): +``` +Запрос: + +{ + "headers": { + "Accept": "application/vnd.kafka.json.v2+json" + }, + "jenumerate": "$", + "jextract": "value", + "method": "GET", + "url": "http:///consumers//instances//records" +} + +Init: + +[ + { + "body": "{\"name\": \"\", \"format\": \"json\", \"auto.offset.reset\": \"earliest\"}", + "headers": { + "Content-Type": "application/vnd.kafka.v2+json" + }, + "method": "POST", + "url": "http:///consumers/" + }, + { + "body": "{\"topics\":[\"\"]}", + "headers": { + "Content-Type": "application/vnd.kafka.v2+json" + }, + "method": "POST", + "url": "http:///consumers//instances//subscription" + } +] + +Shutdown: + +[ + { + "method": "DELETE", + "url": "http:///consumers//instances/" + } +] + +ReInit triggers: + +[ + { + "mode": "json", + "code": 404, + "body": {"error_code":40403,"message":"Consumer instance not found."} + } +] +``` + +Пример конфигурации source (mockingbird читает Avro из топика): +``` +Запрос: + +{ + "headers": { + "Accept": "application/vnd.kafka.avro.v2+json" + }, + "jenumerate": "$", + "jextract": "value", + "method": "GET", + "url": "http:///consumers//instances//records" +} + +Init: + +[ + { + "body": "{\"name\": \"\", \"format\": \"avro\", \"auto.offset.reset\": \"earliest\"}", + "headers": { + "Content-Type": "application/vnd.kafka.v2+json" + }, + "method": "POST", + "url": "http:///consumers/" + }, + { + "body": "{\"topics\":[\"\"]}", + "headers": { + "Content-Type": "application/vnd.kafka.v2+json" + }, + "method": "POST", + "url": "http:///consumers//instances//subscription" + } +] + +Shutdown: + +[ + { + "method": "DELETE", + "url": "http:///consumers//instances/" + } +] + +ReInit triggers: + +[ + { + "mode": "json", + "code": 404, + "body": {"error_code":40403,"message":"Consumer instance not found."} + } +] +``` + +Назначение большинства полей понятно из названий, но назначение некоторых поля стоит раскрыть подробно: +- `jenumerate` - наличие этого поля означает, что в ответе метода может быть несколько сообщений, значение представляет собой путь до поля с массивом. В данном случае массив находится непосредственно в корне ответа +- `jextract` - путь до содержимого сообщения в ответе. В данном случае это поле `value` + +kafka-rest-proxy на данный момент (май 2022) [не поддерживает](https://github.com/confluentinc/kafka-rest/issues/620) топики, в которых сообщение сериализуется в Avro, а ключ - нет + +Пример конфигурации destination (mockingbird пишет JSON в топик): +``` +Запрос: + +{ + "body": { + "records": [ + { + "value": "${_message}" //сюда подставляется ответ, который сформировал мок + } + ] + }, + "headers": { + "Content-Type": "application/vnd.kafka.json.v2+json" + }, + "method": "POST", + "url": "http:///topics/" +} +``` + +Пример конфигурации destination (mockingbird пишет Avro в топик): +``` +Запрос: + +{ + "body": { + "key_schema_id": , + "records": [ + { + "key": "${_message.key}", + "value": "${_message.value}" + } + ], + "value_schema_id": + }, + "headers": { + "Content-Type": "application/vnd.kafka.avro.v2+json" + }, + "method": "POST", + "url": "http:///topics/" +} +``` + +Дополнительные пояснения: +данный пример предполагает, что ответ мока выглядит следующим образом: +``` +{ + "key": <содержимое ключа>, + "value": <содержимое сообщения> +} +``` + +kafka-rest-proxy на данный момент (май 2022) [не поддерживает](https://github.com/confluentinc/kafka-rest/issues/620) топики, в которых сообщение сериализуется в Avro, а ключ - нет \ No newline at end of file diff --git a/readme.md b/readme.md index 9f9aa357..1fe3aeca 100644 --- a/readme.md +++ b/readme.md @@ -1,767 +1,759 @@ - - -# mockingbird - -mockingbird - сервис эмуляции REST-сервисов и сервисов с интерфейсами-очередями - -[Руководство по инсталляции](deployment.md) - -[Руководство по настройке](configuration.md) - -[Работа с очередями](message-brokers.md) - -## Общие принципы работы - -mockingbird поддерживает следующие сценарии: - -* прогон конкретного кейса с конкретным набором событий и HTTP/GRPC ответов -* постоянная имитация happy-path для обеспечения автономности контура(ов) - -Типы конфигураций: -* countdown - автономные конфигурации для тестирования конкретного сценария. Имеют наивысший приоритет при разрешении неоднозначностей. Каждый мок срабатывает n раз (количество задаётся при создании). Автоматически удаляются в полночь. -* ephemeral - конфигурации, автоматически удаляемые в полночь. Если одновременно вызывают метод/приходит сообщение, для которого подходит countdown и ephemeral моки - сработает countdown. -* persistent - конфигурация, предназначеная для постоянной работы. Имеет наименьший приоритет - ->Пример небольшого кейса (короткая заявка) - в конце спецификации - -## Сервисы - -Для упорядочения моков в UI и минимизации количества конфликтных ситуаций в mockingbird реализованы т.н. сервисы. Каждый мок (как HTTP так и сценарий) всегда принадлежит к какому-то из сервисом. -Сервисы создаются заранее и хранятся в базе. Сервис имеет suffix (являющийся по совместительству уникальным id сервиса) и человекочитаемый name. - -## Шаблонизатор JSON - -Для достижения гибкости при сохранении относительной простоты конфигов в сервисе реализован JSON шаблонизатор. Для начала простой пример: - -Шаблон: -```javascript -{ - "description": "${description}", - "topic": "${extras.topic}", - "comment": "${extras.comments.[0].text}", - "meta": { - "field1": "${extras.fields.[0]}" - } -} -``` - -Значения для подстановки: -```javascript -{ - "description": "Some description", - "extras": { - "fields": ["f1", "f2"], - "topic": "Main topic", - "comments": [ - {"text": "First nah!"}, {"text": "Okay"} - ] - } -} -``` - -Результат: -```javascript -{ - "description": "Some description", - "topic": "Main topic", - "comment": "First nah!", - "meta": { - "field1": "f1" - } -} -``` - -В данный момент поддерживается следующий синтаксис: -* `${a.[0].b}` - подстановка значения (JSON) -* `${/a/b/c}` - подстановка значения (XPath) - -ВНИМАНИЕ! НЕ ИСПОЛЬЗУЙТЕ НЕЙМСПЕЙСЫ В XPATH ВЫРАЖЕНИЯХ - -## Шаблонизатор XML - -Шаблон: -``` - - ${/r/t1} - ${/r/t2} - -``` - -Значения для подстановки: -``` - - test - 42 - -``` - -Результат: -``` - - test - 42 - -``` - -## Состояния (state) - -Для поддержки сложных сценариев сервис поддерживает сохранение произвольных состояний. Состояние - документ с произвольной схемой, технически состояние - документ в mongodb. Запись новых состояний может происходить: -* при записи в state (секция persist) с пустым (или отсутствующим) предикатом (секция state) - -## Манипуляции со state - -State аккумулятивно дописывается. Разрешено переписывание полей. - -Поля, по которым будем производиться поиск (используемые в предикатах) должны начинаться с "_". -> для таких полей будет автоматически создаваться sparse индекс - -Префиксы: -* `seed` - значения из блока seed (рандомизируемые на старте заявки) -* `state` - текущий state -* `req` - тело запроса (режимы json, jlens, xpath) -* `message` - тело собщения (в сценариях) -* `query` - query параметры (в заглушках) -* `pathParts` - значения, извлекаемые из URL (в заглушках) см. `Экстрация данных из URL` -* `extracted` - извлечённые значения -* `headers` - HTTP заголовки - -```javascript -{ - "a": "Просто строка", //В поле "a" записывается константа (может быть любое JSON значение) - "b": "${req.fieldB}", //В поле "b" записывается значение из поля fieldB запроса - "c": "${state.c}", //В поле "c" записывается значение из поля "c" текущего состояния - "d": "${req.fieldA}: ${state.a}" //В поле d запишется строка, содержащая req.fieldA и state.a -} -``` - -## Поиск state - -Предикаты для поиска state перечисляются в блоке `state`. Пустой объект (`{}`) в поле state недопустим. -Для поиска state можно использовать данные запроса (без префикса), query параметры (префикс `__query`), значения, извлекаемые из URL (префикс `__segments`) и HTTP заголовки (префикс `__headers`) - -Пример: - -```javascript -{ - "_a": "${fieldB}", //поле из тела запроса - "_b": "${__query.arg1}", //query параметр - "_c": "${__segments.id}", //сегмент URL, см. `Экстрация данных из URL` - "_d": "${__headers.Accept}" //HTTP заголовок -} -``` - - -## Seeding - -Иногда возникает необходимость сгенерировать случайное значение и сохранить и/или вернуть его в результате работы мока. -Для поддержки таких сценариев сделано поле seed, позволяющее задать переменные, которые будут сгенерированы -при инициализации мока. Это позволяет избежать необходимости пересоздавать моки с захардкожеными id - -В seed'ах поддерживается выполнение JavaScript, для поддержки обратной совместимости с синтаксисом псевдофункций определены следующие функции: -* `%{randomString(n)}` - подстановка случайной строки длиной n -* `%{randomString("ABCDEF1234567890", m, n)}` - подстановка случайной строки, состоящей из символов `ABCDEF1234567890` длиной в интервале [m, n) -* `%{randomNumericString(n)}` - подстановка случайной строки, состоящей только из цифр, длиной n -* `%{randomInt(n)}` - подстановка случайного Int в диапазоне [0, n) -* `%{randomInt(m,n)}` - подстановка случайного Int в диапазоне [m, n) -* `%{randomLong(n)}` - подстановка случайного Long в диапазоне [0, n) -* `%{randomLong(m,n)}` - подстановка случайного Long в диапазоне [m, n) -* `%{UUID()}` - подстановка случайного UUID -* `%{now("yyyy-MM-dd'T'HH:mm:ss")}` - текущее время в заданном [формате](https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html) -* `%{today("yyyy-MM-dd")}` - текущая дата в заданном [формате](https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html) - -Можно определять строки со сложным форматом: `%{randomInt(10) +': ' + randomLong(10) + ' | ' + {randomString(12)}`, поддерживаются все псевдофункции из списка выше - -## Резолвинг заглушек/сценариев - -> Найденые заглушки - кандидаты, оставшиеся после валидации URL, заголовков и тела запроса -> Найденые сценарии - кандидаты, оставшиеся после валидации тела сообщения - -| Найденые заглушки (сценарии) | Требуется состояние | Найдено состояний | Результат | -| --------------------------- | ------------------- | ----------------- | --------- | -| №1 | нет | - | Сработает №1 | -| №1 | да | 0 | Ошибка | -| №1 | да | 1 | Сработает №1 | -| №1
№2 | нет
нет | - | Ошибка | -| №1
№2 | нет
да | -
0 | Сработает №1 | -| №1
№2 | нет
да | -
1 | Сработает №2 | -| №1
№2 | нет
да | -
2 (и более) | Ошибка | -| №1
№2 | да
да | 0
0 | Ошибка | -| №1
№2 | да
да | 0
1 | Сработает №2 | -| №1
№2 | да
да | 0
2 (и более) | Ошибка | -| №1
№2 | да
да | 1
1 (и более) | Ошибка | -| №1
№2
№3 | да
да
да | 0
1
0 | Сработает №2 | -| №1
№2
№3 | да
да
да | 0
1
1 | Ошибка | -| №1
№2
№3 | да
да
да | 0
2
0 | Ошибка | - -## Эмуляция REST сервисов - -Алгоритм работы: - -1. Поиск мока по URL/HTTP-verb/заголовков -2. Валидация body -3. Поиск state по предикату -4. Подстановка значений в шаблон ответа -5. Модификация state -6. Отдача response - -### Конфигурация HTTP заглушек - -HTTP заголовки валидируются на полное соответствие значений, лишние заголовки не являются ошибкой - -Валидация тела запросы в HTTP заглушках может работать в следующих режимах: -* no_body - запрос должен быть без тела -* any_body - тело запроса должно быть не пустым, при этом никак не парсится и не проверяется -* raw - тело запроса никак не парсится и проверяется на полное соответствие с содержимым request.body -* json - тело запроса должно быть валидным JSON'ом и проверяется на соответствие с содержимым request.body -* xml - тело запроса должно быть валидным XML и проверяется на соответствие с содержимым request.body -* jlens - тело запроса должно быть валидным JSON'ом и валидируется по условиям, описаным в request.body -* xpath - тело запроса должно быть валидным XML и валидируется по условиям, описаным в request.body -* web_form - тело запроса должно быть в формате x-www-form-urlencoded и валидируется по условиям, описаным в request.body -* multipart - тело запроса должно быть в формате multipart/form-data. Правила валидации частей конфигурируются индивидуально (см. раздел ниже) - -ВНИМАНИЕ! multipart запросы необходимо выполнять на отдельный метод - -/api/mockingbird/execmp - -Для ответов поддерживаются следующие режимы: -* raw -* json -* xml -* binary -* proxy -* json-proxy -* xml-proxy -* no_body - -Режим `no_body` в ответе нужен если заглушка возвращает код 204 или 304. Данные коды выделяются от остальных тем, что у них не может быть никакого тела в ответе, данное поведение описано в [RFC 7231](https://datatracker.ietf.org/doc/html/rfc7231#section-6.3.5) и [RFC 7232](https://datatracker.ietf.org/doc/html/rfc7232#section-4.1). Режим `no_body` можно использовать и с остальными HTTP кодами, но для указных он является обязательным. - -Режимы request и response полностью независимы друг от друга (можно сконфигурировать ответ xml'ем на json запрос при желании, кроме режимов json-proxy и xml-proxy). - -В поле delay можно передать корректный FiniteDuration не дольше 30 секунд - -### Экстрация данных из URL -Бывает, что URL содержит какой-нибудь идентификатор не как параметр, а как непосредственно часть пути. В таких случаях становится невозможным -описать persistent заглушку из-за невозможности полного совпадения пути. На помощь приходит поле pathPattern, в которое можно передать регулярку, -на соответствие которой будет проверяться путь. Отмечу, что хоть сопоставление и производится в монге эффективным способом, злоупотребять этой -возможностью не стоит и при возможности сопоставления по полному совпадению не следует использовать pathPattern - -Пример: -```javascript -{ - "name": "Sample stub", - "scope": "persistent", - "pathPattern": "/pattern/(?\d+)", - "method": "GET", - "request": { - "headers": {}, - "mode": "no_body", - "body": {} - }, - "response": { - "code": 200, - "mode": "json", - "headers": {"Content-Type": "application/json"}, - "body": {"id": "${pathParts.id}"} - } -} -``` -То, что нужно извлечь из пути, нужно делать _именованой_ группой, групп может быть сколько угодно, впоследствии на них можно ссылаться через `pathParts.<имя_группы>` - -### Экстракторы -В некоторых случаях нужно подставить в ответ данные, которые невозможно извлечь простыми средствами. Для этих целей были добавлены экстракторы - -#### Экстрактор xcdata - -Достаёт значения из XML, лежащего в CDATA - -конфигурация: -```javascript -{ - "type": "xcdata", - "prefix": "/root/inner/tag", //Путь до тэга с CDATA - "path": "/path/to" //Путь до нужного тэга -} -``` - -#### Экстрактор jcdata - -Достаёт значения из JSON, лежащего в CDATA - -конфигурация: -```javascript -{ - "type": "jcdata", - "prefix": "/root/inner/tag", //Путь до тэга с CDATA - "path": "path.to" //Путь до нужного значения -} -``` - -#### CDATA inlining -Иногда приходится иметь дело с запросами, в которых внутри CDATA лежит XML. В таких случаях можно заинлайнить содержимое DATA с помощью параметра `inlineCData` (поддерживается в `xpath` и `xml`) - -### Примеры - -#### Полное совпадение, режим json - -```javascript -{ - "name": "Sample stub", - "method": "POST", - "path": "/pos-loans/api/cl/get_partner_lead_info", - "state": { - // Предикаты - }, - "request": { - "headers": {"Content-Type": "application/json"}, - "mode": "json", - "body": { - "trace_id": "42", - "account_number": "228" - } - }, - "persist": { - // Модификации состояния - }, - "response": { - "code": 200, - "mode": "json", - "body": { - "code": 0, - "credit_amount": 802400, - "credit_term": 120, - "interest_rate": 13.9, - "partnum": "CL3.15" - }, - "headers": {"Content-Type": "application/json"}, - "delay": "1 second" - } -} -``` - -#### Полное совпадение, режим raw - -```javascript -{ - "name": "Sample stub", - "method": "POST", - "path": "/pos-loans/api/evil/soap/service" - "state": { - // Предикаты - }, - "request": { - "headers": {"Content-Type": "application/xml"}, - "mode": "raw" - "body": "" - }, - "persist": { - // Модификации состояния - }, - "response": { - "code": 200, - "mode": "raw" - "body": "", - "headers": {"Content-Type": "application/xml"}, - "delay": "1 second" - } -} -``` - -#### Валидация по условиям, режим jlens - -```javascript -{ - "name": "Sample stub", - "method": "POST", - "path": "/pos-loans/api/cl/get_partner_lead_info", - "state": { - // Предикаты - }, - "request": { - "headers": {"Content-Type": "application/json"}, - "mode": "jlens", - "body": { - "meta.id": {"==": 42} - } - }, - "persist": { - // Модификации состояния - }, - "response": { - "code": 200, - "mode": "json", - "body": { - "code": 0, - "credit_amount": 802400, - "credit_term": 120, - "interest_rate": 13.9, - "partnum": "CL3.15" - }, - "headers": {"Content-Type": "application/json"}, - "delay": "1 second" - } -} -``` - -#### Валидация по условиям, режим xpath - -ВНИМАНИЕ! НЕ ИСПОЛЬЗУЙТЕ НЕЙМСПЕЙСЫ В XPATH ВЫРАЖЕНИЯХ - -```javascript -{ - "name": "Sample stub", - "method": "POST", - "path": "/pos-loans/api/cl/get_partner_lead_info", - "state": { - // Предикаты - }, - "request": { - "headers": {"Content-Type": "application/xml"}, - "mode": "xpath", - "body": { - "/payload/response/id": {"==": 42} - }, - "extractors": {"name": {...}, ...} //опционально - }, - "persist": { - // Модификации состояния - }, - "response": { - "code": 200, - "mode": "raw" - "body": "", - "headers": {"Content-Type": "application/xml"}, - "delay": "1 second" - } -} -``` - -#### Валидация по условиям, режим multipart - -ВНИМАНИЕ! multipart запросы необходимо выполнять на отдельный метод - -/api/mockingbird/execmp - -Режимы валидании part: -* `any` - значение никак не валидируется -* `raw` - полное соответствие -* `json` - полное соответствие, значение парсится как Json -* `xml` - полное соответствие, значение парсится как XML -* `urlencoded` - аналогично режиму `web_form` для валидации всего тела -* `jlens` - проверка Json по условиям -* `xpath` - проверка XML по условиям - -```javascript -{ - "name": "Sample stub", - "method": "POST", - "path": "/test/multipart", - "state": { - // Предикаты - }, - "request": { - "headers": {}, - "mode": "multipart", - "body": { - "part1": { - "mode": "json", //режим валидации - "headers": {}, //заголовки part - "value": {} //спецификация значения для валидатора - }, - "part2": { - ... - } - }, - "bypassUnknownParts": true //флаг, позволяющий игнорировать все partы, отсутвующие в спецификации валидатора - //по умолчанию флаг включен, можно передавать только для отключения (false) - }, - "persist": { - // Модификации состояния - }, - "response": { - "code": 200, - "mode": "json", - "body": { - "code": 0, - "credit_amount": 802400, - "credit_term": 120, - "interest_rate": 13.9, - "partnum": "CL3.15" - }, - "headers": {"Content-Type": "application/json"}, - "delay": "1 second" - } -} -``` - -#### Простое проксирование запроса - -```javascript -{ - "name": "Simple proxy", - "method": "POST", - "path": "/pos-loans/api/cl/get_partner_lead_info", - "state": { - // Предикаты - }, - "request": { - // Спецификация запроса - }, - "response": { - "mode": "proxy", - "uri": "http://some.host/api/cl/get_partner_lead_info" - } -} -``` - -#### Проксирование с модификацией JSON ответа - -```javascript -{ - "name": "Simple proxy", - "method": "POST", - "path": "/pos-loans/api/cl/get_partner_lead_info", - "state": { - // Предикаты - }, - "request": { - // Спецификация запроса, mode json или jlens - }, - "response": { - "mode": "json-proxy", - "uri": "http://some.host/api/cl/get_partner_lead_info", - "patch": { - "field.innerField": "${req.someRequestField}" - } - } -} -``` - -#### Проксирование с модификацией XML ответа - -```javascript -{ - "name": "Simple proxy", - "method": "POST", - "path": "/pos-loans/api/cl/get_partner_lead_info", - "state": { - // Предикаты - }, - "request": { - // Спецификация запроса, mode xml или xpath - }, - "response": { - "mode": "xml-proxy", - "uri": "http://some.host/api/cl/get_partner_lead_info", - "patch": { - "/env/someTag": "${/some/requestTag}" - } - } -} -``` - -### DSL предикатов валидации JSON и XML - -в режимах jlens и xpath поддерживается следующее: - -```javascript -{ - "a": {"==": "some value"}, //полное соответствие - "b": {"!=": "some value"}, //не равно - "c": {">": 42} | {">=": 42} | {"<": 42} | {"<=": 42}, //сравнения, только для чисел, комбинируются - "d": {"~=": "\d+"}, //сопоставление с regexp, - "e": {"size": 10}, //длина, для массивов и строк - "f": {"exists": true} //проверка существования -} -``` -Ключами в таких объектах является либо путь в json ("a.b.[0].c") либо xpath ("/a/b/c") -Замечание: в данный момент функции сравнения могут некорректно работать с xpath, указывающими на XML атрибуты. -Обойти проблему можно проверкой на существование/несуществование: -```/tag/otherTag/[@attr='2']": {"exists": true}``` - -в режиме jlens дополнительно поддерживаются следующие операции: -```javascript -{ - "g": {"[_]": ["1", 2, true]}, //поле должно содержать одно из перечисленых значений - "h": {"![_]": ["1", 2, true]}, //поле НЕ должно содержать ни одно из перечисленых знаечний - "i": {"&[_]": ["1", 2, true]} // поле должно быть массивом и содержать все перечисленные значения (при этом порядок не важен) -} -``` - -в режиме xpath дополнительно поддерживаются следующие операции: -```javascript - "/some/tag": {"cdata": {"==": "test"}}, //валидация на полное совпадение CDATA, аргумент должен быть СТРОКОЙ - "/some/tag": {"cdata": {"~=": "\d+"}}, //валидация DATA регуляркой, аргумент должен быть СТРОКОЙ - "/some/tag": {"jcdata": {"a": {"==": 42}}}, //валидируем содержимое CDATA как JSON, поддерживаются все доступные предикаты - "/other/tag": {"xcdata": {"/b": {"==": 42}}} //валидируем содержимое CDATA как XML, поддерживаются все доступные предикаты -``` - -в режиме web_form поддерживаются ТОЛЬКО следующие операции: -`==`, `!=`, `~=`, `size`, `[_]`, `![_]`, `&[_]` - -## Эмуляция GRPC сервисов - -Как это устроено под капотом: -При создании мока вложеные в запрос proto файлы парсятся и преобразуются в json-представление protobuf схемы. В базе хранится именно json-представление, -а не оригинальный proto файл. Первое срабатывание мока может занимать немного больше времени, чем последующие, т.к. при первом срабатывании из -json-представляения генерируется декодер protobuf сообщений. После декодирования данные преобразуются в json, который проверяется json-предикатами, -задаными в поле requestPredicates. Если условия выполняются - то json из response.data (в режиме fill) сериализуется в protobuf и отдаётся в качестве ответа. - -Алгоритм работы: - -1. Поиск мока(-ов) по имени метода -2. Валидация body -3. Поиск state по предикату -4. Подстановка значений в шаблон ответа -5. Модификация state -6. Отдача response - -### Конфигурация GRPC заглушек - -```javascript -{ - "name": "Sample stub", - "scope": "..", - "service": "test", - "methodName": "/pos-loans/api/cl/get_partner_lead_info", - "seed": { - "integrationId": "%{randomString(20)}" //пример - }, - "state": { - // Предикаты - }, - "requestCodecs": "..", //proto-файл схемы запроса в base64 - "requestClass": "..", //имя типа запроса из proto файла - "responseCodecs": "..", //proto-файл схемы ответа в base64 - "responseClass": "..", //имя типа ответа из proto файла - "requestPredicates": { - "meta.id": {"==": 42} - }, - "persist": { - // Модификации состояния - }, - "response": { - "mode": "fill", - "data": { - "code": 0, - "credit_amount": 802400, - "credit_term": 120, - "interest_rate": 13.9, - "partnum": "CL3.15" - }, - "delay": "1 second" - } -} -``` - -## Эмуляция шинных сервисов - -Алгоритм работы: - -1. Поиск мока по source -2. Поиск state по предикату -3. Валидация входящего сообщения -4. Подстановка значений в шаблон ответа -5. Модификация state -6. Отправка response -7. Выполнение колбеков (см. раздел "конфигурация колбеков") - -### Конфигурация - -[Работа с очередями](message-brokers.md) - -### Конфигурация мока - -Для input поддерживаются режимы: -* raw -* json -* xml -* jlens -* xpath - -Для output поддерживаются режимы: -* raw -* json -* xml - -```javascript -{ - "name": "Пришла весна", - "service": "test", - "source": "rmq_example_autobroker_decision", //source из конфига - "input": { - "mode": .. //как для HTTP заглушек - "payload": .. //как body для HTTP заглушек - }, - "state": { - // Предикаты - }, - "persist": { //Опционально - // Модификации состояния - }, - "destination": "rmq_example_q1", // destination из конфига, опционально - "output": { //Опционально - "mode": "raw", - "payload": "..", - "delay": "1 second" - }, - "callback": { .. } -} -``` - -### Конфигурация колбеков - -Для имитации поведения реального мира иногда нужно выполнить вызов HTTP сервиса (пример - забрать GBO когда приходит сообщение) или отправлять дополнительные сообщения в очереди. Для этого можно использовать колбеки. Результат вызова сервиса можно при необходимости распарсить и сохранить в состояние. Коллбеки используют состяние вызвавшего. - -#### Вызов HTTP метода - -Для request поддерживаются режимы -* no_body -* raw -* json -* xml - -Для response поддерживаются режимы -* json -* xml - ->Обратите внимание! ->В всю цепочку колбеков передаётся первоначальный стейт, он не изменяется блоком perist (!!!) - -```javascript -{ - "type": "http", - "request": { - "url": "http://some.host/api/v2/peka", - "method": "POST", - "headers": {"Content-Type": "application/json"}, - "mode": "json", - "body": { - "trace_id": "42", - "account_number": "228" - } - }, - "responseMode": "json" | "xml", //Обязательно только при наличии блока persist - "persist": { //Опционально - // Модификации состояния - }, - "delay": "1 second", //Задержка ПЕРЕД выполнением колбека, опционально - "callback": { .. } //Опционально -} -``` - -#### Отправка сообщения - -Для output поддерживаются режимы: -* raw -* json -* xml - -```javascript -{ - "type": "message", - "destination": "rmq_example_q1", // destination из конфига - "output": { - "mode": "raw", - "payload": ".." - }, - "callback": { .. } //Опционально -} -``` + + +# mockingbird + +mockingbird - a service for emulating REST services and queue-interface services + +[Installation Guide](deployment.md) + +[Configuration Guide](configuration.md) + +[Working with Message Brokers](message-brokers.md) + +[Readme in Russian](readme_ru.md) + +## General Principles of Operation + +mockingbird supports the following scenarios: + +* Execution of a specific case with a specific set of events and HTTP/GRPC responses +* Constant emulation of a happy-path to ensure autonomy of the stage environment(s) + +Types of configurations: +* countdown - standalone configurations for testing a specific scenario. They have the highest priority when resolving ambiguities. Each mock is triggered n times (the number is set during creation). Automatically deleted at midnight. +* ephemeral - configurations that are automatically deleted at midnight. If a method/message is called/arrives simultaneously, for which both countdown and ephemeral mocks are suitable - countdown will be triggered. +* persistent - configuration intended for continuous operation. Has the lowest priority + +## Services + +To organize mocks in the UI and minimize the number of conflict situations, so-called services are implemented in mockingbird. Each mock (both HTTP and scenario) always belongs to one of the services. +Services are created in advance and stored in the database. A service has a suffix (which also serves as the unique service id) and a human-readable name. + +## JSON Templating + +To achieve flexibility while maintaining the relative simplicity of configurations, a JSON templating feature is implemented in the service. To start, here's a simple example: + +Template: +```javascript +{ + "description": "${description}", + "topic": "${extras.topic}", + "comment": "${extras.comments.[0].text}", + "meta": { + "field1": "${extras.fields.[0]}" + } +} +``` + +Values for substitution: +```javascript +{ + "description": "Some description", + "extras": { + "fields": ["f1", "f2"], + "topic": "Main topic", + "comments": [ + {"text": "First nah!"}, {"text": "Okay"} + ] + } +} +``` + +Result: +```javascript +{ + "description": "Some description", + "topic": "Main topic", + "comment": "First nah!", + "meta": { + "field1": "f1" + } +} +``` + +Currently, the following syntax is supported: +* `${a.[0].b}` - value substitution (JSON) +* `${/a/b/c}` - value substitution (XPath) + +WARNING! DO NOT USE NAMESPACES IN XPATH EXPRESSIONS + +## XML Templating + +Template: +``` + + ${/r/t1} + ${/r/t2} + +``` + +Values for substitution: +``` + + test + 42 + +``` + +Result: +``` + + test + 42 + +``` + +## States (state) + +To support complex scenarios, the service supports saving arbitrary states. A state is a document with an arbitrary schema, technically a state is a document in MongoDB. Writing new states can occur: +* when writing to state (the persist section) with an empty (or missing) predicate (the state section) + +## State Manipulations + +State is cumulatively appended. Overwriting fields is allowed. + +Fields used for searching (used in predicates) must start with "_". +> a sparse index will be automatically created for such fields + +Prefixes: +* `seed` - values from the seed block (randomized at the start of the application) +* `state` - the current state +* `req` - the request body (modes json, jlens, xpath) +* `message` - the message body (in scenarios) +* `query` - query parameters (in stubs) +* `pathParts` - values extracted from the URL (in stubs) see `Data Extraction from URL` +* `extracted` - extracted values +* `headers` - HTTP headers + +```javascript +{ + "a": "Just a string", //The field "a" is assigned a constant (can be any JSON value) + "b": "${req.fieldB}", //The field "b" is assigned the value from the fieldB of the request + "c": "${state.c}", //The field "c" is assigned the value from the "c" field of the current state + "d": "${req.fieldA}: ${state.a}" //The field d will contain a string consisting of req.fieldA and state.a +} +``` + +## State Search + +Predicates for state search are listed in the `state` block. An empty object (`{}`) in the state field is not allowed. +For state search, request data (without prefix), query parameters (prefix `__query`), values extracted from the URL (prefix `__segments`), and HTTP headers (prefix `__headers`) can be used + +Example: + +```javascript +{ + "_a": "${fieldB}", //field from the request body + "_b": "${__query.arg1}", //query parameter + "_c": "${__segments.id}", //URL segment, see `Data Extraction from URL` + "_d": "${__headers.Accept}" //HTTP header +} +``` + +## Seeding + +Sometimes there is a need to generate a random value and save and/or return it as a result of the mock's operation. +To support such scenarios, a seed field is provided, allowing to set variables that will be generated +at the mock's initialization. This avoids the need to recreate mocks with hardcoded ids + +JavaScript evaluation is supported in seeds. The following functions are defined for backwards compatibility with "pseudofunctions": +* `%{randomString(n)}` - substitution of a random string of length n +* `%{randomString("ABCDEF1234567890", m, n)}` - substitution of a random string consisting of `ABCDEF1234567890` characters in the range [m, n) +* `%{randomNumericString(n)}` - substitution of a random string consisting only of digits, of length n +* `%{randomInt(n)}` - substitution of a random Int in the range [0, n) +* `%{randomInt(m,n)}` - substitution of a random Int in the range [m, n) +* `%{randomLong(n)}` - substitution of a random Long in the range [0, n) +* `%{randomLong(m,n)}` - substitution of a random Long in the range [m, n) +* `%{UUID}` - substitution of a random UUID +* `%{now(yyyy-MM-dd'T'HH:mm:ss)}` - the current time in the specified [format](https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html) +* `%{today(yyyy-MM-dd)}` - the current date in the specified [format](https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html) + +Complex formatted strings can be defined: `%{randomInt(10)}: %{randomLong(10)} | %{randomString(12)}`, all pseudo-functions from the list above are supported + +## Resolving Stubs/Scenarios + +> Found stubs - candidates remaining after validation of URL, headers, and request body +> Found scenarios - candidates remaining after validation of the message body + +| Found Stubs (Scenarios) | State Required | States Found | Result | +| ---------------------------- | ------------------- | ------------------ | -------------- | +| №1 | No | - | №1 is triggered | +| №1 | Yes | 0 | Error | +| №1 | Yes | 1 | №1 is triggered | +| №1
№2 | No
No | - | Error | +| №1
№2 | No
Yes | -
0 | №1 is triggered | +| №1
№2 | No
Yes | -
1 | №2 is triggered | +| №1
№2 | No
Yes | -
2 (and more) | Error | +| №1
№2 | Yes
Yes | 0
0 | Error | +| №1
№2 | Yes
Yes | 0
1 | №2 is triggered | +| №1
№2 | Yes
Yes | 0
2 (and more) | Error | +| №1
№2 | Yes
Yes | 1
1 (and more) | Error | +| №1
№2
№3 | Yes
Yes
Yes | 0
1
0 | №2 is triggered | +| №1
№2
№3 | Yes
Yes
Yes | 0
1
1 | Error | +| №1
№2
№3 | Yes
Yes
Yes | 0
2
0 | Error | + +## Emulating REST Services + +Workflow: +1. Search for a mock by URL/HTTP-verb/headers +2. Body validation +3. Search for state by predicate +4. Substitution of values in the response template +5. State modification +6. Sending the response + +### Configuration of HTTP Stubs + +HTTP headers are validated for exact match values, extra headers are not considered an error + +Request body validation in HTTP stubs can work in the following modes: +* no_body - the request must be without a body +* any_body - the request body must be non-empty, while it is not parsed or checked +* raw - the request body is not parsed and is checked for full correspondence with the content of request.body +* json - the request body must be a valid JSON and is checked for correspondence with the content of request.body +* xml - the request body must be a valid XML and is checked for correspondence with the content of request.body +* jlens - the request body must be a valid JSON and is validated according to conditions described in request.body +* xpath - the request body must be a valid XML and is validated according to conditions described in request.body +* web_form - the request body must be in x-www-form-urlencoded format and is validated according to conditions described in request.body +* multipart - the request body must be in multipart/form-data format. Validation rules for parts are configured individually (see the section below) + +ATTENTION! multipart requests must be made to a separate method - +/api/mockingbird/execmp + +For responses, the following modes are supported: +* raw +* json +* xml +* binary +* proxy +* json-proxy +* xml-proxy +* no_body + +The `no_body` mode in the response is needed if the stub returns a 204 or 304 code. These codes are distinguished from others by the fact that they cannot have any body in the response, this behavior is described in [RFC 7231](https://datatracker.ietf.org/doc/html/rfc7231#section-6.3.5) and [RFC 7232](https://datatracker.ietf.org/doc/html/rfc7232#section-4.1). The `no_body` mode can also be used with other HTTP codes, but it is mandatory for the specified ones. + +Request and response modes are completely independent of each other (you can configure a response in XML to a JSON request if desired, except for json-proxy and xml-proxy modes). + +In the delay field, you can pass a correct FiniteDuration no longer than 30 seconds + +### Data Extraction from URL +Sometimes, a URL contains an identifier not as a parameter but as a direct part of the path. In such cases, it becomes impossible to describe a persistent stub due to the inability to have a full path match. This is where the `pathPattern` field comes in handy, into which a regex can be passed, and the path will be checked for a match against this regex. It should be noted that although the matching is done in MongoDB in an efficient manner, this feature should not be abused, and the `pathPattern` should not be used if matching by full equality is possible. + +Example: +```javascript +{ + "name": "Sample stub", + "scope": "persistent", + "pathPattern": "/pattern/(?\d+)", + "method": "GET", + "request": { + "headers": {}, + "mode": "no_body", + "body": {} + }, + "response": { + "code": 200, + "mode": "json", + "headers": {"Content-Type": "application/json"}, + "body": {"id": "${pathParts.id}"} + } +} +``` +Anything that needs to be extracted from the path should be done with a _named_ group, and there can be as many groups as needed. Later on, these can be referred to through `pathParts.`. + +### Extractors +In some cases, it's necessary to insert into the response data that cannot be extracted by simple means. For these purposes, extractors have been added. + +#### xcdata Extractor + +Extracts values from XML located within CDATA. + +Configuration: +```javascript +{ + "type": "xcdata", + "prefix": "/root/inner/tag", // Path to the tag with CDATA + "path": "/path/to" // Path to the desired tag +} +``` + +#### jcdata Extractor + +Extracts values from JSON located within CDATA. + +Configuration: +```javascript +{ + "type": "jcdata", + "prefix": "/root/inner/tag", // Path to the tag with CDATA + "path": "path.to" // Path to the desired value +} +``` + +#### CDATA Inlining +Sometimes you have to deal with requests in which XML is nested inside CDATA. In such cases, you can inline the CDATA content using the `inlineCData` parameter (supported in `xpath` and `xml`). + +### Examples + +#### Exact Match, json Mode + +```javascript +{ + "name": "Sample stub", + "method": "POST", + "path": "/pos-loans/api/cl/get_partner_lead_info", + "state": { + // Predicates + }, + "request": { + "headers": {"Content-Type": "application/json"}, + "mode": "json", + "body": { + "trace_id": "42", + "account_number": "228" + } + }, + "persist": { + // State modifications + }, + "response": { + "code": 200, + "mode": "json", + "body": { + "code": 0, + "credit_amount": 802400, + "credit_term": 120, + "interest_rate": 13.9, + "partnum": "CL3.15" + }, + "headers": {"Content-Type": "application/json"}, + "delay": "1 second" + } +} +``` + +#### Exact Match, raw Mode + +```javascript +{ + "name": "Sample stub", + "method": "POST", + "path": "/pos-loans/api/evil/soap/service", + "state": { + // Predicates + }, + "request": { + "headers": {"Content-Type": "application/xml"}, + "mode": "raw", + "body": "" + }, + "persist": { + // State modifications + }, + "response": { + "code": 200, + "mode": "raw", + "body": "", + "headers": {"Content-Type": "application/xml"}, + "delay": "1 second" + } +} +``` + +#### Condition Validation, jlens Mode + +```javascript +{ + "name": "Sample stub", + "method": "POST", + "path": "/pos-loans/api/cl/get_partner_lead_info", + "state": { + // Predicates + }, + "request": { + "headers": {"Content-Type": "application/json"}, + "mode": "jlens", + "body": { + "meta.id": {"==": 42} + } + }, + "persist": { + // State modifications + }, + "response": { + "code": 200, + "mode": "json", + "body": { + "code": 0, + "credit_amount": 802400, + "credit_term": 120, + "interest_rate": 13.9, + "partnum": "CL3.15" + }, + "headers": {"Content-Type": "application/json"}, + "delay": "1 second" + } +} +``` + +#### Condition Validation, xpath Mode + +WARNING! DO NOT USE NAMESPACES IN XPATH EXPRESSIONS + +```javascript +{ + "name": "Sample stub", + "method": "POST", + "path": "/pos-loans/api/cl/get_partner_lead_info", + "state": { + // Predicates + }, + "request": { + "headers": {"Content-Type": "application/xml"}, + "mode": "xpath", + "body": { + "/payload/response/id": {"==": 42} + }, + "extractors": {"name": {...}, ...} //optional + }, + "persist": { + // State modifications + }, + "response": { + "code": 200, + "mode": "raw", + "body": "", + "headers": {"Content-Type": "application/xml"}, + "delay": "1 second" + } +} +``` + +#### Condition Validation, multipart Mode + +WARNING! multipart requests must be performed on a separate method - +/api/mockingbird/execmp + +Part validation modes: +* `any` - value is not validated +* `raw` - exact match +* `json` - exact match, value parsed as Json +* `xml` - exact match, value parsed as XML +* `urlencoded` - similar to `web_form` mode for validating the entire body +* `jlens` - Json condition check +* `xpath` - XML condition check + +```javascript +{ + "name": "Sample stub", + "method": "POST", + "path": "/test/multipart", + "state": { + // Predicates + }, + "request": { + "headers": {}, + "mode": "multipart", + "body": { + "part1": { + "mode": "json", //validation mode + "headers": {}, //part headers + "value": {} //value specification for the validator + }, + "part2": { + ... + } + }, + "bypassUnknownParts": true //flag allowing to ignore all parts not present in the validator's specification + //by default, the flag is enabled, can be passed only to disable (false) + }, + "persist": { + // State modifications + }, + "response": { + "code": 200, + "mode": "json", + "body": { + "code": 0, + "credit_amount": 802400, + "credit_term": 120, + "interest_rate": 13.9, + "partnum": "CL3.15" + }, + "headers": {"Content-Type": "application/json"}, + "delay": "1 second" + } +} +``` + +#### Simple Request Proxying + +```javascript +{ + "name": "Simple proxy", + "method": "POST", + "path": "/pos-loans/api/cl/get_partner_lead_info", + "state": { + // Predicates + }, + "request": { + // Request specification + }, + "response": { + "mode": "proxy", + "uri": "http://some.host/api/cl/get_partner_lead_info" + } +} +``` + +#### Proxying with JSON Response Modification + +```javascript +{ + "name": "Simple proxy", + "method": "POST", + "path": "/pos-loans/api/cl/get_partner_lead_info", + "state": { + // Predicates + }, + "request": { + // Request specification, mode json or jlens + }, + "response": { + "mode": "json-proxy", + "uri": "http://some.host/api/cl/get_partner_lead_info", + "patch": { + "field.innerField": "${req.someRequestField}" + } + } +} +``` + +#### Proxying with XML Response Modification + +```javascript +{ + "name": "Simple proxy", + "method": "POST", + "path": "/pos-loans/api/cl/get_partner_lead_info", + "state": { + // Predicates + }, + "request": { + // Request specification, mode xml or xpath + }, + "response": { + "mode": "xml-proxy", + "uri": "http://some.host/api/cl/get_partner_lead_info", + "patch": { + "/env/someTag": "${/some/requestTag}" + } + } +} +``` + +### DSL for JSON and XML Validation Predicates + +In jlens and xpath modes, the following is supported: + +```javascript +{ + "a": {"==": "some value"}, //exact match + "b": {"!=": "some value"}, //not equal + "c": {">": 42} | {">=": 42} | {"<": 42} | {"<=": 42}, //comparisons, for numbers only, can be combined + "d": {"~=": "\\d+"}, //regexp match + "e": {"size": 10}, //length, for arrays and strings + "f": {"exists": true} //existence check +} +``` +Keys in such objects are either a path in json ("a.b.[0].c") or xpath ("/a/b/c"). +Note: Currently, comparison functions may not work correctly with xpath pointing to XML attributes. +The problem can be bypassed by checking for existence/non-existence: +```/tag/otherTag/[@attr='2']": {"exists": true}``` + +In jlens mode, the following operations are additionally supported: +```javascript +{ + "g": {"[_]": ["1", 2, true]}, //the field must contain one of the listed values + "h": {"![_]": ["1", 2, true]}, //the field must NOT contain any of the listed values + "i": {"&[_]": ["1", 2, true]} //the field must be an array containing all listed values (order does not matter) +} +``` + +In xpath mode, the following operations are additionally supported: +```javascript + "/some/tag": {"cdata": {"==": "test"}}, //validation for exact match of CDATA, argument must be a STRING + "/some/tag": {"cdata": {"~=": "\d+"}}, //CDATA regex validation, argument must be a STRING + "/some/tag": {"jcdata": {"a": {"==": 42}}}, //validating CDATA content as JSON, all available predicates are supported + "/other/tag": {"xcdata": {"/b": {"==": 42}}} //validating CDATA content as XML, all available predicates are supported +``` + +In web_form mode, ONLY the following operations are supported: +`==`, `!=`, `~=`, `size`, `[_]`, `![_]`, `&[_]` + +## Emulating GRPC Services + +How it works under the hood: +When creating a mock, the proto files nested in the request are parsed and transformed into a json representation of the protobuf schema. The database stores the json representation, not the original proto file. The first triggering of the mock may take a little longer than subsequent ones because a protobuf message decoder is generated from the json representation on the first trigger. After decoding, the data is transformed into json, which is checked by json predicates specified in the requestPredicates field. If the conditions are met, then the json from response.data (in fill mode) is serialized into protobuf and returned as a response. + +Workflow: + +1. Search for mocks by method name +2. Body validation +3. Search for state by predicate +4. Substituting values in the response template +5. State modification +6. Response delivery + +### Configuration of GRPC Stubs + +```javascript +{ + "name": "Sample stub", + "scope": "..", + "service": "test", + "methodName": "/pos-loans/api/cl/get_partner_lead_info", + "seed": { + "integrationId": "%{randomString(20)}" //example + }, + "state": { + // Predicates + }, + "requestCodecs": "..", //request schema proto-file in base64 + "requestClass": "..", //name of the request type from proto file + "responseCodecs": "..", //response schema proto-file in base64 + "responseClass": "..", //name of the response type from proto file + "requestPredicates": { + "meta.id": {"==": 42} + }, + "persist": { + // State modifications + }, + "response": { + "mode": "fill", + "data": { + "code": 0, + "credit_amount": 802400, + "credit_term": 120, + "interest_rate": 13.9, + "partnum": "CL3.15" + }, + "delay": "1 second" + } +} +``` + +## Emulating Bus Services + +Workflow: + +1. Search for the mock by source. +2. Search for state by predicate. +3. Validate incoming message. +4. Substitute values into the response template. +5. Modify state. +6. Send response. +7. Execute callbacks (see the "callbacks configuration" section). + +### Configuration + +[Working with Message Brokers](message-brokers.md) + +### Mock Configuration + +Supported modes for input: +* raw +* json +* xml +* jlens +* xpath + +Supported modes for output: +* raw +* json +* xml + +```javascript +{ + "name": "Spring has come", + "service": "test", + "source": "rmq_example_autobroker_decision", //source from the config + "input": { + "mode": .. //as for HTTP stubs + "payload": .. //as body for HTTP stubs + }, + "state": { + // Predicates + }, + "persist": { //Optional + // State modifications + }, + "destination": "rmq_example_q1", // destination from the config, optional + "output": { //Optional + "mode": "raw", + "payload": "..", + "delay": "1 second" + }, + "callback": { .. } +} +``` + +### Callback Configuration + +To mimic the behavior of the real world, sometimes it is necessary to call an HTTP service (for example, to fetch GBO when a message arrives) or to send additional messages to queues. For this purpose, callbacks can be used. The result of the service call can be parsed and saved in the state if necessary. Callbacks use the state of the caller. + +#### Calling an HTTP Method + +Supported modes for request: +* no_body +* raw +* json +* xml + +Supported modes for response: +* json +* xml + +>Please note! +>The initial state is passed along the entire chain of callbacks, and it is not modified by the persist block (!!!) + +```javascript +{ + "type": "http", + "request": { + "url": "http://some.host/api/v2/peka", + "method": "POST", + "headers": {"Content-Type": "application/json"}, + "mode": "json", + "body": { + "trace_id": "42", + "account_number": "228" + } + }, + "responseMode": "json" | "xml", //Mandatory only if the persist block is present + "persist": { //Optional + // State modifications + }, + "delay": "1 second", //Delay BEFORE executing the callback, optional + "callback": { .. } //Optional +} +``` + +#### Sending a Message + +Supported modes for output: +* raw +* json +* xml + +```javascript +{ + "type": "message", + "destination": "rmq_example_q1", // destination from the config + "output": { + "mode": "raw", + "payload": ".." + }, + "callback": { .. } //Optional +} +``` diff --git a/readme_ru.md b/readme_ru.md new file mode 100644 index 00000000..6de67ffc --- /dev/null +++ b/readme_ru.md @@ -0,0 +1,765 @@ + + +# mockingbird + +mockingbird - сервис эмуляции REST-сервисов и сервисов с интерфейсами-очередями + +[Руководство по инсталляции](deployment_ru.md) + +[Руководство по настройке](configuration_ru.md) + +[Работа с очередями](message-brokers_ru.md) + +## Общие принципы работы + +mockingbird поддерживает следующие сценарии: + +* прогон конкретного кейса с конкретным набором событий и HTTP/GRPC ответов +* постоянная имитация happy-path для обеспечения автономности контура(ов) + +Типы конфигураций: +* countdown - автономные конфигурации для тестирования конкретного сценария. Имеют наивысший приоритет при разрешении неоднозначностей. Каждый мок срабатывает n раз (количество задаётся при создании). Автоматически удаляются в полночь. +* ephemeral - конфигурации, автоматически удаляемые в полночь. Если одновременно вызывают метод/приходит сообщение, для которого подходит countdown и ephemeral моки - сработает countdown. +* persistent - конфигурация, предназначеная для постоянной работы. Имеет наименьший приоритет + +## Сервисы + +Для упорядочения моков в UI и минимизации количества конфликтных ситуаций в mockingbird реализованы т.н. сервисы. Каждый мок (как HTTP так и сценарий) всегда принадлежит к какому-то из сервисом. +Сервисы создаются заранее и хранятся в базе. Сервис имеет suffix (являющийся по совместительству уникальным id сервиса) и человекочитаемый name. + +## Шаблонизатор JSON + +Для достижения гибкости при сохранении относительной простоты конфигов в сервисе реализован JSON шаблонизатор. Для начала простой пример: + +Шаблон: +```javascript +{ + "description": "${description}", + "topic": "${extras.topic}", + "comment": "${extras.comments.[0].text}", + "meta": { + "field1": "${extras.fields.[0]}" + } +} +``` + +Значения для подстановки: +```javascript +{ + "description": "Some description", + "extras": { + "fields": ["f1", "f2"], + "topic": "Main topic", + "comments": [ + {"text": "First nah!"}, {"text": "Okay"} + ] + } +} +``` + +Результат: +```javascript +{ + "description": "Some description", + "topic": "Main topic", + "comment": "First nah!", + "meta": { + "field1": "f1" + } +} +``` + +В данный момент поддерживается следующий синтаксис: +* `${a.[0].b}` - подстановка значения (JSON) +* `${/a/b/c}` - подстановка значения (XPath) + +ВНИМАНИЕ! НЕ ИСПОЛЬЗУЙТЕ НЕЙМСПЕЙСЫ В XPATH ВЫРАЖЕНИЯХ + +## Шаблонизатор XML + +Шаблон: +``` + + ${/r/t1} + ${/r/t2} + +``` + +Значения для подстановки: +``` + + test + 42 + +``` + +Результат: +``` + + test + 42 + +``` + +## Состояния (state) + +Для поддержки сложных сценариев сервис поддерживает сохранение произвольных состояний. Состояние - документ с произвольной схемой, технически состояние - документ в mongodb. Запись новых состояний может происходить: +* при записи в state (секция persist) с пустым (или отсутствующим) предикатом (секция state) + +## Манипуляции со state + +State аккумулятивно дописывается. Разрешено переписывание полей. + +Поля, по которым будем производиться поиск (используемые в предикатах) должны начинаться с "_". +> для таких полей будет автоматически создаваться sparse индекс + +Префиксы: +* `seed` - значения из блока seed (рандомизируемые на старте заявки) +* `state` - текущий state +* `req` - тело запроса (режимы json, jlens, xpath) +* `message` - тело собщения (в сценариях) +* `query` - query параметры (в заглушках) +* `pathParts` - значения, извлекаемые из URL (в заглушках) см. `Экстрация данных из URL` +* `extracted` - извлечённые значения +* `headers` - HTTP заголовки + +```javascript +{ + "a": "Просто строка", //В поле "a" записывается константа (может быть любое JSON значение) + "b": "${req.fieldB}", //В поле "b" записывается значение из поля fieldB запроса + "c": "${state.c}", //В поле "c" записывается значение из поля "c" текущего состояния + "d": "${req.fieldA}: ${state.a}" //В поле d запишется строка, содержащая req.fieldA и state.a +} +``` + +## Поиск state + +Предикаты для поиска state перечисляются в блоке `state`. Пустой объект (`{}`) в поле state недопустим. +Для поиска state можно использовать данные запроса (без префикса), query параметры (префикс `__query`), значения, извлекаемые из URL (префикс `__segments`) и HTTP заголовки (префикс `__headers`) + +Пример: + +```javascript +{ + "_a": "${fieldB}", //поле из тела запроса + "_b": "${__query.arg1}", //query параметр + "_c": "${__segments.id}", //сегмент URL, см. `Экстрация данных из URL` + "_d": "${__headers.Accept}" //HTTP заголовок +} +``` + + +## Seeding + +Иногда возникает необходимость сгенерировать случайное значение и сохранить и/или вернуть его в результате работы мока. +Для поддержки таких сценариев сделано поле seed, позволяющее задать переменные, которые будут сгенерированы +при инициализации мока. Это позволяет избежать необходимости пересоздавать моки с захардкожеными id + +В seed'ах поддерживается выполнение JavaScript, для поддержки обратной совместимости с синтаксисом псевдофункций определены следующие функции: +* `%{randomString(n)}` - подстановка случайной строки длиной n +* `%{randomString("ABCDEF1234567890", m, n)}` - подстановка случайной строки, состоящей из символов `ABCDEF1234567890` длиной в интервале [m, n) +* `%{randomNumericString(n)}` - подстановка случайной строки, состоящей только из цифр, длиной n +* `%{randomInt(n)}` - подстановка случайного Int в диапазоне [0, n) +* `%{randomInt(m,n)}` - подстановка случайного Int в диапазоне [m, n) +* `%{randomLong(n)}` - подстановка случайного Long в диапазоне [0, n) +* `%{randomLong(m,n)}` - подстановка случайного Long в диапазоне [m, n) +* `%{UUID()}` - подстановка случайного UUID +* `%{now("yyyy-MM-dd'T'HH:mm:ss")}` - текущее время в заданном [формате](https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html) +* `%{today("yyyy-MM-dd")}` - текущая дата в заданном [формате](https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html) + +Можно определять строки со сложным форматом: `%{randomInt(10) +': ' + randomLong(10) + ' | ' + {randomString(12)}`, поддерживаются все псевдофункции из списка выше + +## Резолвинг заглушек/сценариев + +> Найденые заглушки - кандидаты, оставшиеся после валидации URL, заголовков и тела запроса +> Найденые сценарии - кандидаты, оставшиеся после валидации тела сообщения + +| Найденые заглушки (сценарии) | Требуется состояние | Найдено состояний | Результат | +| --------------------------- | ------------------- | ----------------- | --------- | +| №1 | нет | - | Сработает №1 | +| №1 | да | 0 | Ошибка | +| №1 | да | 1 | Сработает №1 | +| №1
№2 | нет
нет | - | Ошибка | +| №1
№2 | нет
да | -
0 | Сработает №1 | +| №1
№2 | нет
да | -
1 | Сработает №2 | +| №1
№2 | нет
да | -
2 (и более) | Ошибка | +| №1
№2 | да
да | 0
0 | Ошибка | +| №1
№2 | да
да | 0
1 | Сработает №2 | +| №1
№2 | да
да | 0
2 (и более) | Ошибка | +| №1
№2 | да
да | 1
1 (и более) | Ошибка | +| №1
№2
№3 | да
да
да | 0
1
0 | Сработает №2 | +| №1
№2
№3 | да
да
да | 0
1
1 | Ошибка | +| №1
№2
№3 | да
да
да | 0
2
0 | Ошибка | + +## Эмуляция REST сервисов + +Алгоритм работы: + +1. Поиск мока по URL/HTTP-verb/заголовков +2. Валидация body +3. Поиск state по предикату +4. Подстановка значений в шаблон ответа +5. Модификация state +6. Отдача response + +### Конфигурация HTTP заглушек + +HTTP заголовки валидируются на полное соответствие значений, лишние заголовки не являются ошибкой + +Валидация тела запросы в HTTP заглушках может работать в следующих режимах: +* no_body - запрос должен быть без тела +* any_body - тело запроса должно быть не пустым, при этом никак не парсится и не проверяется +* raw - тело запроса никак не парсится и проверяется на полное соответствие с содержимым request.body +* json - тело запроса должно быть валидным JSON'ом и проверяется на соответствие с содержимым request.body +* xml - тело запроса должно быть валидным XML и проверяется на соответствие с содержимым request.body +* jlens - тело запроса должно быть валидным JSON'ом и валидируется по условиям, описаным в request.body +* xpath - тело запроса должно быть валидным XML и валидируется по условиям, описаным в request.body +* web_form - тело запроса должно быть в формате x-www-form-urlencoded и валидируется по условиям, описаным в request.body +* multipart - тело запроса должно быть в формате multipart/form-data. Правила валидации частей конфигурируются индивидуально (см. раздел ниже) + +ВНИМАНИЕ! multipart запросы необходимо выполнять на отдельный метод - +/api/mockingbird/execmp + +Для ответов поддерживаются следующие режимы: +* raw +* json +* xml +* binary +* proxy +* json-proxy +* xml-proxy +* no_body + +Режим `no_body` в ответе нужен если заглушка возвращает код 204 или 304. Данные коды выделяются от остальных тем, что у них не может быть никакого тела в ответе, данное поведение описано в [RFC 7231](https://datatracker.ietf.org/doc/html/rfc7231#section-6.3.5) и [RFC 7232](https://datatracker.ietf.org/doc/html/rfc7232#section-4.1). Режим `no_body` можно использовать и с остальными HTTP кодами, но для указных он является обязательным. + +Режимы request и response полностью независимы друг от друга (можно сконфигурировать ответ xml'ем на json запрос при желании, кроме режимов json-proxy и xml-proxy). + +В поле delay можно передать корректный FiniteDuration не дольше 30 секунд + +### Экстрация данных из URL +Бывает, что URL содержит какой-нибудь идентификатор не как параметр, а как непосредственно часть пути. В таких случаях становится невозможным +описать persistent заглушку из-за невозможности полного совпадения пути. На помощь приходит поле pathPattern, в которое можно передать регулярку, +на соответствие которой будет проверяться путь. Отмечу, что хоть сопоставление и производится в монге эффективным способом, злоупотребять этой +возможностью не стоит и при возможности сопоставления по полному совпадению не следует использовать pathPattern + +Пример: +```javascript +{ + "name": "Sample stub", + "scope": "persistent", + "pathPattern": "/pattern/(?\d+)", + "method": "GET", + "request": { + "headers": {}, + "mode": "no_body", + "body": {} + }, + "response": { + "code": 200, + "mode": "json", + "headers": {"Content-Type": "application/json"}, + "body": {"id": "${pathParts.id}"} + } +} +``` +То, что нужно извлечь из пути, нужно делать _именованой_ группой, групп может быть сколько угодно, впоследствии на них можно ссылаться через `pathParts.<имя_группы>` + +### Экстракторы +В некоторых случаях нужно подставить в ответ данные, которые невозможно извлечь простыми средствами. Для этих целей были добавлены экстракторы + +#### Экстрактор xcdata + +Достаёт значения из XML, лежащего в CDATA + +конфигурация: +```javascript +{ + "type": "xcdata", + "prefix": "/root/inner/tag", //Путь до тэга с CDATA + "path": "/path/to" //Путь до нужного тэга +} +``` + +#### Экстрактор jcdata + +Достаёт значения из JSON, лежащего в CDATA + +конфигурация: +```javascript +{ + "type": "jcdata", + "prefix": "/root/inner/tag", //Путь до тэга с CDATA + "path": "path.to" //Путь до нужного значения +} +``` + +#### CDATA inlining +Иногда приходится иметь дело с запросами, в которых внутри CDATA лежит XML. В таких случаях можно заинлайнить содержимое DATA с помощью параметра `inlineCData` (поддерживается в `xpath` и `xml`) + +### Примеры + +#### Полное совпадение, режим json + +```javascript +{ + "name": "Sample stub", + "method": "POST", + "path": "/pos-loans/api/cl/get_partner_lead_info", + "state": { + // Предикаты + }, + "request": { + "headers": {"Content-Type": "application/json"}, + "mode": "json", + "body": { + "trace_id": "42", + "account_number": "228" + } + }, + "persist": { + // Модификации состояния + }, + "response": { + "code": 200, + "mode": "json", + "body": { + "code": 0, + "credit_amount": 802400, + "credit_term": 120, + "interest_rate": 13.9, + "partnum": "CL3.15" + }, + "headers": {"Content-Type": "application/json"}, + "delay": "1 second" + } +} +``` + +#### Полное совпадение, режим raw + +```javascript +{ + "name": "Sample stub", + "method": "POST", + "path": "/pos-loans/api/evil/soap/service" + "state": { + // Предикаты + }, + "request": { + "headers": {"Content-Type": "application/xml"}, + "mode": "raw" + "body": "" + }, + "persist": { + // Модификации состояния + }, + "response": { + "code": 200, + "mode": "raw" + "body": "", + "headers": {"Content-Type": "application/xml"}, + "delay": "1 second" + } +} +``` + +#### Валидация по условиям, режим jlens + +```javascript +{ + "name": "Sample stub", + "method": "POST", + "path": "/pos-loans/api/cl/get_partner_lead_info", + "state": { + // Предикаты + }, + "request": { + "headers": {"Content-Type": "application/json"}, + "mode": "jlens", + "body": { + "meta.id": {"==": 42} + } + }, + "persist": { + // Модификации состояния + }, + "response": { + "code": 200, + "mode": "json", + "body": { + "code": 0, + "credit_amount": 802400, + "credit_term": 120, + "interest_rate": 13.9, + "partnum": "CL3.15" + }, + "headers": {"Content-Type": "application/json"}, + "delay": "1 second" + } +} +``` + +#### Валидация по условиям, режим xpath + +ВНИМАНИЕ! НЕ ИСПОЛЬЗУЙТЕ НЕЙМСПЕЙСЫ В XPATH ВЫРАЖЕНИЯХ + +```javascript +{ + "name": "Sample stub", + "method": "POST", + "path": "/pos-loans/api/cl/get_partner_lead_info", + "state": { + // Предикаты + }, + "request": { + "headers": {"Content-Type": "application/xml"}, + "mode": "xpath", + "body": { + "/payload/response/id": {"==": 42} + }, + "extractors": {"name": {...}, ...} //опционально + }, + "persist": { + // Модификации состояния + }, + "response": { + "code": 200, + "mode": "raw" + "body": "", + "headers": {"Content-Type": "application/xml"}, + "delay": "1 second" + } +} +``` + +#### Валидация по условиям, режим multipart + +ВНИМАНИЕ! multipart запросы необходимо выполнять на отдельный метод - +/api/mockingbird/execmp + +Режимы валидании part: +* `any` - значение никак не валидируется +* `raw` - полное соответствие +* `json` - полное соответствие, значение парсится как Json +* `xml` - полное соответствие, значение парсится как XML +* `urlencoded` - аналогично режиму `web_form` для валидации всего тела +* `jlens` - проверка Json по условиям +* `xpath` - проверка XML по условиям + +```javascript +{ + "name": "Sample stub", + "method": "POST", + "path": "/test/multipart", + "state": { + // Предикаты + }, + "request": { + "headers": {}, + "mode": "multipart", + "body": { + "part1": { + "mode": "json", //режим валидации + "headers": {}, //заголовки part + "value": {} //спецификация значения для валидатора + }, + "part2": { + ... + } + }, + "bypassUnknownParts": true //флаг, позволяющий игнорировать все partы, отсутвующие в спецификации валидатора + //по умолчанию флаг включен, можно передавать только для отключения (false) + }, + "persist": { + // Модификации состояния + }, + "response": { + "code": 200, + "mode": "json", + "body": { + "code": 0, + "credit_amount": 802400, + "credit_term": 120, + "interest_rate": 13.9, + "partnum": "CL3.15" + }, + "headers": {"Content-Type": "application/json"}, + "delay": "1 second" + } +} +``` + +#### Простое проксирование запроса + +```javascript +{ + "name": "Simple proxy", + "method": "POST", + "path": "/pos-loans/api/cl/get_partner_lead_info", + "state": { + // Предикаты + }, + "request": { + // Спецификация запроса + }, + "response": { + "mode": "proxy", + "uri": "http://some.host/api/cl/get_partner_lead_info" + } +} +``` + +#### Проксирование с модификацией JSON ответа + +```javascript +{ + "name": "Simple proxy", + "method": "POST", + "path": "/pos-loans/api/cl/get_partner_lead_info", + "state": { + // Предикаты + }, + "request": { + // Спецификация запроса, mode json или jlens + }, + "response": { + "mode": "json-proxy", + "uri": "http://some.host/api/cl/get_partner_lead_info", + "patch": { + "field.innerField": "${req.someRequestField}" + } + } +} +``` + +#### Проксирование с модификацией XML ответа + +```javascript +{ + "name": "Simple proxy", + "method": "POST", + "path": "/pos-loans/api/cl/get_partner_lead_info", + "state": { + // Предикаты + }, + "request": { + // Спецификация запроса, mode xml или xpath + }, + "response": { + "mode": "xml-proxy", + "uri": "http://some.host/api/cl/get_partner_lead_info", + "patch": { + "/env/someTag": "${/some/requestTag}" + } + } +} +``` + +### DSL предикатов валидации JSON и XML + +в режимах jlens и xpath поддерживается следующее: + +```javascript +{ + "a": {"==": "some value"}, //полное соответствие + "b": {"!=": "some value"}, //не равно + "c": {">": 42} | {">=": 42} | {"<": 42} | {"<=": 42}, //сравнения, только для чисел, комбинируются + "d": {"~=": "\d+"}, //сопоставление с regexp, + "e": {"size": 10}, //длина, для массивов и строк + "f": {"exists": true} //проверка существования +} +``` +Ключами в таких объектах является либо путь в json ("a.b.[0].c") либо xpath ("/a/b/c") +Замечание: в данный момент функции сравнения могут некорректно работать с xpath, указывающими на XML атрибуты. +Обойти проблему можно проверкой на существование/несуществование: +```/tag/otherTag/[@attr='2']": {"exists": true}``` + +в режиме jlens дополнительно поддерживаются следующие операции: +```javascript +{ + "g": {"[_]": ["1", 2, true]}, //поле должно содержать одно из перечисленых значений + "h": {"![_]": ["1", 2, true]}, //поле НЕ должно содержать ни одно из перечисленых знаечний + "i": {"&[_]": ["1", 2, true]} // поле должно быть массивом и содержать все перечисленные значения (при этом порядок не важен) +} +``` + +в режиме xpath дополнительно поддерживаются следующие операции: +```javascript + "/some/tag": {"cdata": {"==": "test"}}, //валидация на полное совпадение CDATA, аргумент должен быть СТРОКОЙ + "/some/tag": {"cdata": {"~=": "\d+"}}, //валидация DATA регуляркой, аргумент должен быть СТРОКОЙ + "/some/tag": {"jcdata": {"a": {"==": 42}}}, //валидируем содержимое CDATA как JSON, поддерживаются все доступные предикаты + "/other/tag": {"xcdata": {"/b": {"==": 42}}} //валидируем содержимое CDATA как XML, поддерживаются все доступные предикаты +``` + +в режиме web_form поддерживаются ТОЛЬКО следующие операции: +`==`, `!=`, `~=`, `size`, `[_]`, `![_]`, `&[_]` + +## Эмуляция GRPC сервисов + +Как это устроено под капотом: +При создании мока вложеные в запрос proto файлы парсятся и преобразуются в json-представление protobuf схемы. В базе хранится именно json-представление, +а не оригинальный proto файл. Первое срабатывание мока может занимать немного больше времени, чем последующие, т.к. при первом срабатывании из +json-представляения генерируется декодер protobuf сообщений. После декодирования данные преобразуются в json, который проверяется json-предикатами, +задаными в поле requestPredicates. Если условия выполняются - то json из response.data (в режиме fill) сериализуется в protobuf и отдаётся в качестве ответа. + +Алгоритм работы: + +1. Поиск мока(-ов) по имени метода +2. Валидация body +3. Поиск state по предикату +4. Подстановка значений в шаблон ответа +5. Модификация state +6. Отдача response + +### Конфигурация GRPC заглушек + +```javascript +{ + "name": "Sample stub", + "scope": "..", + "service": "test", + "methodName": "/pos-loans/api/cl/get_partner_lead_info", + "seed": { + "integrationId": "%{randomString(20)}" //пример + }, + "state": { + // Предикаты + }, + "requestCodecs": "..", //proto-файл схемы запроса в base64 + "requestClass": "..", //имя типа запроса из proto файла + "responseCodecs": "..", //proto-файл схемы ответа в base64 + "responseClass": "..", //имя типа ответа из proto файла + "requestPredicates": { + "meta.id": {"==": 42} + }, + "persist": { + // Модификации состояния + }, + "response": { + "mode": "fill", + "data": { + "code": 0, + "credit_amount": 802400, + "credit_term": 120, + "interest_rate": 13.9, + "partnum": "CL3.15" + }, + "delay": "1 second" + } +} +``` + +## Эмуляция шинных сервисов + +Алгоритм работы: + +1. Поиск мока по source +2. Поиск state по предикату +3. Валидация входящего сообщения +4. Подстановка значений в шаблон ответа +5. Модификация state +6. Отправка response +7. Выполнение колбеков (см. раздел "конфигурация колбеков") + +### Конфигурация + +[Работа с очередями](message-brokers.md) + +### Конфигурация мока + +Для input поддерживаются режимы: +* raw +* json +* xml +* jlens +* xpath + +Для output поддерживаются режимы: +* raw +* json +* xml + +```javascript +{ + "name": "Пришла весна", + "service": "test", + "source": "rmq_example_autobroker_decision", //source из конфига + "input": { + "mode": .. //как для HTTP заглушек + "payload": .. //как body для HTTP заглушек + }, + "state": { + // Предикаты + }, + "persist": { //Опционально + // Модификации состояния + }, + "destination": "rmq_example_q1", // destination из конфига, опционально + "output": { //Опционально + "mode": "raw", + "payload": "..", + "delay": "1 second" + }, + "callback": { .. } +} +``` + +### Конфигурация колбеков + +Для имитации поведения реального мира иногда нужно выполнить вызов HTTP сервиса (пример - забрать GBO когда приходит сообщение) или отправлять дополнительные сообщения в очереди. Для этого можно использовать колбеки. Результат вызова сервиса можно при необходимости распарсить и сохранить в состояние. Коллбеки используют состяние вызвавшего. + +#### Вызов HTTP метода + +Для request поддерживаются режимы +* no_body +* raw +* json +* xml + +Для response поддерживаются режимы +* json +* xml + +>Обратите внимание! +>В всю цепочку колбеков передаётся первоначальный стейт, он не изменяется блоком perist (!!!) + +```javascript +{ + "type": "http", + "request": { + "url": "http://some.host/api/v2/peka", + "method": "POST", + "headers": {"Content-Type": "application/json"}, + "mode": "json", + "body": { + "trace_id": "42", + "account_number": "228" + } + }, + "responseMode": "json" | "xml", //Обязательно только при наличии блока persist + "persist": { //Опционально + // Модификации состояния + }, + "delay": "1 second", //Задержка ПЕРЕД выполнением колбека, опционально + "callback": { .. } //Опционально +} +``` + +#### Отправка сообщения + +Для output поддерживаются режимы: +* raw +* json +* xml + +```javascript +{ + "type": "message", + "destination": "rmq_example_q1", // destination из конфига + "output": { + "mode": "raw", + "payload": ".." + }, + "callback": { .. } //Опционально +} +```