Skip to content

Tinkoff/mockingbird

Folders and files

NameName
Last commit message
Last commit date

Latest commit

8087f54 · Aug 8, 2023

History

70 Commits
Aug 8, 2023
Aug 8, 2023
Aug 8, 2023
Aug 8, 2023
Jun 29, 2023
Apr 27, 2023
Jul 13, 2023
Nov 15, 2022
Nov 15, 2022
Mar 15, 2023
Nov 15, 2022
Nov 25, 2022
Aug 8, 2023
Aug 6, 2023
Jul 14, 2023

Repository files navigation

mockingbird

mockingbird - сервис эмуляции REST-сервисов и сервисов с интерфейсами-очередями

Руководство по инсталляции

Руководство по настройке

Работа с очередями

Общие принципы работы

mockingbird поддерживает следующие сценарии:

  • прогон конкретного кейса с конкретным набором событий и HTTP/GRPC ответов
  • постоянная имитация happy-path для обеспечения автономности контура(ов)

Типы конфигураций:

  • countdown - автономные конфигурации для тестирования конкретного сценария. Имеют наивысший приоритет при разрешении неоднозначностей. Каждый мок срабатывает n раз (количество задаётся при создании). Автоматически удаляются в полночь.
  • ephemeral - конфигурации, автоматически удаляемые в полночь. Если одновременно вызывают метод/приходит сообщение, для которого подходит countdown и ephemeral моки - сработает countdown.
  • persistent - конфигурация, предназначеная для постоянной работы. Имеет наименьший приоритет

Пример небольшого кейса (короткая заявка) - в конце спецификации

Сервисы

Для упорядочения моков в UI и минимизации количества конфликтных ситуаций в mockingbird реализованы т.н. сервисы. Каждый мок (как HTTP так и сценарий) всегда принадлежит к какому-то из сервисом. Сервисы создаются заранее и хранятся в базе. Сервис имеет suffix (являющийся по совместительству уникальным id сервиса) и человекочитаемый name.

Шаблонизатор JSON

Для достижения гибкости при сохранении относительной простоты конфигов в сервисе реализован JSON шаблонизатор. Для начала простой пример:

Шаблон:

{
  "description": "${description}",
  "topic": "${extras.topic}",
  "comment": "${extras.comments.[0].text}",
  "meta": {
    "field1": "${extras.fields.[0]}"
  }
}

Значения для подстановки:

{
  "description": "Some description",
  "extras": {
    "fields": ["f1", "f2"],
    "topic": "Main topic",
    "comments": [
      {"text": "First nah!"}, {"text": "Okay"}
    ]
  }
}

Результат:

{
  "description": "Some description",
  "topic": "Main topic",
  "comment": "First nah!",
  "meta": {
    "field1": "f1"
  }
}

В данный момент поддерживается следующий синтаксис:

  • ${a.[0].b} - подстановка значения (JSON)
  • ${/a/b/c} - подстановка значения (XPath)

ВНИМАНИЕ! НЕ ИСПОЛЬЗУЙТЕ НЕЙМСПЕЙСЫ В XPATH ВЫРАЖЕНИЯХ

Шаблонизатор XML

Шаблон:

<root>
    <tag1>${/r/t1}</tag1>
    <tag2 a2="${/r/t2/@a2}">${/r/t2}</tag2>
</root>

Значения для подстановки:

<r>
    <t1>test</t1>
    <t2 a2="attr2">42</t2>
</r>

Результат:

<root>
    <tag1>test</tag1>
    <tag2 a2="attr2">42</tag2>
</root>

