84

Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

  • Upload
    sqalab

  • View
    401

  • Download
    3

Embed Size (px)

Citation preview

Page 1: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни
Page 2: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

Автоматизация функционального тестирования REST API

секреты, тонкости и подводные камни

Page 3: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

Павел Асанов

QA Lead

Page 4: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

90 городов

700 тыс. пользователей

12 млн просмотров

1 млн отзывов

3 млн посетителей

Page 5: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни
Page 6: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни
Page 7: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни
Page 8: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни
Page 9: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

API как продукт

Page 10: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

API как продукт

80%

20%

Page 11: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

Регрессия

3 месяца 5 минут

Page 12: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

● Ресурсы, однозначно определяемые по URL

● Представления ресурсов (JSON)

● Методы работы с ресурсами

REST - это...

POST /reviews GET /reviews/666PUT /reviews/666PATCH /reviews/666DELETE /reviews/666

Page 13: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

Что тестируем?

Page 14: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

1. Логика

Page 15: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни
Page 16: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

отсутствие обязательных параметров

невалидный протокол

параметры запроса

скрытыйобычный скрытый с особым типом

авторизованный гость

БАК

удалённыйзаблокированный

забаненный

филиал БАК

автор неактивированный удалённый

обычный

корректные

с фото

невалидные значения

параметров

невалидные типы

параметров

БАК

expired

другой юзер

приватныйконтент

пользователь

аксесс токен

Page 17: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

отсутствие обязательных параметров

невалидный протокол

параметры запроса

скрытыйобычный скрытый с особым типом

авторизованный гость

БАК

удалённыйзаблокированный

забаненный

филиал БАК

автор неактивированный удалённый

обычный

корректные

с фото

невалидные значения

параметров

невалидные типы

параметров

БАК

expired

другой юзер

201

Авторизованный пользователь добавляет отзыв с фото

приватныйконтент

пользователь

аксесс токенPOST

Page 18: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

отсутствие обязательных параметров

невалидный протокол

параметры запроса

скрытыйобычный скрытый с особым типом

авторизованный гость

БАК

удалённыйзаблокированный

забаненный

филиал БАК

автор неактивированный удалённый

обычный

корректные

с фото

невалидные значения

параметров

невалидные типы

параметров

БАК

expired

другой юзер

200

Авторизованный владелец Бизнес-аккаунта запрашивает приватное обращение

приватныйконтент

пользователь

аксесс токен

GET

Page 19: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

отсутствие обязательных параметров

невалидный протокол

параметры запроса

скрытыйобычный скрытый с особым типом

авторизованный гость

БАК

удалённыйзаблокированный

забаненный

филиал БАК

автор неактивированный удалённый

обычный

корректные

с фото

невалидные значения

параметров

невалидные типы

параметров

БАК

expired

другой юзер

401

Гость добавляет отзыв

приватныйконтент

пользователь

аксесс токен

POST

Page 20: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

отсутствие обязательных параметров

невалидный протокол

параметры запроса

скрытыйобычный скрытый с особым типом

авторизованный гость

БАК

удалённыйзаблокированный

забаненный

филиал БАК

автор неактивированный удалённый

обычный

корректные

с фото

невалидные значения

параметров

невалидные типы

параметров

БАК

expired

другой юзер

403

Авторизованный пользователь запрашивает чужое приватное обращение

приватныйконтент

пользователь

аксесс токен

GET

Page 21: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

отсутствие обязательных параметров

невалидный протокол

параметры запроса

скрытыйобычный скрытый с особым типом

авторизованный гостьаксесс токен

БАК

удалённыйзаблокированный

забаненный

филиал БАК

автор неактивированный удалённый

обычный

корректные

с фото

невалидные значения

параметров

невалидные типы

параметров

БАК

expired

другой юзер

500 400 400 404

Невалидный запрос

приватныйконтент

пользователь

Page 22: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

2. Данные и их формат

