mockingbird - сервис эмуляции REST-сервисов и сервисов с интерфейсами-очередями
mockingbird поддерживает следующие сценарии:
- прогон конкретного кейса с конкретным набором событий и HTTP/GRPC ответов
- постоянная имитация happy-path для обеспечения автономности контура(ов)
Типы конфигураций:
- countdown - автономные конфигурации для тестирования конкретного сценария. Имеют наивысший приоритет при разрешении неоднозначностей. Каждый мок срабатывает n раз (количество задаётся при создании). Автоматически удаляются в полночь.
- ephemeral - конфигурации, автоматически удаляемые в полночь. Если одновременно вызывают метод/приходит сообщение, для которого подходит countdown и ephemeral моки - сработает countdown.
- persistent - конфигурация, предназначеная для постоянной работы. Имеет наименьший приоритет
Пример небольшого кейса (короткая заявка) - в конце спецификации
Для упорядочения моков в UI и минимизации количества конфликтных ситуаций в mockingbird реализованы т.н. сервисы. Каждый мок (как HTTP так и сценарий) всегда принадлежит к какому-то из сервисом. Сервисы создаются заранее и хранятся в базе. Сервис имеет suffix (являющийся по совместительству уникальным id сервиса) и человекочитаемый name.
Для достижения гибкости при сохранении относительной простоты конфигов в сервисе реализован 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 ВЫРАЖЕНИЯХ
Шаблон:
<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>
Для поддержки сложных сценариев сервис поддерживает сохранение произвольных состояний. Состояние - документ с произвольной схемой, технически состояние - документ в mongodb. Запись новых состояний может происходить:
- при записи в state (секция persist) с пустым (или отсутствующим) предикатом (секция 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 можно использовать данные запроса (без префикса), query параметры (префикс __query), значения, извлекаемые из URL (префикс __segments) и HTTP заголовки (префикс __headers)
Пример:
{
  "_a": "${fieldB}", //поле из тела запроса
  "_b": "${__query.arg1}", //query параметр
  "_c": "${__segments.id}", //сегмент URL, см. `Экстрация данных из URL`
  "_d": "${__headers.Accept}" //HTTP заголовок
}Иногда возникает необходимость сгенерировать случайное значение и сохранить и/или вернуть его в результате работы мока. Для поддержки таких сценариев сделано поле 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 | Ошибка | 
Алгоритм работы:
- Поиск мока по URL/HTTP-verb/заголовков
- Валидация body
- Поиск state по предикату
- Подстановка значений в шаблон ответа
- Модификация state
- Отдача response
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 содержит какой-нибудь идентификатор не как параметр, а как непосредственно часть пути. В таких случаях становится невозможным описать 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.<имя_группы>
В некоторых случаях нужно подставить в ответ данные, которые невозможно извлечь простыми средствами. Для этих целей были добавлены экстракторы
Достаёт значения из XML, лежащего в CDATA
конфигурация:
{
  "type": "xcdata",
  "prefix": "/root/inner/tag", //Путь до тэга с CDATA
  "path": "/path/to" //Путь до нужного тэга
}Достаёт значения из JSON, лежащего в CDATA
конфигурация:
{
  "type": "jcdata",
  "prefix": "/root/inner/tag", //Путь до тэга с CDATA
  "path": "path.to" //Путь до нужного значения
}Иногда приходится иметь дело с запросами, в которых внутри CDATA лежит XML. В таких случаях можно заинлайнить содержимое DATA с помощью параметра inlineCData (поддерживается в xpath и xml)
{
    "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"
    }
}{
    "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"
    }
}{
    "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 ВЫРАЖЕНИЯХ
{
    "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 запросы необходимо выполнять на отдельный метод - /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"
  }
}{
  "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}"
    }
  }
}{
  "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}"
    }
  }
}в режимах 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, [_], ![_], &[_]
Как это устроено под капотом: При создании мока вложеные в запрос proto файлы парсятся и преобразуются в json-представление protobuf схемы. В базе хранится именно json-представление, а не оригинальный proto файл. Первое срабатывание мока может занимать немного больше времени, чем последующие, т.к. при первом срабатывании из json-представляения генерируется декодер protobuf сообщений. После декодирования данные преобразуются в json, который проверяется json-предикатами, задаными в поле requestPredicates. Если условия выполняются - то json из response.data (в режиме fill) сериализуется в protobuf и отдаётся в качестве ответа.
Алгоритм работы:
- Поиск мока(-ов) по имени метода
- Валидация body
- Поиск state по предикату
- Подстановка значений в шаблон ответа
- Модификация state
- Отдача response
{
    "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"
    }
}Алгоритм работы:
- Поиск мока по source
- Поиск state по предикату
- Валидация входящего сообщения
- Подстановка значений в шаблон ответа
- Модификация state
- Отправка response
- Выполнение колбеков (см. раздел "конфигурация колбеков")
Для 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 когда приходит сообщение) или отправлять дополнительные сообщения в очереди. Для этого можно использовать колбеки. Результат вызова сервиса можно при необходимости распарсить и сохранить в состояние. Коллбеки используют состяние вызвавшего.
Для 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": { .. } //Опционально
}