Состояния (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 заголовки
{
  "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)

Пример:

{
  "_a": "${fieldB}", //поле из тела запроса
  "_b": "${__query.arg1}", //query параметр
  "_c": "${__segments.id}", //сегмент URL, см. `Экстрация данных из URL`
  "_d": "${__headers.Accept}" //HTTP заголовок
}

Seeding

Иногда возникает необходимость сгенерировать случайное значение и сохранить и/или вернуть его в результате работы мока. Для поддержки таких сценариев сделано поле seed, позволяющее задать переменные, которые будут сгенерированы при инициализации мока. Это позволяет избежать необходимости пересоздавать моки с захардкожеными id

В seed'ах поддерживается синтаксис псевдофункций:

  • %{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)} - текущее время в заданном формате
  • %{today(yyyy-MM-dd)} - текущая дата в заданном формате

Можно определять строки со сложным форматом: %{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

Режимы request и response полностью независимы друг от друга (можно сконфигурировать ответ xml'ем на json запрос при желании, кроме режимов json-proxy и xml-proxy)

В поле delay можно передать корректный FiniteDuration не дольше 30 секунд

Экстрация данных из URL

Бывает, что URL содержит какой-нибудь идентификатор не как параметр, а как непосредственно часть пути. В таких случаях становится невозможным описать persistent заглушку из-за невозможности полного совпадения пути. На помощь приходит поле pathPattern, в которое можно передать регулярку, на соответствие которой будет проверяться путь. Отмечу, что хоть сопоставление и производится в монге эффективным способом, злоупотребять этой возможностью не стоит и при возможности сопоставления по полному совпадению не следует использовать pathPattern

Пример:

{
  "name": "Sample stub",
  "scope": "persistent",
  "pathPattern": "/pattern/(?<id>\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

конфигурация:

{
  "type": "xcdata",
  "prefix": "/root/inner/tag", //Путь до тэга с CDATA
  "path": "/path/to" //Путь до нужного тэга
}

Экстрактор jcdata

Достаёт значения из JSON, лежащего в CDATA

конфигурация:

{
  "type": "jcdata",
  "prefix": "/root/inner/tag", //Путь до тэга с CDATA
  "path": "path.to" //Путь до нужного значения
}

CDATA inlining

Иногда приходится иметь дело с запросами, в которых внутри CDATA лежит XML. В таких случаях можно заинлайнить содержимое DATA с помощью параметра inlineCData (поддерживается в xpath и xml)

Примеры

Полное совпадение, режим json

{
    "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

{
    "name": "Sample stub",
    "method": "POST",
    "path": "/pos-loans/api/evil/soap/service"
    "state": {
      // Предикаты
    },
    "request": {
        "headers": {"Content-Type": "application/xml"},
        "mode": "raw"
        "body": "<xml><request type=\"rqt\"></request></xml>"
    },
    "persist": {
      // Модификации состояния
    },
    "response": {
        "code": 200,
        "mode": "raw"
        "body": "<xml><response type=\"rqt\"></response></xml>",
        "headers": {"Content-Type": "application/xml"},
        "delay": "1 second"
    }
}

Валидация по условиям, режим jlens

{
    "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 ВЫРАЖЕНИЯХ

{
    "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": "<xml><response type=\"rst\"></response></xml>",
        "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 по условиям
{
    "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"
    }
}

Простое проксирование запроса

{
  "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 ответа

{
  "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 ответа

{
  "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 поддерживается следующее:

{
  "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 дополнительно поддерживаются следующие операции:

{
    "g": {"[_]": ["1", 2, true]}, //поле должно содержать одно из перечисленых значений
    "h": {"![_]": ["1", 2, true]}, //поле НЕ должно содержать ни одно из перечисленых знаечний
    "i": {"&[_]": ["1", 2, true]} // поле должно быть массивом и содержать все перечисленные значения (при этом порядок не важен)
}

в режиме xpath дополнительно поддерживаются следующие операции:

  "/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 заглушек

{
    "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. Выполнение колбеков (см. раздел "конфигурация колбеков")

Конфигурация

Работа с очередями

Конфигурация мока

Для input поддерживаются режимы:

  • raw
  • json
  • xml
  • jlens
  • xpath

Для output поддерживаются режимы:

  • raw
  • json
  • xml
{
  "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 (!!!)

{
  "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
{
  "type": "message",
  "destination": "rmq_example_q1", // destination из конфига
  "output": {
    "mode": "raw",
    "payload": ".."
  },
  "callback": { .. } //Опционально
}