Page 23: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

{code: 200status: "success"review: {

filial_id: "985690699651034"

text: "Для меня Code Fest - это глоток свежего воздуха. Только надо баги пофиксить."

is_recommended: false

project_id: 1

source: "flamp"

rating: 4

date_created: "2011-03-28T05:30:50+04:00"

user_id: 171

photo: null

}}

Page 24: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

{code: 200status: "success"review: {

filial_id: "985690699651034"

text: "Для меня Code Fest - это глоток свежего воздуха. Только надо баги пофиксить."

is_recommended: false

project_id: 1

source: "flamp"

rating: 4

date_created: "2011-03-28T05:30:50+04:00"

user_id: 171

photo: null

}}

точное значение

Page 25: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

{code: 200status: "success"review: {

filial_id: "985690699651034"

text: "Для меня Code Fest - это глоток свежего воздуха. Только надо баги пофиксить."

is_recommended: false

project_id: 1

source: "flamp"

rating: 4

date_created: "2011-03-28T05:30:50+04:00"

user_id: 171

photo: null

}}

точное значение

тип значения и его диапазон

Page 26: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

{code: 200status: "success"review: {

filial_id: "985690699651034"

text: "Для меня Code Fest - это глоток свежего воздуха. Только надо баги пофиксить."

is_recommended: false

project_id: 1

source: "flamp"

rating: 4

date_created: "2011-03-28T05:30:50+04:00"

user_id: 171

photo: null

}}

точное значение

enum

тип значения и его диапазон

Page 27: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

{code: 200status: "success"review: {

filial_id: "985690699651034"

text: "Для меня Code Fest - это глоток свежего воздуха. Только надо баги пофиксить."

is_recommended: false

project_id: 1

source: "flamp"

rating: 4

date_created: "2011-03-28T05:30:50+04:00"

user_id: 171

photo: null

}}

точное значение

enum

тип значения и его диапазон

формат значения

Page 28: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

{code: 200status: "success"review: {

filial_id: "985690699651034"

text: "Для меня Code Fest - это глоток свежего воздуха. Только надо баги пофиксить."

is_recommended: false

project_id: 1

source: "flamp"

rating: 4

date_created: "2011-03-28T05:30:50+04:00"

user_id: 171

photo: null

}}

точное значение

наличие атрибута

enum

тип значения и его диапазон

формат значения

Page 29: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

3. Изменение состояния системы

Page 30: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

● После DELETE сущность стала недоступна

● После логаута access token удаляется

Page 31: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

● После DELETE сущность стала недоступна

● После логаута access token удаляется

запрос сущности возвращает 404

Page 32: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

● После DELETE сущность стала недоступна

● После логаута access token удаляется

запрос с данным токеном возвращает 401

запрос сущности возвращает 404

Page 33: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

● После DELETE сущность стала недоступна

● После логаута access token удаляется

запрос с данным токеном возвращает 401или

запрос сущности возвращает 404

access token отсутствует в БД

Page 34: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

Глава первая, в которой мы знакомимся с JSON-schema

Page 35: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

JSON-схема — это…

1) готовая документация + примеры

Page 36: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

header: "Информация об отзыве",description: "Информация об отзыве",request: { type: "GET", url: "https://flamp.ru/api/2.0/reviews/{id}", properties: {

id: { type: "integer", required: true,

examples: ["1"], description: "Идентификатор отзыва" }

}},

Page 37: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

response: { type: "object", required: true, properties: {

code: "{{ common/code }}", status: "{{ common/status }}", review: "{{ objects/review }}"

}},

...

Page 38: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

2) валидация запроса на сервере

Page 39: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

3) валидация ответа на сервере в dev-режиме

Page 40: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

+ простейший smoke-тест+ полная проверка формата

Page 41: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

+ простейший smoke-тест+ полная проверка формата

- white-box- схема может содержать ошибки- нельзя использовать на продакшне

Page 42: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

Глава вторая, в которой мы добавляем assertions в JSON-

schema

Page 43: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

assertions: [ {

name: "View Review #1", request: { id: 1 }, response: { status: "success", review: {

id: 1, date_created: "_is_datetime", date_edited: "_is_datetime|null"

} }

},]

Page 44: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

assertions: [{ name: "View Review #99999999", request: { id: 99999999 }, response: { status: "error" }

}]

Page 45: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

Flamposcope

Page 46: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

+ smoke testing+ выполняется ~20 сек+ тесты пишет разработчик

Page 47: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

+ smoke testing+ выполняется ~20 сек+ тесты пишет разработчик

- только GET- захардкоженные данные- тесты пишет разработчик

Page 48: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

Глава третья, в которой мы хотели прикрутить

к JSON-схеме UI

Page 49: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни
Page 50: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни
Page 51: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

+ удобный UI+ нет кода+ автоподстановка из схемы

Page 52: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

+ удобный UI+ нет кода+ автоподстановка из схемы

- серый ящик- фикстуры- поддержка UI

Page 53: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

Глава четвёртая, в которой мы начали писать на PHP

Page 54: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

Параметризованные тестыhttps://flamp.ru/api/2.0/filials/?what=кафе&project=1&lon=71&lat=65&radius=1000&sort=relevance&limit=12&page=5&with_markers=false&building_id=1&metarubric=323&depth=1&scopes=reviews&access_token=fa07c31442508e2248fcd634a786d409428ca50d

Page 55: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

Параметризованные тестыhttps://flamp.ru/api/2.0/filials/?what=кафе&project=1&lon=71&lat=65&radius=1000&sort=relevance&limit=12&page=5&with_markers=false&building_id=1&metarubric=323&depth=1&scopes=reviews&access_token=fa07c31442508e2248fcd634a786d409428ca50d

[params_set_1] => 200,[params_set_2] => 404,[params_set_3] => 401,[params_set_4] => 403,[params_set_5] => 400,[params_set_6] => 500,...

Page 56: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

Параметризованные тестыhttps://flamp.ru/api/2.0/filials/?what=кафе&project=1&lon=71&lat=65&radius=1000&sort=relevance&limit=12&page=5&with_markers=false&building_id=1&metarubric=323&depth=1&scopes=reviews&access_token=fa07c31442508e2248fcd634a786d409428ca50d

[params_set_1] => 200,[params_set_2] => 404,[params_set_3] => 401,[params_set_4] => 403,[params_set_5] => 400,[params_set_6] => 500,...

набор N

...

набор 1 тест

Page 57: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

2fingers =PHPUnit + сomposer + http-клиент

(Guzzle поверх cURL) + данные

Page 58: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

данные

dataProvider

параметры

проверки

Структура теста

test

Page 59: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

Тестовые наборыТестовые наборыТестовые наборы

Данные

Установка параметров запроса

источник данных

Проверки

если success

ожидаемый код

ожидаемый результат

фактический результат

ожидаемый кодрольпараметры

запросаобъект

запрос

код ответа

ожидаемый ответ

тело ответа

Page 60: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

Данные

фикстуры дамп

Page 61: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

● реальные

● случайные, но однородные

● уникальные

Требования к данным

Page 62: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

Работа с БД

$entity = Db()->entity('review')->forFilial('141265770608749')

->isHidden(false)->withPhoto(true)->getRandomEntity();

$user_id = Db()->user()->withStatus(1)->getRandomUser()->id;

$article = Db()->table('articles')->isPublished(true)->getRow();

Page 63: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

Всегда ли?

'bacs' => [

'flamp_nsk' => 1,

'flamp_krsk' => 4,

'flamp_msk' => 13,

],

'filials' => [

'pac' => '141265770608749',

'pac2' => '141265771836316',

'bac' => '141265771910841',

'simple' => '141265771459351',

],

config.php

Page 64: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

assertions

$this->assertEquals($user_id, $actual->user_id, “incorrect user_id”);

$this->assertEquals($project_id, $actual->project_id, “incorrect project_id”);

$this->assertEquals($id, $actual->id, “incorrect id”);

...

$this->assert($expected, $actual);

Page 65: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

// Фактический результат$actual = $this->getResponseBody();

// Ожидаемый результат$expected = [

'user' => CHECK_STRING_NOT_EMPTY,'access_token' => CHECK_STRING_NOT_EMPTY

];

// Сравниваем ФР и ОР$this->assert($expected, $actual);

assertions

Page 66: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

// Ожидаемый результат $expected = [ 'filial_id' => $filial_id, 'user_id' => $user_id, 'text' => $text, 'date_created' => CHECK_DATETIME_FORMAT, 'date_edited' => null, 'comments_count' => 0, 'likes_score' => 0, 'source' => CHECK_SOURCE, 'filial' => CHECK_NOT_NULL, 'user' => CHECK_EXIST, 'comments' => CHECK_NOT_NULL, 'official_answer' => null, 'additional_data' => [ 'is_liked' => false, 'is_subscribed_to_comments' => false ], 'url' => CHECK_STRING_NOT_EMPTY, 'id' => CHECK_POSITIVE ];

Page 67: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

// Ожидаемый результат $expected = [ 'filial_id' => $filial_id, 'user_id' => $user_id, 'text' => $text, 'date_created' => CHECK_DATETIME_FORMAT, 'date_edited' => null, 'comments_count' => 0, 'likes_score' => 0, 'source' => CHECK_SOURCE, 'filial' => CHECK_NOT_NULL, 'user' => CHECK_EXIST, 'comments' => CHECK_NOT_NULL, 'official_answer' => null, 'additional_data' => [ 'is_liked' => false, 'is_subscribed_to_comments' => false ], 'url' => CHECK_STRING_NOT_EMPTY, 'id' => CHECK_POSITIVE ];

точное значение

точное значение

Page 68: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

// Ожидаемый результат $expected = [ 'filial_id' => $filial_id, 'user_id' => $user_id, 'text' => $text, 'date_created' => CHECK_DATETIME_FORMAT, 'date_edited' => null, 'comments_count' => 0, 'likes_score' => 0, 'source' => CHECK_SOURCE, 'filial' => CHECK_NOT_NULL, 'user' => CHECK_EXIST, 'comments' => CHECK_NOT_NULL, 'official_answer' => null, 'additional_data' => [ 'is_liked' => false, 'is_subscribed_to_comments' => false ], 'url' => CHECK_STRING_NOT_EMPTY, 'id' => CHECK_POSITIVE ];

точное значение

тип значения и его диапазон

точное значение

Page 69: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

// Ожидаемый результат $expected = [ 'filial_id' => $filial_id, 'user_id' => $user_id, 'text' => $text, 'date_created' => CHECK_DATETIME_FORMAT, 'date_edited' => null, 'comments_count' => 0, 'likes_score' => 0, 'source' => CHECK_SOURCE, 'filial' => CHECK_NOT_NULL, 'user' => CHECK_EXIST, 'comments' => CHECK_NOT_NULL, 'official_answer' => null, 'additional_data' => [ 'is_liked' => false, 'is_subscribed_to_comments' => false ], 'url' => CHECK_STRING_NOT_EMPTY, 'id' => CHECK_POSITIVE ];

точное значение

enum

тип значения и его диапазон

точное значение

Page 70: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

// Ожидаемый результат $expected = [ 'filial_id' => $filial_id, 'user_id' => $user_id, 'text' => $text, 'date_created' => CHECK_DATETIME_FORMAT, 'date_edited' => null, 'comments_count' => 0, 'likes_score' => 0, 'source' => CHECK_SOURCE, 'filial' => CHECK_NOT_NULL, 'user' => CHECK_EXIST, 'comments' => CHECK_NOT_NULL, 'official_answer' => null, 'additional_data' => [ 'is_liked' => false, 'is_subscribed_to_comments' => false ], 'url' => CHECK_STRING_NOT_EMPTY, 'id' => CHECK_POSITIVE ];

формат значения

точное значение

enum

тип значения и его диапазон

точное значение

Page 71: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

// Ожидаемый результат $expected = [ 'filial_id' => $filial_id, 'user_id' => $user_id, 'text' => $text, 'date_created' => CHECK_DATETIME_FORMAT, 'date_edited' => null, 'comments_count' => 0, 'likes_score' => 0, 'source' => CHECK_SOURCE, 'filial' => CHECK_NOT_NULL, 'user' => CHECK_EXIST, 'comments' => CHECK_NOT_NULL, 'official_answer' => null, 'additional_data' => [ 'is_liked' => false, 'is_subscribed_to_comments' => false ], 'url' => CHECK_STRING_NOT_EMPTY, 'id' => CHECK_POSITIVE ];

формат значения

точное значение

enum

тип значения и его диапазон

наличие атрибута

точное значение

Page 72: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

Примеры

Page 73: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

public function providerGetBlogAuthors()

{

// запись блога $article = Db()->table('articles')->isPublished(true)->hasAuthor(true)->getRow();

// неопубликованная запись блога $article_not_published = Db()->table('articles')->isPublished(false)->getRow();

return [

// опубликованная [$article, Config()->roles->user, 200],

// неопубликованная под авторизованным юзером [$article_not_published, Config()->roles->user, 404],

// неопубликованная под гостем [$article_not_published, Config()->roles->guest, 404],

];

}

public function testGetBlogAuthors($article, $user_id, $expected_code)

Формируем тестовые наборы

Page 74: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

/**

* @dataProvider providerGetBlogAuthors

*/

public function testGetBlogAuthors($article, $user_id, $expected_code)

{

// Задаём http-метод, метод API, параметры запроса $this->http_method = 'GET';

$this->method = "blogs/{$article->id}/authors";

// Выполняем запрос и проверяем коды ответа $this->asUser($user_id)->send();

$this->waitFor($expected_code);

Получаем авторов записи блога

Page 75: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

// проверяем поля в ответе if ($expected_code === 200) {

$actual = $this->getResponseBody()->authors;

// авторов может быть много, проверяем первого $expected[0] = [

'name' => CHECK_STRING_NOT_EMPTY,

'user' => CHECK_NOT_NULL,

];

$this->assert($expected, $actual);

}

Получаем авторов записи блога

Page 76: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

public function testDeleteReview($entity_id, $cause, $user_id, $expected_code)

{

// Задаём http-метод, метод API, параметры запроса $this->http_method = 'DELETE';

$this->method = "reviews/{$entity_id}";

$this->params = [

'cause' => $cause

];

// Выполняем запрос и проверяем коды ответа $this->asUser($user_id)->send();

$this->waitFor($expected_code);

// Проверка на повторное удаление сущности if ($expected_code === 202) {

$this->asUser($user_id)->send();

$this->waitFor(404);

}

}

Удаляем отзыв

Page 77: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

Запуск и отладка

Page 78: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

Запуск и отладка

Page 79: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

Приёмочное тестирование фичи

10-20 сек

Page 80: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

Регрессия

1400 тестов

35 000 проверок

5 минут 1400 тестов

Page 81: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

Регрессия

1400 тестов

35 000 проверок

5 минут 1400 тестов

28 860 минутвместо

Page 82: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

Процессы

● отдельный репозиторий

● все пушат, мёржат, коммитят

● новая фича покрывается при тестировании,или даже параллельно с разработкой :)

Page 83: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

Flamp API

JSON-schema

Flamposcope

2fingers

Луковица качества API

Page 84: Автоматизация функционального тестирования REST API: секреты, тонкости и подводные камни

Вопросы?