diff --git a/docs/05-testing/01-tests-list.md b/docs/05-testing/01-tests-list.md index ebab855..8e275de 100644 --- a/docs/05-testing/01-tests-list.md +++ b/docs/05-testing/01-tests-list.md @@ -1,171 +1,114 @@ -### Поставщики компонентов - -1. **Критерий приемки: Пользователь успешно создает учетную запись на платформе.** - - *Тест 1: Регистрация с корректными данными.* - - Входные параметры: корректные данные пользователя (электронная почта, пароль, имя, компания). - - Ожидаемые параметры: успешное создание учетной записи, получение подтверждения. - - Метрики: время выполнения запроса, корректность получения подтверждения. - - *Тест 2: Регистрация с некорректными данными.* - - Входные параметры: некорректные данные пользователя. - - Ожидаемые параметры: сообщение об ошибке ввода данных. - - Метрики: время выполнения запроса, корректность сообщения об ошибке. - -2. **Критерий приемки: Поставщик может добавить новое объявление с изображениями, описанием и ценой компонента.** - - *Тест 1: Добавление объявления с корректными данными.* - - Входные параметры: корректные данные о компоненте (изображения, описание, цена). - - Ожидаемые параметры: успешное добавление объявления. - - Метрики: время выполнения запроса, корректность добавления объявления. - - *Тест 2: Добавление объявления с некорректными данными.* - - Входные параметры: некорректные данные о компоненте. - - Ожидаемые параметры: сообщение об ошибке ввода данных. - - Метрики: время выполнения запроса, корректность сообщения об ошибке. - -3. **Критерий приемки: Поставщик получает уведомление о новом заказе.** - - *Тест 1: Получение уведомления о новом заказе.* - - Входные параметры: новый заказ. - - Ожидаемые параметры: уведомление о новом заказе. - - Метрики: время выполнения запроса, корректность получения уведомления. - - *Тест 2: Отсутствие уведомления при отсутствии новых заказов.* - - Входные параметры: отсутствие новых заказов. - - Ожидаемые параметры: отсутствие уведомления. - - Метрики: время выполнения запроса, корректность отсутствия уведомления. - -### Производители промышленных товаров - -1. **Критерий приемки: Производитель может использовать поиск и фильтры для нахождения нужных компонентов.** - - *Тест 1: Поиск компонентов с корректными данными.* - - Входные параметры: корректные критерии поиска (название, характеристики компонентов). - - Ожидаемые параметры: список найденных компонентов. - - Метрики: время выполнения запроса, корректность списка найденных компонентов. - - *Тест 2: Поиск компонентов с некорректными данными.* - - Входные параметры: некорректные критерии поиска. - - Ожидаемые параметры: отсутствие результатов поиска. - - Метрики: время выполнения запроса, корректность отсутствия результатов. - -2. **Критерий приемки: Производитель может добавить компоненты в корзину и оформить заказ.** - - *Тест 1: Добавление компонентов в корзину с корректными данными.* - - Входные параметры: выбранные компоненты. - - Ожидаемые параметры: успешное добавление компонентов в корзину. - - Метрики: время выполнения запроса, корректность добавления в корзину. - - *Тест 2: Добавление компонентов в корзину с некорректными данными.* - - Входные параметры: некорректные выбранные компоненты. - - Ожидаемые параметры: сообщение об ошибке при добавлении. - - Метрики: время выполнения запроса, корректность сообщения об ошибке. - -### Потребители промышленных товаров - -1. **Критерий приемки: Пользователь может использовать поиск и фильтры для нахождения нужных промышленных товаров.** - - *Тест 1: Поиск товаров с корректными данными.* - - Входные параметры: корректные критерии поиска (название, характеристики товаров). - - Ожидаемые параметры: список найденных товаров. - - Метрики: время выполнения запроса, корректность списка найденных товаров. - - *Тест 2: Поиск товаров с некорректными данными.* - - Входные параметры: некорректные критерии поиска. - - Ожидаемые параметры: отсутствие результатов поиска. - - Метрики: время выполнения запроса, корректность отсутствия результатов. - -2. **Критерий приемки: Пользователь может просматривать подробные описания и изображения товаров перед покупкой.** - - *Тест 1: Просмотр описания и изображений товара.* - - Входные параметры: выбранный товар. - - Ожидаемые параметры: подробное описание товара и его изображения. - - Метрики: время выполнения запроса, корректность отображения данных товара. - - *Тест 2: Просмотр несуществующего товара.* - - Входные параметры: несуществующий товар. - - Ожидаемые параметры: сообщение об ошибке о несуществующем товаре. - - Метрики: время выполнения запроса, корректность сообщения об ошибке. - -### Разработчики и технические специалисты - -1. **Критерий приемки: Инфраструктура платформы поддерживает высокую производительность и отказоустойчивость.** - - *Тест 1: Нагрузочное тестирование сервера.* - - Входные параметры: определенное количество одновременных запросов. - - Ожидаемые параметры: стабильная работа сервера без перебоев. - - Метрики: время выполнения запросов, процент успешных запросов. - - *Тест 2: Тестирование восстановления после сбоя.* - - Входные параметры: имитация сбоя сервера. - - Ожидаемые параметры: быстрое восстановление работы сервера. - - Метрики: время восстановления работы сервера, корректность восстановления данных. - -2. **Критерий приемки: Баги и ошибки в работе платформы максимально быстро исправляются.** - - *Тест 1: Тестирование реакции на критические ошибки.* - - Входные параметры: критическая ошибка в работе системы. - - Ожидаемые параметры: немедленная реакция разработчиков и быстрое исправление ошибки. - - Метрики: время обнаружения ошибки, время исправления ошибки. - - *Тест 2: Тестирование процесса отчетности об ошибках.* - - Входные параметры: зарегистрированная ошибка. - - Ожидаемые параметры: подтверждение получения отчета об ошибке и уведомление о ее исправлении. - - Метрики: время отправки отчета, время получения уведомления. - -### Менеджеры логистики - -1. **Критерий приемки: Время доставки компонентов минимизировано.** - - *Тест 1: Оценка времени доставки компонентов в рамках местных доставок.* - - Входные параметры: адрес доставки, местонахождение склада. - - Ожидаемые параметры: минимальное время доставки компонентов. - - Метрики: среднее время доставки, минимальное время доставки. - - *Тест 2: Оценка времени доставки компонентов в рамках международных доставок.* - - Входные параметры: страны отправления и доставки. - - Ожидаемые параметры: оптимизированное время доставки компонентов. - - Метрики: среднее время доставки, минимальное время доставки. - -2. **Критерий приемки: Стоимость доставки снижена до минимально возможного уровня.** - - *Тест 1: Оценка стоимости местных доставок.* - - Входные параметры: расстояние между складом и местом доставки. - - Ожидаемые параметры: минимальная стоимость доставки компонентов. - - Метрики: средняя стоимость доставки, минимальная стоимость доставки. - - *Тест 2: Оценка стоимости международных доставок.* - - Входные параметры: расстояние между странами отправления и доставки. - - Ожидаемые параметры: оптимизированная стоимость доставки компонентов. - - Метрики: средняя стоимость доставки, минимальная стоимость доставки. - -### Маркетинговые специалисты - -1. **Критерий приемки: Увеличение количества зарегистрированных пользователей на платформе.** - - *Тест 1: Оценка изменения количества зарегистрированных пользователей за определенный период.* - - Входные параметры: временной интервал. - - Ожидаемые параметры: увеличение числа зарегистрированных пользователей. - - Метрики: процентный рост количества пользователей, абсолютное изменение количества пользователей. - - *Тест 2: Оценка эффективности маркетинговых кампаний.* - - Входные параметры: результаты маркетинговых активностей. - - Ожидаемые параметры: повышение регистраций после проведения кампаний. - - Метрики: коэффициент конверсии, количество регистраций после кампании. - -### Юристы и правовые консультанты - -1. **Критерий приемки: Разработаны и внедрены пользовательское соглашение и политика конфиденциальности, соответствующие - требованиям законодательства.** - - *Тест 1: Анализ содержания пользовательского соглашения и политики конфиденциальности.* - - Входные параметры: текст пользовательского соглашения и политики конфиденциальности. - - Ожидаемые параметры: соответствие законодательству, отсутствие противоречий. - - Метрики: процент соответствия требованиям законодательства. - -### Финансовые аналитики и бухгалтеры - -1. **Критерий приемки: Ведение учета финансовых операций платформы в соответствии с требованиями бухгалтерского учета.** - - *Тест 1: Проверка корректности финансовых операций.* - - Входные параметры: данные о финансовых операциях. - - Ожидаемые параметры: отсутствие ошибок и несоответствий. - - Метрики: точность учета, процент ошибок в данных. - - *Тест 2: Анализ соответствия учетных данных требованиям законодательства.* - - Входные параметры: учетные данные и требования законодательства. - - Ожидаемые параметры: соответствие данных требованиям. - - Метрики: процент соответствия законодательству. - -### Владелец проекта - -1. **Критерий приемки: Разработана стратегия развития платформы с учетом текущих трендов рынка и потребностей - пользователей.** - - *Тест 1: Анализ стратегии развития.* - - Входные параметры: стратегия развития. - - Ожидаемые параметры: соответствие требованиям текущего рынка и пользователей. - - Метрики: процент соответствия требованиям рынка, удовлетворенность пользователями. - -### Инвестор - -1. **Критерий приемки: Предоставлены финансовые средства в соответствии с соглашением между инвестором и владельцем - проекта.** - - *Тест 1: Оценка соответствия предоставленных средств соглашению.* - - Входные параметры: соглашение между инвестором и владельцем проекта, предоставленные финансовые средства. - - Ожидаемые параметры: соответствие предоставленных средств условиям соглашения. - - Метрики: процент соответствия условиям соглашения, анализ плана расходов. +### 1. Регистрация на платформе: + +#### User Story: + +Как поставщик компонентов, я хочу зарегистрироваться на платформе, чтобы начать размещать свои товары. + +#### Тестовые случаи: + +1. Проверить, что форма регистрации отображается корректно. +2. Ввести корректные данные и нажать кнопку "Зарегистрироваться". +3. Убедиться, что пользователь успешно создает учетную запись на платформе. +4. Проверить, что после регистрации поставщик получает подтверждение об успешной регистрации. +5. Попробовать зарегистрироваться с некорректными данными и убедиться, что система выдает соответствующее сообщение об + ошибке. + +### 2. Управление объявлениями: + +#### User Story: + +Как поставщик компонентов, я хочу иметь возможность создавать, редактировать и удалять объявления о своих компонентах. + +#### Тестовые случаи: + +1. Проверить возможность создания нового объявления о компоненте. +2. Убедиться, что новое объявление отображается на платформе. +3. Проверить возможность редактирования существующего объявления. +4. Убедиться, что изменения в объявлении сохраняются корректно. +5. Проверить возможность удаления существующего объявления. +6. Убедиться, что объявление удаляется из системы без остатков. + +### 3. Управление заказами: + +#### User Story: + +Как поставщик компонентов, я хочу получать уведомления о новых заказах и иметь возможность управлять ими. + +#### Тестовые случаи: + +1. Проверить, что поставщик получает уведомление о новом заказе. +2. Убедиться, что список заказов отображается в учетной записи поставщика. +3. Попробовать подтвердить заказ и убедиться, что это происходит без ошибок. +4. Попробовать отклонить заказ и убедиться, что это происходит корректно. +5. Проверить, что статус заказа обновляется после подтверждения или отклонения. + +### 4. Поиск и выбор компонентов: + +#### User Story: + +Как производитель промышленных товаров, я хочу иметь возможность искать и выбирать подходящие компоненты для создания +моих продуктов. + +#### Тестовые случаи: + +1. Проверить, что функция поиска компонентов отображается на странице. +2. Попытаться использовать фильтры для поиска конкретного компонента. +3. Убедиться, что система отображает результаты поиска корректно. +4. Проверить, что производитель может просматривать подробные описания и изображения компонентов. +5. Добавить выбранные компоненты в корзину и убедиться, что это происходит без ошибок. + +### 5. Управление заказами компонентов: + +#### User Story: + +Как производитель промышленных товаров, я хочу иметь возможность заказывать необходимые компоненты и отслеживать статус +моих заказов. + +#### Тестовые случаи: + +1. Попробовать добавить компоненты в корзину и оформить заказ. +2. Убедиться, что производитель получает подтверждение о размещении заказа. +3. Проверить, что информация о статусе заказа обновляется в реальном времени. +4. Убедиться, что пользователь может отслеживать статус заказа через личный кабинет. +5. Попробовать отменить заказ и убедиться, что это происходит без ошибок. + +### 6. Поиск и покупка промышленных товаров: + +#### User Story: + +Как потребитель промышленных товаров, я хочу иметь возможность находить и покупать необходимые товары для своего +бизнеса. + +#### Тестовые случаи: + +1. Проверить, что функция поиска товаров отображается на странице. +2. Использовать фильтры для нахождения нужных промышленных товаров. +3. Убедиться, что система отображает детальные описания, характеристики и изображения товаров перед покупкой. +4. Попытаться оформить заказ и убедиться, что процесс проходит без ошибок. +5. Проверить, что пользователь получает подтверждение о размещении заказа. + +### 7. Отслеживание статуса заказа: + +#### User Story: + +Как потребитель промышленных товаров, я хочу иметь возможность отслеживать статус моих заказов для контроля над +процессом доставки. + +#### Тестовые случаи: + +1. Проверить, что пользователь может просматривать статус своих заказов через личный кабинет. +2. Убедиться, что информация о статусе заказа обновляется в реальном времени. +3. Пользователь должен получать уведомления об изменениях в статусе заказа. + +### 8. Поддержка технической инфраструктуры: + +#### User Story: + +Как разработчик или технический специалист, я хочу обеспечить надежную работу платформы для всех пользователей. + +#### Тестовые случаи: + +1. Проверить производительность и отказоустойчивость инфраструктуры платформы. +2. Убедиться, что баги и ошибки в работе платформы быстро исправляются. +3. Проверить, что безопасность данных пользователей обеспечена и конфиденциальность информации сохранена. diff --git a/docs/05-testing/test-01.01.md b/docs/05-testing/test-01.01.md new file mode 100644 index 0000000..5b9b950 --- /dev/null +++ b/docs/05-testing/test-01.01.md @@ -0,0 +1,28 @@ +### Карточка тестового случая + +**Название:** Проверка корректного отображения формы регистрации + +**Описание:** +Этот тестовый случай проверяет, что форма регистрации отображается на платформе корректно и все ее элементы доступны для +ввода данных. + +**Предусловия:** + +1. Доступ к интернету. +2. Пользователь находится на странице регистрации платформы. + +**Шаги:** + +1. Открыть веб-браузер и перейти на страницу регистрации платформы. +2. Проверить, что форма регистрации отображается на экране. +3. Убедиться, что все необходимые поля для ввода данных присутствуют на форме (например, поле для ввода электронной + почты, пароля, имени и т. д.). +4. Проверить, что форма содержит кнопку "Зарегистрироваться" или аналогичную, предназначенную для отправки данных. +5. Проверить визуальное оформление формы: соответствие цветовой схеме платформы, четкость текста, отступы и т. д. +6. Попробовать ввести тестовые данные в поля формы и убедиться, что они корректно отображаются. + +**Ожидаемый результат:** +Форма регистрации отображается корректно, все необходимые поля присутствуют и доступны для ввода данных, визуальное +оформление соответствует стандартам дизайна платформы. + +**Тип теста:** Функциональный тест, UI/UX тест diff --git a/docs/05-testing/test-01.02.md b/docs/05-testing/test-01.02.md new file mode 100644 index 0000000..dbcfe24 --- /dev/null +++ b/docs/05-testing/test-01.02.md @@ -0,0 +1,25 @@ +### Карточка тестового случая + +**Название:** Ввод корректных данных и нажатие кнопки "Зарегистрироваться" + +**Описание:** +Этот тестовый случай проверяет корректность процесса регистрации на платформе после ввода корректных данных в форму +регистрации. + +**Предусловия:** + +1. Доступ к интернету. +2. Пользователь находится на странице регистрации платформы. + +**Шаги:** + +1. Открыть веб-браузер и перейти на страницу регистрации платформы. +2. Убедиться, что форма регистрации отображается на экране. +3. Ввести корректные данные в поля формы (например, действительный адрес электронной почты, пароль, имя и т. д.). +4. Нажать кнопку "Зарегистрироваться" или аналогичную, предназначенную для отправки данных. +5. Дождаться ответа от сервера. + +**Ожидаемый результат:** +Пользователь успешно зарегистрирован на платформе после ввода корректных данных и нажатия кнопки "Зарегистрироваться". + +**Тип теста:** Функциональный тест diff --git a/docs/05-testing/test-02.01.md b/docs/05-testing/test-02.01.md new file mode 100644 index 0000000..3f626a3 --- /dev/null +++ b/docs/05-testing/test-02.01.md @@ -0,0 +1,24 @@ +### Карточка тестового случая + +**Название:** Проверка возможности создания нового объявления о компоненте + +**Описание:** +Этот тестовый случай проверяет функциональность создания нового объявления о компоненте на платформе. + +**Предусловия:** + +1. Доступ к интернету. +2. Пользователь авторизован на платформе и находится на странице управления объявлениями. + +**Шаги:** + +1. Открыть веб-браузер и перейти на страницу управления объявлениями на платформе. +2. Нажать кнопку "Создать новое объявление" или аналогичную. +3. Заполнить все обязательные поля для нового объявления (например, заголовок, описание, цена, изображения и т. д.). +4. Нажать кнопку "Сохранить" или аналогичную для создания объявления. +5. Проверить, что объявление успешно создано и отображается на платформе. + +**Ожидаемый результат:** +Новое объявление о компоненте успешно создано и отображается на платформе, все введенные данные корректно отображаются. + +**Тип теста:** Функциональный тест diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a92adc0..9f94bdd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,25 +3,58 @@ kotlin = "1.9.23" kotlinx-datetime = "0.5.0" kotlinx-serialization = "1.6.3" +coroutines = "1.8.0" binaryCompabilityValidator = "0.13.2" openapi-generator = "7.3.0" jackson = "2.16.1" + +logback = "1.5.3" +kotest = "5.8.0" +kermit = "2.0.3" + +#Frameworks +ktor = "2.3.9" + +#Testing +testcontainers = "1.19.7" + # BASE jvm-compiler = "17" jvm-language = "21" [libraries] plugin-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } - plugin-binaryCompatibilityValidator = { module = "org.jetbrains.kotlinx:binary-compatibility-validator", version.ref = "binaryCompabilityValidator" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" } kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serialization" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } +coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } +coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } jackson-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson" } jackson-datatype = { module = "com.fasterxml.jackson.datatype:jackson-datatype-jsr310", version.ref = "jackson" } + +# Logging +logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } +kermit = { module = "co.touchlab:kermit", version.ref = "kermit" } + +# Ktor +ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } +ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } + +# Testing +kotest-junit5 = { module = "io.kotest:kotest-runner-junit5", version.ref = "kotest" } +kotest-core = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" } +kotest-datatest = { module = "io.kotest:kotest-framework-datatest", version.ref = "kotest" } +kotest-property = { module = "io.kotest:kotest-property", version.ref = "kotest" } + +testcontainers-core = { module = "org.testcontainers:testcontainers", version.ref = "testcontainers" } + +[bundles] +kotest = ["kotest-junit5", "kotest-core", "kotest-datatest", "kotest-property"] + [plugins] kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } diff --git a/lessons/gradle.properties b/lessons/gradle.properties index e8aa26a..8927c78 100644 --- a/lessons/gradle.properties +++ b/lessons/gradle.properties @@ -6,3 +6,5 @@ kotlin.native.ignoreDisabledTargets=true kotlinVersion=1.9.22 coroutinesVersion=1.7.3 datetimeVersion=0.5.0 +jUnitJupiterVersion=5.10.2 +kotestVersion=5.6.1 diff --git a/lessons/m4l4-testing/build.gradle.kts b/lessons/m4l4-testing/build.gradle.kts new file mode 100644 index 0000000..5b4d3b5 --- /dev/null +++ b/lessons/m4l4-testing/build.gradle.kts @@ -0,0 +1,84 @@ +import org.gradle.api.tasks.testing.logging.TestExceptionFormat +import org.gradle.api.tasks.testing.logging.TestLogEvent + +plugins { + kotlin("multiplatform") + id("io.kotest.multiplatform") +} + +kotlin { + jvm {} + js { + browser { +// testTask { +// useMocha() +// } + } + binaries.executable() + } + + val kotestVersion: String by project + val coroutinesVersion: String by project + val jUnitJupiterVersion: String by project + + sourceSets { + val commonMain by getting { + dependencies { + implementation(kotlin("stdlib-common")) + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") + } + } + val commonTest by getting { + dependencies { + implementation(kotlin("test-common")) + implementation(kotlin("test-annotations-common")) + + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion") + + implementation("io.kotest:kotest-framework-engine:$kotestVersion") + implementation("io.kotest:kotest-framework-datatest:$kotestVersion") + implementation("io.kotest:kotest-assertions-core:$kotestVersion") + implementation("io.kotest:kotest-property:$kotestVersion") + } + } +// val jsMain by getting { +// dependencies { +// implementation(kotlin("stdlib-js")) +// } +// } + val jsTest by getting { + dependencies { + implementation(kotlin("test-js")) + } + } + val jvmMain by getting { + dependencies { + implementation(kotlin("stdlib")) + } + } + val jvmTest by getting { + dependencies { + implementation(kotlin("test-junit5")) + implementation("io.kotest:kotest-runner-junit5-jvm:$kotestVersion") + implementation("org.junit.jupiter:junit-jupiter-params:$jUnitJupiterVersion") + } + } + } +} + +tasks { + withType().configureEach { + useJUnitPlatform { +// includeTags.add("sampling") + } + filter { + isFailOnNoMatchingTests = false + } + testLogging { + showExceptions = true + showStandardStreams = true + events = setOf(TestLogEvent.FAILED, TestLogEvent.PASSED) + exceptionFormat = TestExceptionFormat.FULL + } + } +} diff --git a/lessons/m4l4-testing/src/commonMain/kotlin/DateString.kt b/lessons/m4l4-testing/src/commonMain/kotlin/DateString.kt new file mode 100644 index 0000000..7826a85 --- /dev/null +++ b/lessons/m4l4-testing/src/commonMain/kotlin/DateString.kt @@ -0,0 +1,6 @@ +data class DateString( + val iso: String +) + +//for ex 2022-04-16T07:50:06.696578 +expect fun currentDate(): DateString diff --git a/lessons/m4l4-testing/src/commonTest/kotlin/CommonTestCase.kt b/lessons/m4l4-testing/src/commonTest/kotlin/CommonTestCase.kt new file mode 100644 index 0000000..877242a --- /dev/null +++ b/lessons/m4l4-testing/src/commonTest/kotlin/CommonTestCase.kt @@ -0,0 +1,30 @@ +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.string.shouldContainOnlyDigits + +class UUIDTestCommon : FunSpec() { + init { + test("date should starts with year") { + println(currentDate().iso) + currentDate().iso + .take(4) + .shouldContainOnlyDigits() + } + + test("date should contains separator") { + currentDate().iso shouldContain "T" + } + + test("date should contains date and time") { + val data = currentDate().iso + .split("T") + + //simple checs + data.size shouldBe 2 + + data.first().filter { it == '-' }.length shouldBe 2 + data.last().filter { it == ':' } shouldBe "::" + } + } +} diff --git a/lessons/m4l4-testing/src/commonTest/kotlin/KTestTest.kt b/lessons/m4l4-testing/src/commonTest/kotlin/KTestTest.kt new file mode 100644 index 0000000..3ea3718 --- /dev/null +++ b/lessons/m4l4-testing/src/commonTest/kotlin/KTestTest.kt @@ -0,0 +1,25 @@ +import kotlin.test.* + +class KTestTest { + + @Test + fun kTest() { + assertEquals(4, 2 * 2) + } + + @Ignore + @Test + fun ignoredTest() { + println("I will never be invoked") + } + + @BeforeTest + fun beforeTest() { + println("Before Test") + } + + @AfterTest + fun afterTest() { + println("After Test") + } +} diff --git a/lessons/m4l4-testing/src/commonTest/kotlin/KotestWithBDD.kt b/lessons/m4l4-testing/src/commonTest/kotlin/KotestWithBDD.kt new file mode 100644 index 0000000..bc2a0b6 --- /dev/null +++ b/lessons/m4l4-testing/src/commonTest/kotlin/KotestWithBDD.kt @@ -0,0 +1,19 @@ +import io.kotest.core.spec.style.BehaviorSpec + +class KotestWithBDD() : BehaviorSpec({ + Given("State A") { + println("state A ") + When("Action A") { + println("in action A") + Then("State => A1") { + println("becomes A1") + } + } + When("Action B") { + println("in action B") + Then("State => B1") { + println("becomes B1") + } + } + } +}) diff --git a/lessons/m4l4-testing/src/commonTest/kotlin/KotestWithParams.kt b/lessons/m4l4-testing/src/commonTest/kotlin/KotestWithParams.kt new file mode 100644 index 0000000..a620dfa --- /dev/null +++ b/lessons/m4l4-testing/src/commonTest/kotlin/KotestWithParams.kt @@ -0,0 +1,47 @@ +import io.kotest.assertions.assertSoftly +import io.kotest.common.ExperimentalKotest +import io.kotest.common.Platform +import io.kotest.common.platform +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.core.spec.style.ShouldSpec +import io.kotest.core.spec.style.describeSpec +import io.kotest.data.forAll +import io.kotest.data.row +import io.kotest.datatest.withData +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldMatch + +class KotestWithParams : ShouldSpec({ + withData( + mapOf( + "10x2" to Triple(10, 2, 20), + "20x2" to Triple(20, 2, 40), + "30x2" to Triple(30, 2, 60), + ) + ) { (a, b, c) -> + a * b shouldBe c + } +}) + +@OptIn(ExperimentalKotest::class) +class EmailTest : DescribeSpec({ + include(emailValidation) +}) + +@ExperimentalKotest +val emailValidation = describeSpec { + +// describe("Registration").config(enabled = platform != Platform.JS) { + describe("Registration").config(enabled = platform != Platform.JS) { + context("Checking user's mail") { + forAll( + row("test@test.com"), + row("simple-user@gmail.com"), + ) { + assertSoftly { + it shouldMatch "^(.+)@(.+)\$" + } + } + } + } +} diff --git a/lessons/m4l4-testing/src/jsMain/kotlin/currentDate.kt b/lessons/m4l4-testing/src/jsMain/kotlin/currentDate.kt new file mode 100644 index 0000000..b64ebf8 --- /dev/null +++ b/lessons/m4l4-testing/src/jsMain/kotlin/currentDate.kt @@ -0,0 +1,5 @@ +import kotlin.js.Date + +actual fun currentDate(): DateString { + return DateString(Date().toISOString()) +} diff --git a/lessons/m4l4-testing/src/jsTest/kotlin/KotestTestJs.kt b/lessons/m4l4-testing/src/jsTest/kotlin/KotestTestJs.kt new file mode 100644 index 0000000..626c77f --- /dev/null +++ b/lessons/m4l4-testing/src/jsTest/kotlin/KotestTestJs.kt @@ -0,0 +1,28 @@ +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.string.shouldContainOnlyDigits + +class UUIDTestJS : StringSpec({ + "date should starts with year" { + println(currentDate().iso) // sample for sout in js + currentDate().iso + .take(4) + .shouldContainOnlyDigits() + } + + "date should contains separator" { + currentDate().iso shouldContain "T" + } + + "date should contains date and time" { + val data = currentDate().iso + .split("T") + + //simple checks + data.size shouldBe 2 + + data.first().filter { it == '-' }.length shouldBe 2 + data.last().filter { it == ':' } shouldBe "::" + } +}) diff --git a/lessons/m4l4-testing/src/jvmMain/kotlin/Message.kt b/lessons/m4l4-testing/src/jvmMain/kotlin/Message.kt new file mode 100644 index 0000000..91b4d8a --- /dev/null +++ b/lessons/m4l4-testing/src/jvmMain/kotlin/Message.kt @@ -0,0 +1,21 @@ +fun getMessage(): String { + return "Hello, my friend!" +} + +fun getMessage(name: String): String { + return if (name.uppercase() == name) { + "HELLO $name!" + } else { + "Hello $name!" + } +} + +fun getMessage(vararg name: String): String { + return if (name.find { it.uppercase() == it } != null) { + getMessage(name.joinToString(", ") { + it.uppercase() + }) + } else { + getMessage(name.joinToString(", ")) + } +} diff --git a/lessons/m4l4-testing/src/jvmMain/kotlin/currentDate.kt b/lessons/m4l4-testing/src/jvmMain/kotlin/currentDate.kt new file mode 100644 index 0000000..5ef87a3 --- /dev/null +++ b/lessons/m4l4-testing/src/jvmMain/kotlin/currentDate.kt @@ -0,0 +1,6 @@ +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +actual fun currentDate(): DateString { + return DateString(LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)) +} diff --git a/lessons/m4l4-testing/src/jvmTest/kotlin/Junit5ComplexTestCase.kt b/lessons/m4l4-testing/src/jvmTest/kotlin/Junit5ComplexTestCase.kt new file mode 100644 index 0000000..8a08e26 --- /dev/null +++ b/lessons/m4l4-testing/src/jvmTest/kotlin/Junit5ComplexTestCase.kt @@ -0,0 +1,44 @@ +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertAll +import kotlin.test.assertEquals + + +internal class Junit5ComplexTestCase { + + @Test + fun `simple test`() { + val args = listOf( + "my line is whaat?" to "my line is whaat?", + " line is whaat?" to "line is whaat?", + "line is whaat?" to "line is whaat?", + " is whaat?" to "is whaat?", + "is whaat?" to "is whaat?", + " whaat?" to "whaat?", + "whaat?" to "whaat?", + "?" to "?", + ) + + assertAll("Trimmed lines", *args.map { (expected, actual) -> + { assertEquals(expected.trimStart(), actual) } + }.toTypedArray()) + } + + @Suppress("DIVISION_BY_ZERO") + @Test + fun `test dividing by zero`() { + val exception = Assertions.assertThrows(ArithmeticException::class.java) { + 5 / 0 + } + + Assertions.assertNotNull(exception.message) + Assertions.assertTrue(exception.message!!.contains("by zero")) + } + + @Test + fun `test supplier`() { + val str = "my line" + Assertions.assertFalse(str::isEmpty) + Assertions.assertTrue(str::isNotBlank) + } +} diff --git a/lessons/m4l4-testing/src/jvmTest/kotlin/Junit5DDTTestCase.kt b/lessons/m4l4-testing/src/jvmTest/kotlin/Junit5DDTTestCase.kt new file mode 100644 index 0000000..104ff45 --- /dev/null +++ b/lessons/m4l4-testing/src/jvmTest/kotlin/Junit5DDTTestCase.kt @@ -0,0 +1,32 @@ +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.DynamicTest +import org.junit.jupiter.api.TestFactory + + +internal class Junit5DDTTestCase { + + @TestFactory + fun `test multi`() = listOf( + DynamicTest.dynamicTest("when I multiply 10*2 then I get 20") { + Assertions.assertEquals(20, 10 * 2) + }, + DynamicTest.dynamicTest("when I multiply 10*0 then I get 0") { + Assertions.assertEquals(0, 10 * 0) + }, + ) + + private val data = listOf( + Triple(1, 13, 13), + Triple(2, 21, 42), + Triple(3, 34, 102), + Triple(4, 55, 220), + Triple(5, 89, 445), + ) + + @TestFactory + fun testSquares() = data.map { (a, b, expected) -> + DynamicTest.dynamicTest("when I multiply $a*$b then I get $expected") { + Assertions.assertEquals(expected, a * b) + } + } +} diff --git a/lessons/m4l4-testing/src/jvmTest/kotlin/Junit5ParamTestCase.kt b/lessons/m4l4-testing/src/jvmTest/kotlin/Junit5ParamTestCase.kt new file mode 100644 index 0000000..b1d575f --- /dev/null +++ b/lessons/m4l4-testing/src/jvmTest/kotlin/Junit5ParamTestCase.kt @@ -0,0 +1,27 @@ +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource + +internal class Junit5ParamTestCase { + @ParameterizedTest + @MethodSource("numbers") + fun `test multiply`(a: Int, b: Int, expected: Int) { + Assertions.assertEquals(expected, a * b) + } + + @ParameterizedTest(name = "pair {index}: {0} and {1}") + @MethodSource("numbers") + fun `test multiply with custom name`(a: Int, b: Int, expected: Int) { + Assertions.assertEquals(expected, a * b) + } + + companion object { + @JvmStatic + fun numbers() = listOf( + Arguments.of(1, 1, 1), + Arguments.of(3, 14, 42), + Arguments.of(2, 71, 142), + ) + } +} diff --git a/lessons/m4l4-testing/src/jvmTest/kotlin/Junit5TagsAndCondsTestCase.kt b/lessons/m4l4-testing/src/jvmTest/kotlin/Junit5TagsAndCondsTestCase.kt new file mode 100644 index 0000000..afa2c9f --- /dev/null +++ b/lessons/m4l4-testing/src/jvmTest/kotlin/Junit5TagsAndCondsTestCase.kt @@ -0,0 +1,56 @@ +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Tags +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.condition.* +import kotlin.test.Ignore +import kotlin.test.assertEquals + + +@Tags( + Tag("junit"), + Tag("sampling") +) +internal class Junit5TagsTestCase { + @Test + fun `Log to base 2 of 8 should be equal to 3`() { + Assertions.assertEquals(25, 5 * 5) + } +} + +internal class Junit5CondsTestCase { + + @Test + @Ignore("disabled") + fun `not working rn`() = doTest(8, 11, 19) + + @Test + @EnabledOnOs(OS.MAC, disabledReason = "Not supported") + fun `mac only`() = doTest(55, 33, 88) + + @Test + @EnabledOnOs(OS.LINUX, disabledReason = "Not supported") + fun `linux only`() = doTest(11, 22, 33) + + @Test + @EnabledOnOs(OS.WINDOWS, disabledReason = "Not supported") + fun `windows only`() = doTest(55, 33, 88) + + @Test + @EnabledForJreRange(min = JRE.JAVA_8, max = JRE.JAVA_11) + fun `java 8-11 only`() = doTest(8, 11, 19) + + @Test + @EnabledIfEnvironmentVariable(named = "HOME", matches = "/Users/Kirill.Krylov") + fun `env test`() = doTest(8, 11, 19) + + @Test + @EnabledIfSystemProperty(named = "java.home", matches = "/Users/Kirill.Krylov") + fun `sys prop test`() = doTest(8, 11, 19) + + private fun doTest(a: Int, b: Int, expected: Int) { + val res = a + b + + assertEquals(expected, res) + } +} diff --git a/lessons/m4l4-testing/src/jvmTest/kotlin/Junit5TestCase.kt b/lessons/m4l4-testing/src/jvmTest/kotlin/Junit5TestCase.kt new file mode 100644 index 0000000..8ad5158 --- /dev/null +++ b/lessons/m4l4-testing/src/jvmTest/kotlin/Junit5TestCase.kt @@ -0,0 +1,79 @@ +import org.junit.jupiter.api.* +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.assertEquals + + +internal class Junit5TestCase { + @Test + fun `simple test`() { + val num1 = 123 + val num2 = 321 + + // using kotlin.test + assertEquals(444, num1 + num2) + assertEquals(444, num1 + num2, "Plus is not working") + + // using jupiter + Assertions.assertEquals(444, num1 + num2, "Plus is not working") + Assertions.assertEquals(444, num1 + num2) { + // do not create string if not necessary + "Plus is not working" + } + } +} + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class Junit5LifeCycleTestCase { + lateinit var operation: String + + @BeforeTest + fun setUpFixture() { + operation = "*" + println(operation) + } + + @Test + fun `check operation`() { + Assertions.assertEquals(operation, "*") + + val num1 = 123 + val num2 = 321 + + // using jupiter + Assertions.assertEquals(444, num1 + num2, "Plus is not working") + } + + @AfterTest + fun tearDownFixture() { + operation = "NONE" + println(operation) + } + + @AfterEach + fun afterEach() { + println("Tes is done!") + } + + @AfterEach + fun beforeEach() { + println("Tes is ready!") + } +} + +@TestMethodOrder(MethodOrderer.OrderAnnotation::class) +internal class Junit5OrderTestCase { + @Test + @DisplayName("my custom first test") + @Order(1) + fun firstTest() { + // ... + } + + @Test + @DisplayName("my custom second test with super-cool name") + @Order(2) + fun secondTest() { + // ... + } +} diff --git a/lessons/m4l4-testing/src/jvmTest/kotlin/KotestTestJVM.kt b/lessons/m4l4-testing/src/jvmTest/kotlin/KotestTestJVM.kt new file mode 100644 index 0000000..ee671b2 --- /dev/null +++ b/lessons/m4l4-testing/src/jvmTest/kotlin/KotestTestJVM.kt @@ -0,0 +1,28 @@ +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.string.shouldContainOnlyDigits + +class UUIDTestJVM : StringSpec({ + "date should starts with year" { + println(currentDate().iso) + currentDate().iso + .take(4) + .shouldContainOnlyDigits() + } + + "date should contains separator" { + currentDate().iso shouldContain "T" + } + + "date should conains date and time" { + val data = currentDate().iso + .split("T") + + //simple checs + data.size shouldBe 2 + + data.first().filter { it == '-' }.length shouldBe 2 + data.last().filter { it == ':' } shouldBe "::" + } +}) diff --git a/lessons/m4l4-testing/src/jvmTest/kotlin/MessageTestCase.kt b/lessons/m4l4-testing/src/jvmTest/kotlin/MessageTestCase.kt new file mode 100644 index 0000000..13722c5 --- /dev/null +++ b/lessons/m4l4-testing/src/jvmTest/kotlin/MessageTestCase.kt @@ -0,0 +1,53 @@ +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +class MessageTestCase { + @Test + fun `test message is string`() { + val msg = getMessage() + + Assertions.assertInstanceOf(String::class.java, msg) + } + + @Test + fun `test message with name`() { + val msg = getMessage("Bob") + + Assertions.assertEquals("Hello Bob!", msg) + } + + @Test + fun `test message with another name`() { + val msg = getMessage("Ann") + + Assertions.assertEquals("Hello Ann!", msg) + } + + @Test + fun `test message with empty name`() { + val msg = getMessage() + + Assertions.assertEquals("Hello, my friend!", msg) + } + + @Test + fun `test message with CAPS name`() { + val msg = getMessage("JANE") + + Assertions.assertEquals("HELLO JANE!", msg) + } + + @Test + fun `test message with multiple names`() { + val msg = getMessage("Bob", "Anna") + + Assertions.assertEquals("Hello Bob, Anna!", msg) + } + + @Test + fun `test message with multiple names with CAPS`() { + val msg = getMessage("Bob", "Anna", "JANE") + + Assertions.assertEquals("HELLO BOB, ANNA, JANE!", msg) + } +} diff --git a/lessons/settings.gradle.kts b/lessons/settings.gradle.kts index dca1c1c..48d279c 100644 --- a/lessons/settings.gradle.kts +++ b/lessons/settings.gradle.kts @@ -1,7 +1,9 @@ pluginManagement { plugins { val kotlinVersion: String by settings + val kotestVersion: String by settings kotlin("jvm") version kotlinVersion + id("io.kotest.multiplatform") version kotestVersion } } @@ -21,6 +23,7 @@ include("m2l3-kmp") include("m2l4-1-interop") include("m2l4-2-jni") include("m2l5-gradle") +include("m4l4-testing") include(":m2l5-gradle:sub1:ssub1", ":m2l5-gradle:sub1:ssub2") diff --git a/ok-marketplace-tests/.gitignore b/ok-marketplace-tests/.gitignore new file mode 100644 index 0000000..d635568 --- /dev/null +++ b/ok-marketplace-tests/.gitignore @@ -0,0 +1,8 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +/.idea/ +/kotlin-js-store/ diff --git a/ok-marketplace-tests/build.gradle.kts b/ok-marketplace-tests/build.gradle.kts new file mode 100644 index 0000000..e5a9767 --- /dev/null +++ b/ok-marketplace-tests/build.gradle.kts @@ -0,0 +1,33 @@ +plugins { + alias(libs.plugins.kotlin.jvm) apply false + alias(libs.plugins.kotlin.multiplatform) apply false +} + +group = "ru.otus.otuskotlin.marketplace.tests" +version = "0.0.1" + +allprojects { + repositories { + mavenCentral() + } +} + +subprojects { + group = rootProject.group + version = rootProject.version +} + +ext { + val specDir = layout.projectDirectory.dir("../specs") + set("spec-v1", specDir.file("specs-ad-v1.yaml").toString()) + set("spec-v2", specDir.file("specs-ad-v2.yaml").toString()) +} + +tasks { + arrayOf("build", "clean", "check").forEach {tsk -> + create(tsk) { + group = "build" + dependsOn(subprojects.map { it.getTasksByName(tsk,false)}) + } + } +} diff --git a/ok-marketplace-tests/gradle.properties b/ok-marketplace-tests/gradle.properties new file mode 100644 index 0000000..28db788 --- /dev/null +++ b/ok-marketplace-tests/gradle.properties @@ -0,0 +1,3 @@ +kotlin.code.style=official +kotlin.native.ignoreDisabledTargets=true +#kotlin.native.cacheKind.linuxX64=none diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/build.gradle.kts b/ok-marketplace-tests/ok-marketplace-e2e-be/build.gradle.kts new file mode 100644 index 0000000..8f34904 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/build.gradle.kts @@ -0,0 +1,39 @@ +plugins { + kotlin("jvm") +} + +dependencies { + implementation(kotlin("stdlib")) + + implementation("ru.otus.otuskotlin.marketplace:ok-marketplace-api-v1-jackson") + implementation("ru.otus.otuskotlin.marketplace:ok-marketplace-api-v2-kmp") + + testImplementation(libs.logback) + testImplementation(libs.kermit) + + testImplementation(libs.bundles.kotest) + +// implementation("com.rabbitmq:amqp-client:$rabbitVersion") +// implementation("org.apache.kafka:kafka-clients:$kafkaVersion") + + testImplementation(libs.testcontainers.core) + testImplementation(libs.coroutines.core) + + testImplementation(libs.ktor.client.core) + testImplementation(libs.ktor.client.okhttp) +// testImplementation("io.ktor:ktor-client-core:$ktorVersion") +// testImplementation("io.ktor:ktor-client-okhttp:$ktorVersion") +// testImplementation("io.ktor:ktor-client-okhttp-jvm:$ktorVersion") +} + +var severity: String = "MINOR" + +tasks { + withType().configureEach { + useJUnitPlatform() +// dependsOn(gradle.includedBuild(":ok-marketplace-app-spring").task("dockerBuildImage")) +// dependsOn(gradle.includedBuild(":ok-marketplace-app-ktor").task("publishImageToLocalRegistry")) +// dependsOn(gradle.includedBuild(":ok-marketplace-app-rabbit").task("dockerBuildImage")) +// dependsOn(gradle.includedBuild(":ok-marketplace-app-kafka").task("dockerBuildImage")) + } +} diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/docker-compose-kafka.yml b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/docker-compose-kafka.yml new file mode 100644 index 0000000..ac6c84a --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/docker-compose-kafka.yml @@ -0,0 +1,71 @@ +version: '2.4' +services: + zookeeper: + image: confluentinc/cp-zookeeper:7.0.9 + healthcheck: + test: "[[ $$(echo srvr | nc localhost 2181 | grep -oG 'Mode: standalone') = \"Mode: standalone\" ]]" + interval: 10s + timeout: 1s + retries: 30 + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + ports: + - "2181:2181" + networks: + - app-tier + + kafka: + image: confluentinc/cp-kafka:7.0.9 + depends_on: + zookeeper: + condition: service_healthy + healthcheck: + test: "test $$( /usr/bin/zookeeper-shell zookeeper:2181 get /brokers/ids/1 | grep { ) != ''" + interval: 3s + timeout: 2s + retries: 300 + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181' + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:9091 + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 + KAFKA_JMX_PORT: 9101 + KAFKA_JMX_HOSTNAME: localhost + ports: + - "9092:9092" + - "9091:9091" + - "9101:9101" + networks: + - app-tier + + kafdrop: + image: obsidiandynamics/kafdrop + restart: "no" + ports: + - "9000:9000" + environment: + KAFKA_BROKERCONNECT: "kafka:9092" + JVM_OPTS: "-Xms16M -Xmx48M -Xss180K -XX:-TieredCompilation -XX:+UseStringDeduplication -noverify" + depends_on: + - "kafka" + networks: + - app-tier + + app-kafka: + image: ok-marketplace-app-kafka:latest + depends_on: + kafka: + condition: service_healthy + environment: + KAFKA_HOSTS: "kafka:9092" + networks: + - app-tier + +networks: + app-tier: + driver: bridge \ No newline at end of file diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/docker-compose-ktor.yml b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/docker-compose-ktor.yml new file mode 100644 index 0000000..0190cd7 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/docker-compose-ktor.yml @@ -0,0 +1,8 @@ +# Конфигурация для spring + (в перспективе) postgresql + +version: '3' +services: + app-ktor: + image: ok-marketplace-app-ktor-ktor:1.0-SNAPSHOT + ports: + - "8080:8080" diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/docker-compose-rabbit.yml b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/docker-compose-rabbit.yml new file mode 100644 index 0000000..a16bb91 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/docker-compose-rabbit.yml @@ -0,0 +1,33 @@ +# Конфигурация для rabbit + (в перспективе) postgresql + +version: '3' +services: + app-rabbit: + image: ok-marketplace-app-rabbit:latest + depends_on: + rabbit: + condition: service_healthy + environment: + RABBIT_HOST: rabbit + networks: + - app-tier + + rabbit: + image: rabbitmq:3.11.14-management + ports: + - "5672:5672" + - "15672:15672" + environment: + RABBITMQ_DEFAULT_USER: guest + RABBITMQ_DEFAULT_PASS: guest + healthcheck: + test: rabbitmq-diagnostics -q ping + interval: 10s + timeout: 10s + retries: 3 + networks: + - app-tier + +networks: + app-tier: + driver: bridge \ No newline at end of file diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/docker-compose-spring.yml b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/docker-compose-spring.yml new file mode 100644 index 0000000..0b7fa2c --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/docker-compose-spring.yml @@ -0,0 +1,8 @@ +# Конфигурация для spring + (в перспективе) postgresql + +version: '3' +services: + app-spring: + image: ok-marketplace-app-spring:latest + ports: + - "8080:8080" diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/docker-compose-wiremock.yml b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/docker-compose-wiremock.yml new file mode 100644 index 0000000..650745b --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/docker-compose-wiremock.yml @@ -0,0 +1,12 @@ +# Конфигурация для spring + (в перспективе) postgresql + +version: '3' +services: + app-wiremock: + image: wiremock/wiremock:3.4.2 + ports: + - "8080:8080" + volumes: +# - ./__files:/home/wiremock/__files + - ./volumes/wm-marketplace/mappings:/home/wiremock/mappings +# entrypoint: ["/docker-entrypoint.sh", "--global-response-templating", "--disable-gzip", "--verbose"] diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/root.json b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/root.json new file mode 100644 index 0000000..5d14de1 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/root.json @@ -0,0 +1,14 @@ +{ + "request": { + "method": "GET", + "url": "/" + }, + + "response": { + "status": 200, + "body": "Hello, world!", + "headers": { + "Content-Type": "text/plain" + } + } +} diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v1-create.json b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v1-create.json new file mode 100644 index 0000000..33562ea --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v1-create.json @@ -0,0 +1,25 @@ +{ + "request": { + "method": "POST", + "url": "/v1/ad/create" + }, + + "response": { + "status": 200, + "jsonBody": { + "responseType": "create", + "result": "success", + "ad": { + "id": "123", + "title": "{{{jsonPath request.body '$.ad.title'}}}", + "description": "Требуется болт 100x5 с шестигранной шляпкой", + "adType": "{{{jsonPath request.body '$.ad.adType'}}}", + "visibility": "public" + } + }, + "headers": { + "Content-Type": "application/json" + }, + "transformers": ["response-template"] + } +} diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v1-delete.json b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v1-delete.json new file mode 100644 index 0000000..be537f3 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v1-delete.json @@ -0,0 +1,24 @@ +{ + "request": { + "method": "POST", + "url": "/v1/ad/delete" + }, + "response": { + "status": 200, + "jsonBody": { + "responseType": "delete", + "result": "success", + "ad": { + "id": "{{{jsonPath request.body '$.ad.id'}}}", + "title": "Требуется болт", + "description": "Требуется болт 100x5 с шестигранной шляпкой", + "adType": "demand", + "visibility": "public" + } + }, + "headers": { + "Content-Type": "application/json" + }, + "transformers": ["response-template"] + } +} diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v1-offers.json b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v1-offers.json new file mode 100644 index 0000000..605755e --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v1-offers.json @@ -0,0 +1,39 @@ +{ + "request": { + "method": "POST", + "url": "/v1/ad/offers" + }, + "response": { + "status": 200, + "jsonBody": { + "responseType": "offers", + "result": "success", + "ad": { + "id": "123", + "title": "Требуется болт", + "description": "Требуется болт 100x5 с шестигранной шляпкой", + "adType": "supply", + "visibility": "public" + }, + "ads": [ + { + "id": "123", + "title": "Selling Bolt", + "description": "Требуется болт 100x5 с шестигранной шляпкой", + "adType": "demand", + "visibility": "public" + }, + { + "id": "124", + "title": "Selling Nut", + "description": "Требуется болт 100x5 с шестигранной шляпкой", + "adType": "demand", + "visibility": "public" + } + ] + }, + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v1-read.json b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v1-read.json new file mode 100644 index 0000000..6e8e92e --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v1-read.json @@ -0,0 +1,25 @@ +{ + "request": { + "method": "POST", + "url": "/v1/ad/read" + }, + + "response": { + "status": 200, + "jsonBody": { + "responseType": "read", + "result": "success", + "ad": { + "id": "{{{jsonPath request.body '$.ad.id'}}}", + "title": "Требуется болт", + "description": "Требуется болт 100x5 с шестигранной шляпкой", + "adType": "demand", + "visibility": "public" + } + }, + "headers": { + "Content-Type": "application/json" + }, + "transformers": ["response-template"] + } +} diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v1-search-bolt.json b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v1-search-bolt.json new file mode 100644 index 0000000..190d9fc --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v1-search-bolt.json @@ -0,0 +1,28 @@ +{ + "request": { + "method": "POST", + "url": "/v1/ad/search", + "bodyPatterns": [ + {"matchesJsonPath" : "$.adFilter[?(@.searchString == 'Bolt')]"} + ] + }, + "response": { + "status": 200, + "jsonBody": { + "responseType": "search", + "result": "success", + "ads": [ + { + "id": "123", + "title": "Selling Bolt", + "description": "Требуется болт 100x5 с шестигранной шляпкой", + "adType": "demand", + "visibility": "public" + } + ] + }, + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v1-search-selling.json b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v1-search-selling.json new file mode 100644 index 0000000..75afa99 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v1-search-selling.json @@ -0,0 +1,35 @@ +{ + "request": { + "method": "POST", + "url": "/v1/ad/search", + "bodyPatterns": [ + {"matchesJsonPath" : "$.adFilter[?(@.searchString == 'Selling')]"} + ] + }, + "response": { + "status": 200, + "jsonBody": { + "responseType": "search", + "result": "success", + "ads": [ + { + "id": "123", + "title": "Selling Bolt", + "description": "Требуется болт 100x5 с шестигранной шляпкой", + "adType": "demand", + "visibility": "public" + }, + { + "id": "124", + "title": "Selling Nut", + "description": "Требуется болт 100x5 с шестигранной шляпкой", + "adType": "demand", + "visibility": "public" + } + ] + }, + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v1-search.json b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v1-search.json new file mode 100644 index 0000000..aba8ba7 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v1-search.json @@ -0,0 +1,32 @@ +{ + "request": { + "method": "POST", + "url": "/v1/ad/search" + }, + "response": { + "status": 200, + "jsonBody": { + "responseType": "search", + "result": "success", + "ads": [ + { + "id": "123", + "title": "Selling Bolt", + "description": "Требуется болт 100x5 с шестигранной шляпкой", + "adType": "demand", + "visibility": "public" + }, + { + "id": "124", + "title": "Selling Nut", + "description": "Требуется болт 100x5 с шестигранной шляпкой", + "adType": "demand", + "visibility": "public" + } + ] + }, + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v1-update.json b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v1-update.json new file mode 100644 index 0000000..a8effb2 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v1-update.json @@ -0,0 +1,24 @@ +{ + "request": { + "method": "POST", + "url": "/v1/ad/update" + }, + + "response": { + "status": 200, + "jsonBody": { + "responseType": "update", + "result": "success", + "ad": { + "id": "123", + "title": "Selling Nut", + "description": "Требуется болт 100x5 с шестигранной шляпкой", + "adType": "demand", + "visibility": "public" + } + }, + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-create.json b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-create.json new file mode 100644 index 0000000..a35cc86 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-create.json @@ -0,0 +1,25 @@ +{ + "request": { + "method": "POST", + "url": "/v2/ad/create" + }, + + "response": { + "status": 200, + "jsonBody": { + "responseType": "create", + "result": "success", + "ad": { + "id": "123", + "title": "{{{jsonPath request.body '$.ad.title'}}}", + "description": "Требуется болт 100x5 с шестигранной шляпкой", + "adType": "{{{jsonPath request.body '$.ad.adType'}}}", + "visibility": "public" + } + }, + "headers": { + "Content-Type": "application/json" + }, + "transformers": ["response-template"] + } +} diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-delete.json b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-delete.json new file mode 100644 index 0000000..a3d7839 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-delete.json @@ -0,0 +1,24 @@ +{ + "request": { + "method": "POST", + "url": "/v2/ad/delete" + }, + "response": { + "status": 200, + "jsonBody": { + "responseType": "delete", + "result": "success", + "ad": { + "id": "{{{jsonPath request.body '$.ad.id'}}}", + "title": "Требуется болт", + "description": "Требуется болт 100x5 с шестигранной шляпкой", + "adType": "demand", + "visibility": "public" + } + }, + "headers": { + "Content-Type": "application/json" + }, + "transformers": ["response-template"] + } +} diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-offers.json b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-offers.json new file mode 100644 index 0000000..c38adf7 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-offers.json @@ -0,0 +1,39 @@ +{ + "request": { + "method": "POST", + "url": "/v2/ad/offers" + }, + "response": { + "status": 200, + "jsonBody": { + "responseType": "offers", + "result": "success", + "ad": { + "id": "123", + "title": "Требуется болт", + "description": "Требуется болт 100x5 с шестигранной шляпкой", + "adType": "supply", + "visibility": "public" + }, + "ads": [ + { + "id": "123", + "title": "Selling Bolt", + "description": "Требуется болт 100x5 с шестигранной шляпкой", + "adType": "demand", + "visibility": "public" + }, + { + "id": "124", + "title": "Selling Nut", + "description": "Требуется болт 100x5 с шестигранной шляпкой", + "adType": "demand", + "visibility": "public" + } + ] + }, + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-read.json b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-read.json new file mode 100644 index 0000000..cfeff7a --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-read.json @@ -0,0 +1,25 @@ +{ + "request": { + "method": "POST", + "url": "/v2/ad/read" + }, + + "response": { + "status": 200, + "jsonBody": { + "responseType": "read", + "result": "success", + "ad": { + "id": "{{{jsonPath request.body '$.ad.id'}}}", + "title": "Требуется болт", + "description": "Требуется болт 100x5 с шестигранной шляпкой", + "adType": "demand", + "visibility": "public" + } + }, + "headers": { + "Content-Type": "application/json" + }, + "transformers": ["response-template"] + } +} diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-search-bolt.json b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-search-bolt.json new file mode 100644 index 0000000..717c318 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-search-bolt.json @@ -0,0 +1,28 @@ +{ + "request": { + "method": "POST", + "url": "/v2/ad/search", + "bodyPatterns": [ + {"matchesJsonPath" : "$.adFilter[?(@.searchString == 'Bolt')]"} + ] + }, + "response": { + "status": 200, + "jsonBody": { + "responseType": "search", + "result": "success", + "ads": [ + { + "id": "123", + "title": "Selling Bolt", + "description": "Требуется болт 100x5 с шестигранной шляпкой", + "adType": "demand", + "visibility": "public" + } + ] + }, + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-search-selling.json b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-search-selling.json new file mode 100644 index 0000000..5d883fc --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-search-selling.json @@ -0,0 +1,35 @@ +{ + "request": { + "method": "POST", + "url": "/v2/ad/search", + "bodyPatterns": [ + {"matchesJsonPath" : "$.adFilter[?(@.searchString == 'Selling')]"} + ] + }, + "response": { + "status": 200, + "jsonBody": { + "responseType": "search", + "result": "success", + "ads": [ + { + "id": "123", + "title": "Selling Bolt", + "description": "Требуется болт 100x5 с шестигранной шляпкой", + "adType": "demand", + "visibility": "public" + }, + { + "id": "124", + "title": "Selling Nut", + "description": "Требуется болт 100x5 с шестигранной шляпкой", + "adType": "demand", + "visibility": "public" + } + ] + }, + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-update.json b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-update.json new file mode 100644 index 0000000..48f5d28 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-update.json @@ -0,0 +1,25 @@ +{ + "request": { + "method": "POST", + "url": "/v2/ad/update" + }, + + "response": { + "status": 200, + "jsonBody": { + "responseType": "update", + "result": "success", + "ad": { + "id": "123", + "title": "Selling Nut", + "description": "Требуется болт 100x5 с шестигранной шляпкой", + "adType": "demand", + "visibility": "public" + } + }, + "headers": { + "Content-Type": "application/json" + }, + "transformers": ["response-template"] + } +} diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/readme.md b/ok-marketplace-tests/ok-marketplace-e2e-be/readme.md new file mode 100644 index 0000000..9cab867 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/readme.md @@ -0,0 +1,38 @@ +Приемочные тесты + +# Целевое решение + +С использованием docker-compose запускаются наши приложения (spring, ktor, kafka и т.п.), поднимается база данных и +тестируются обе версии API путем отправки соответствующих запросов и проверки ответов. + +Проект зависит только от транспортных моделей (чтобы было удобно отправлять запросы и проверять ответы). + +# Roadmap + +1. Оснастка и проверка на Wiremock +2. Только spring и без БД +3. Добавляем Ktor +4. Добавляем Rabbit +5. Добавляем Kafka +6. При появлении работы с БД + * раскомментируем и убираем все // TODO + * в каждое из приложений выше добавляем БД + * добавляем очистку БД + +# Организация проекта + +* `docker` - обертки над соответствующими docker-compose +* `fixture` - оснастка + * `docker` - оснастка для docker + * `client` - клиенты для разных протоколов +* `test` - сами тесты + * `AccRestTest` - запуск http/rest тестов для spring и ktor + * `testVx` - тесты для соотв. версии АПИ + * `action.vx` - примитивные действия для тестов и вспомогательные matcher-ы + +# Поведение тестов + +* Тесты разбиты на отдельные классы (наследники BaseFunSpec, который основан на FunSpec). +* Перед запуском тестов в классе поднимается соответствующий docker-compose +* После завершения тестов в классе docker-compose завершается +* Перед каждым тестом выполняется очистка базы данных, чтобы сделать тесты независимыми друг от друга diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/SimpleWiremockRootTest.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/SimpleWiremockRootTest.kt new file mode 100644 index 0000000..a6c2cbb --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/SimpleWiremockRootTest.kt @@ -0,0 +1,69 @@ +package ru.otus.otuskotlin.marketplace.e2e.be + +import co.touchlab.kermit.Logger +import io.kotest.common.ExperimentalKotest +import io.kotest.core.spec.IsolationMode +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe +import io.ktor.client.* +import io.ktor.client.engine.okhttp.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import org.testcontainers.containers.DockerComposeContainer +import java.io.File + +private val log = Logger + +/** + * Простейший демонстрационный тест для запуска с использованием wiremock + * Предназначен для первоначального ознакомления со стеком + */ +@OptIn(ExperimentalKotest::class) +class SimpleWiremockRootTest : StringSpec({ + this.blockingTest = true + isolationMode = IsolationMode.SingleInstance + beforeSpec { start() } + + "Root GET method is working" { + val client = HttpClient(OkHttp) + val response = client.get(getUrl().buildString()) + val bodyString = response.call.response.bodyAsText() + bodyString shouldBe "Hello, world!" + } + + afterSpec { stop() } +}) { + companion object { + private val service = "app-wiremock_1" + private val port = 8080 + + private val compose by lazy { + DockerComposeContainer(File("docker-compose/docker-compose-wiremock.yml")).apply { + withOptions("--compatibility") + withExposedService(service, port) + withLocalCompose(true) + } + } + + fun start() { + kotlin.runCatching { compose.start() }.onFailure { + log.e { "Failed to start wiremock" } + throw it + } + log.w("\n=========== wiremock started =========== \n") + } + + fun stop() { + compose.close() + log.w("\n=========== wiremock complete =========== \n") + } + + private fun getUrl() = URLBuilder( + protocol = URLProtocol.HTTP, + host = compose.getServiceHost(service, port), + port = compose.getServicePort(service, port), + ) + + } +} diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/docker/KafkaDockerCompose.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/docker/KafkaDockerCompose.kt new file mode 100644 index 0000000..4b64b6e --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/docker/KafkaDockerCompose.kt @@ -0,0 +1,7 @@ +package ru.otus.otuskotlin.marketplace.e2e.be.docker + +import ru.otus.otuskotlin.marketplace.e2e.be.fixture.docker.AbstractDockerCompose + +object KafkaDockerCompose : AbstractDockerCompose( + "kafka_1", 9091, "docker-compose-kafka.yml" +) diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/docker/KtorDockerCompose.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/docker/KtorDockerCompose.kt new file mode 100644 index 0000000..8a6560d --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/docker/KtorDockerCompose.kt @@ -0,0 +1,7 @@ +package ru.otus.otuskotlin.marketplace.e2e.be.docker + +import ru.otus.otuskotlin.marketplace.e2e.be.fixture.docker.AbstractDockerCompose + +object KtorDockerCompose : AbstractDockerCompose( + "app-ktor_1", 8080, "docker-compose-ktor.yml" +) diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/docker/RabbitDockerCompose.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/docker/RabbitDockerCompose.kt new file mode 100644 index 0000000..d839e91 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/docker/RabbitDockerCompose.kt @@ -0,0 +1,12 @@ +package ru.otus.otuskotlin.marketplace.e2e.be.docker + +import ru.otus.otuskotlin.marketplace.e2e.be.fixture.docker.AbstractDockerCompose + +object RabbitDockerCompose : AbstractDockerCompose( + "rabbit_1", 5672, "docker-compose-rabbit.yml" +) { + override val user: String + get() = "guest" + override val password: String + get() = "guest" +} diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/docker/SpringDockerCompose.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/docker/SpringDockerCompose.kt new file mode 100644 index 0000000..1490594 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/docker/SpringDockerCompose.kt @@ -0,0 +1,7 @@ +package ru.otus.otuskotlin.marketplace.e2e.be.docker + +import ru.otus.otuskotlin.marketplace.e2e.be.fixture.docker.AbstractDockerCompose + +object SpringDockerCompose : AbstractDockerCompose( + "app-spring_1", 8080, "docker-compose-spring.yml" +) diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/docker/WiremockDockerCompose.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/docker/WiremockDockerCompose.kt new file mode 100644 index 0000000..a329639 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/docker/WiremockDockerCompose.kt @@ -0,0 +1,7 @@ +package ru.otus.otuskotlin.marketplace.e2e.be.docker + +import ru.otus.otuskotlin.marketplace.e2e.be.fixture.docker.AbstractDockerCompose + +object WiremockDockerCompose : AbstractDockerCompose( + "app-wiremock_1", 8080, "docker-compose-wiremock.yml" +) diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/fixture/BaseFunSpec.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/fixture/BaseFunSpec.kt new file mode 100644 index 0000000..778ed35 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/fixture/BaseFunSpec.kt @@ -0,0 +1,28 @@ +package ru.otus.otuskotlin.marketplace.e2e.be.fixture + +import io.kotest.core.spec.Spec +import io.kotest.core.spec.style.FunSpec +import io.kotest.core.test.TestCase +import ru.otus.otuskotlin.marketplace.blackbox.fixture.docker.DockerCompose + +/** + * Базовая реализация тестов, которая выполняет запуск и останов контейнеров, а также очистку БД. + * Основана на FunSpec + */ +abstract class BaseFunSpec( + private val dockerCompose: DockerCompose, + body: FunSpec.() -> Unit +) : FunSpec(body) { + + override suspend fun beforeSpec(spec: Spec) { + dockerCompose.start() + } + + override suspend fun afterSpec(spec: Spec) { + dockerCompose.stop() + } + + override suspend fun beforeEach(testCase: TestCase) { + dockerCompose.clearDb() + } +} diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/fixture/client/Client.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/fixture/client/Client.kt new file mode 100644 index 0000000..1ffc716 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/fixture/client/Client.kt @@ -0,0 +1,15 @@ +package ru.otus.otuskotlin.marketplace.e2e.be.fixture.client + +/** + * Клиент к нашему приложению в докер-композе, который умеет отправлять запрос и получать ответ. + * Способ отправки/получения зависит от приложения - rabbit, http, ws, ... + */ +interface Client { + /** + * @param version версия АПИ (v1) + * @param path путь к ресурсу, имя топика и т.п. (ad/create) + * @param request тело сообщения в виде строки + * @return тело ответа + */ + suspend fun sendAndReceive(version: String, path: String, request: String): String +} diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/fixture/client/KafkaClient.kt_ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/fixture/client/KafkaClient.kt_ new file mode 100644 index 0000000..3a88975 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/fixture/client/KafkaClient.kt_ @@ -0,0 +1,58 @@ +package ru.otus.otuskotlin.marketplace.blackbox.fixture.client + +import org.apache.kafka.clients.consumer.ConsumerConfig +import org.apache.kafka.clients.consumer.KafkaConsumer +import org.apache.kafka.clients.producer.KafkaProducer +import org.apache.kafka.clients.producer.ProducerConfig +import org.apache.kafka.clients.producer.ProducerRecord +import org.apache.kafka.common.serialization.StringDeserializer +import org.apache.kafka.common.serialization.StringSerializer +import ru.otus.otuskotlin.marketplace.blackbox.fixture.docker.DockerCompose +import java.time.Duration +import java.util.UUID + +/** + * Отправка запросов в очереди kafka + */ +class KafkaClient(dockerCompose: DockerCompose) : Client { + private val host by lazy { + val url = dockerCompose.inputUrl + "${url.host}:${url.port}" + } + private val producer by lazy { + KafkaProducer( + mapOf( + ProducerConfig.BOOTSTRAP_SERVERS_CONFIG to host, + ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG to StringSerializer::class.java, + ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG to StringSerializer::class.java + ) + ) + } + private val consumer by lazy { + KafkaConsumer( + mapOf( + ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG to host, + ConsumerConfig.GROUP_ID_CONFIG to UUID.randomUUID().toString(), + ConsumerConfig.AUTO_OFFSET_RESET_CONFIG to "earliest", + ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG to StringDeserializer::class.java, + ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG to StringDeserializer::class.java + ) + ).also { + it.subscribe(versions.map { "marketplace-out-$it" }) + } + } + private var counter = 0 + private val versions = setOf("v1", "v2") + + override suspend fun sendAndReceive(version: String, path: String, request: String): String { + if (version !in versions) { + throw UnsupportedOperationException("Unknown version $version") + } + + counter += 1 + producer.send(ProducerRecord("marketplace-in-$version", "test-$counter", request)).get() + + val read = consumer.poll(Duration.ofSeconds(20)) + return read.firstOrNull()?.value() ?: "" + } +} \ No newline at end of file diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/fixture/client/RabbitClient.kt_ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/fixture/client/RabbitClient.kt_ new file mode 100644 index 0000000..39d7cae --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/fixture/client/RabbitClient.kt_ @@ -0,0 +1,58 @@ +package ru.otus.otuskotlin.marketplace.blackbox.fixture.client + +import com.rabbitmq.client.CancelCallback +import com.rabbitmq.client.ConnectionFactory +import com.rabbitmq.client.DeliverCallback +import io.kotest.common.runBlocking +import kotlinx.coroutines.channels.Channel +import ru.otus.otuskotlin.marketplace.blackbox.fixture.docker.DockerCompose + +/** + * Клиент, работающий через rabbit-mq + * Запросы уходят в $version-queue, а ответы читаются из $version-queue-out + */ +class RabbitClient( + dockerCompose: DockerCompose, +) : Client { + private val channel by lazy { + Thread.sleep(20_000) + ConnectionFactory().apply { + val url = dockerCompose.inputUrl + host = url.host + port = url.port + username = dockerCompose.user + password = dockerCompose.password + }.newConnection().createChannel() + } + private val coroChannelByVersion = mutableMapOf>() + + private fun getCoroChannel(version: String): Channel = coroChannelByVersion.computeIfAbsent(version) { + val coroChannel = Channel() + + val deliverCallback = DeliverCallback { consumerTag, delivery -> + val responseJson = String(delivery.body, Charsets.UTF_8) + println("Received in callback $version by $consumerTag:\n$responseJson") + runBlocking { + coroChannel.send(responseJson) + } + } + + channel.basicConsume("$version-queue-out", true, deliverCallback, CancelCallback { }) + + coroChannel + } + + override suspend fun sendAndReceive(version: String, path: String, request: String): String { + val coroChannel = getCoroChannel(version) + + // выкинем элемент из канала (мало ли) + coroChannel.tryReceive() + + println("Send $version:\n$request") + channel.basicPublish("", "$version-queue", null, request.toByteArray()) + + val response = coroChannel.receive() + println("Received:\n$response") + return response + } +} diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/fixture/client/RestClient.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/fixture/client/RestClient.kt new file mode 100644 index 0000000..7c38e27 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/fixture/client/RestClient.kt @@ -0,0 +1,33 @@ +package ru.otus.otuskotlin.marketplace.e2e.be.fixture.client + +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.engine.okhttp.* +import io.ktor.client.request.* +import io.ktor.http.* +import ru.otus.otuskotlin.marketplace.blackbox.fixture.docker.DockerCompose + +/** + * Отправка запросов по http/rest + */ +class RestClient(dockerCompose: DockerCompose) : Client { + private val urlBuilder by lazy { dockerCompose.inputUrl } + private val client = HttpClient(OkHttp) + override suspend fun sendAndReceive(version: String, path: String, request: String): String { + val url = urlBuilder.apply { + path("$version/$path") + }.build() + + val resp = client.post { + url(url) + headers { + append(HttpHeaders.ContentType, ContentType.Application.Json) + } + accept(ContentType.Application.Json) + setBody(request) + + }.call + + return resp.body() + } +} diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/fixture/client/WebSocketClient.kt_ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/fixture/client/WebSocketClient.kt_ new file mode 100644 index 0000000..7b1683d --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/fixture/client/WebSocketClient.kt_ @@ -0,0 +1,43 @@ +package ru.otus.otuskotlin.marketplace.blackbox.fixture.client + +import io.ktor.client.* +import io.ktor.client.engine.okhttp.* +import io.ktor.client.plugins.websocket.* +import io.ktor.http.* +import io.ktor.websocket.* +import kotlinx.coroutines.withTimeout +import ru.otus.otuskotlin.marketplace.blackbox.fixture.docker.DockerCompose + +/** + * Отправка запросов по http/websocket + */ +class WebSocketClient(dockerCompose: DockerCompose) : Client { + private val urlBuilder by lazy { dockerCompose.inputUrl } + private val client = HttpClient(OkHttp) { + install(WebSockets) + } + + override suspend fun sendAndReceive(version: String, path: String, request: String): String { + val url = urlBuilder.apply { + protocol = URLProtocol.WS + path("ws/$version") + }.build().toString() + + var response = "" + client.webSocket(url) { + withTimeout(3000) { + val incame = incoming.receive() as Frame.Text + val data = incame.readText() + // init - игнорим + } + send(Frame.Text(request)) + + withTimeout(3000) { + val incame = incoming.receive() as Frame.Text + response = incame.readText() + } + } + + return response + } +} \ No newline at end of file diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/fixture/docker/AbstractDockerCompose.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/fixture/docker/AbstractDockerCompose.kt new file mode 100644 index 0000000..8b2de1c --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/fixture/docker/AbstractDockerCompose.kt @@ -0,0 +1,76 @@ +package ru.otus.otuskotlin.marketplace.e2e.be.fixture.docker + +import co.touchlab.kermit.Logger +import io.ktor.http.* +import org.testcontainers.containers.DockerComposeContainer +import ru.otus.otuskotlin.marketplace.blackbox.fixture.docker.DockerCompose +import java.io.File + +private val log = Logger + +/** + * apps - список приложений в docker-compose. Первое приложение - "главное", его url возвращается как inputUrl + * (например ваш сервис при работе по rest или брокер сообщений при работе с брокером) + * dockerComposeName - имя docker-compose файла (относительно ok-marketplace-acceptance/docker-compose) + */ +abstract class AbstractDockerCompose( + private val apps: List, + private val dockerComposeName: String) +: DockerCompose { + + constructor(service: String, port: Int, dockerComposeName: String) + : this(listOf(AppInfo(service, port)), dockerComposeName) + private fun getComposeFile(): File { + val file = File("docker-compose/$dockerComposeName") + if (!file.exists()) throw IllegalArgumentException("file $dockerComposeName not found!") + return file + } + + private val compose = + DockerComposeContainer(getComposeFile()).apply { + withOptions("--compatibility") + apps.forEach { (service, port) -> + withExposedService( + service, + port, + ) + } + withLocalCompose(true) + } + + override fun start() { + kotlin.runCatching { compose.start() }.onFailure { + log.e { "Failed to start $dockerComposeName" } + throw it + } + + log.w("\n=========== $dockerComposeName started =========== \n") + apps.forEachIndexed { index, _ -> + log.i { "Started docker-compose with App at: ${getUrl(index)}" } + } + } + + override fun stop() { + compose.close() + log.w("\n=========== $dockerComposeName complete =========== \n") + } + + override fun clearDb() { + log.w("===== clearDb =====") + // TODO сделать очистку БД, когда до этого дойдет + } + + override val inputUrl: URLBuilder + get() = getUrl(0) + + fun getUrl(no: Int) = URLBuilder( + protocol = URLProtocol.HTTP, + host = apps[no].let { compose.getServiceHost(it.service, it.port) }, + port = apps[no].let { compose.getServicePort(it.service, it.port) }, + ) + data class AppInfo( + val service: String, + val port: Int, + ) + +} diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/fixture/docker/DockerCompose.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/fixture/docker/DockerCompose.kt new file mode 100644 index 0000000..594342f --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/fixture/docker/DockerCompose.kt @@ -0,0 +1,33 @@ +package ru.otus.otuskotlin.marketplace.blackbox.fixture.docker + +import io.ktor.http.URLBuilder + +/** + * Это обертка над сервисами в docker-compose. Позволяет их запускать и останавливать, + * получать url для отправки запросов и очищать БД (чтобы между тестами не делать пересоздание контейнеров) + */ +interface DockerCompose { + fun start() + fun stop() + + /** + * Очищает БД (возвращает ее к начальному состоянию) + */ + fun clearDb() + + /** + * URL для отправки запросов + */ + + val inputUrl: URLBuilder + + /** + * Пользователь для подключения (доступен не везде) + */ + val user: String get() = throw UnsupportedOperationException("no user") + /** + * Пароль для подключения (доступен не везде) + */ + val password: String get() = throw UnsupportedOperationException("no password") + +} \ No newline at end of file diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/package.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/package.kt new file mode 100644 index 0000000..2753a93 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/package.kt @@ -0,0 +1 @@ +package ru.otus.otuskotlin.marketplace.e2e.be diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/AccKafkaTest.kt_ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/AccKafkaTest.kt_ new file mode 100644 index 0000000..b5426bd --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/AccKafkaTest.kt_ @@ -0,0 +1,12 @@ +package ru.otus.otuskotlin.marketplace.e2e.be.test + +import ru.otus.otuskotlin.marketplace.e2e.be.fixture.BaseFunSpec +import ru.otus.otuskotlin.marketplace.e2e.be.docker.KafkaDockerCompose +import ru.otus.otuskotlin.marketplace.blackbox.fixture.client.KafkaClient + +class AccKafkaTest : BaseFunSpec(KafkaDockerCompose, { + val client = KafkaClient(KafkaDockerCompose) + + testApiV1(client) + testApiV2(client) +}) diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/AccRabbitTest.kt_ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/AccRabbitTest.kt_ new file mode 100644 index 0000000..2948a98 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/AccRabbitTest.kt_ @@ -0,0 +1,12 @@ +package ru.otus.otuskotlin.marketplace.e2e.be.test + +import ru.otus.otuskotlin.marketplace.e2e.be.docker.RabbitDockerCompose +import ru.otus.otuskotlin.marketplace.e2e.be.fixture.BaseFunSpec +import ru.otus.otuskotlin.marketplace.blackbox.fixture.client.RabbitClient + +class AccRabbitTest : BaseFunSpec(RabbitDockerCompose, { + val client = RabbitClient(RabbitDockerCompose) + + testApiV1(client) + testApiV2(client) +}) diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/AccRestTest.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/AccRestTest.kt new file mode 100644 index 0000000..848a9c6 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/AccRestTest.kt @@ -0,0 +1,20 @@ +package ru.otus.otuskotlin.marketplace.e2e.be.test + +import io.kotest.core.annotation.Ignored +import ru.otus.otuskotlin.marketplace.e2e.be.docker.WiremockDockerCompose +import ru.otus.otuskotlin.marketplace.e2e.be.fixture.BaseFunSpec +import ru.otus.otuskotlin.marketplace.blackbox.fixture.docker.DockerCompose +import ru.otus.otuskotlin.marketplace.e2e.be.fixture.client.RestClient + +// Kotest не сможет подставить правильный аргумент конструктора, поэтому +// нужно запретить ему запускать этот класс +@Ignored +open class AccRestTestBase(dockerCompose: DockerCompose) : BaseFunSpec(dockerCompose, { + val restClient = RestClient(dockerCompose) + testApiV1(restClient, "rest ") + testApiV2(restClient, "rest ") +}) + +class AccRestWiremockTest : AccRestTestBase(WiremockDockerCompose) +//class AccRestSpringTest : AccRestTestBase(SpringDockerCompose) +//class AccRestKtorTest : AccRestTestBase(KtorDockerCompose) diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/AccWebsocketTest.kt_ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/AccWebsocketTest.kt_ new file mode 100644 index 0000000..36903a2 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/AccWebsocketTest.kt_ @@ -0,0 +1,19 @@ +package ru.otus.otuskotlin.marketplace.e2e.be.test + +import fixture.client.RestClient +import io.kotest.core.annotation.Ignored +import ru.otus.otuskotlin.marketplace.e2e.be.docker.KtorDockerCompose +import ru.otus.otuskotlin.marketplace.e2e.be.docker.SpringDockerCompose +import ru.otus.otuskotlin.marketplace.e2e.be.fixture.BaseFunSpec +import ru.otus.otuskotlin.marketplace.blackbox.fixture.client.WebSocketClient +import ru.otus.otuskotlin.marketplace.blackbox.fixture.docker.DockerCompose + +@Ignored +open class AccRestTestBase(dockerCompose: DockerCompose) : BaseFunSpec(dockerCompose, { + val websocketClient = WebSocketClient(dockerCompose) + testApiV1(websocketClient, "websocket ") + testApiV2(websocketClient, "websocket ") +}) + +class AccRestSpringTest : AccRestTestBase(SpringDockerCompose, true) +class AccRestKtorTest : AccRestTestBase(KtorDockerCompose, false) diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/matchers.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/matchers.kt new file mode 100644 index 0000000..9b2d949 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/matchers.kt @@ -0,0 +1,21 @@ +package ru.otus.otuskotlin.marketplace.blackbox.test.action + +import io.kotest.matchers.Matcher +import io.kotest.matchers.MatcherResult + +val beValidId = Matcher { + MatcherResult( + it != null, + { "id should not be null" }, + { "id should be null" }, + ) +} + +val beValidLock = Matcher { + MatcherResult( + true, // TODO заменить на it != null, когда заработают локи + { "lock should not be null" }, + { "lock should be null" }, + ) +} + diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/createAdV1.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/createAdV1.kt new file mode 100644 index 0000000..fbcc2b6 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/createAdV1.kt @@ -0,0 +1,34 @@ +package ru.otus.otuskotlin.marketplace.e2e.be.test.action.v1 + +import io.kotest.assertions.asClue +import io.kotest.assertions.withClue +import io.kotest.matchers.should +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import ru.otus.otuskotlin.marketplace.api.v1.models.* +import ru.otus.otuskotlin.marketplace.e2e.be.fixture.client.Client + +suspend fun Client.createAd(ad: AdCreateObject = someCreateAd): AdResponseObject = createAd(ad) { + it should haveSuccessResult + it.ad shouldNotBe null + it.ad?.apply { + title shouldBe ad.title + description shouldBe ad.description + adType shouldBe ad.adType + visibility shouldBe ad.visibility + } + it.ad!! +} + +suspend fun Client.createAd(ad: AdCreateObject = someCreateAd, block: (AdCreateResponse) -> T): T = + withClue("createAdV1: $ad") { + val response = sendAndReceive( + "ad/create", AdCreateRequest( + requestType = "create", + debug = debug, + ad = ad + ) + ) as AdCreateResponse + + response.asClue(block) + } diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/dataV1.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/dataV1.kt new file mode 100644 index 0000000..4fd9d6b --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/dataV1.kt @@ -0,0 +1,12 @@ +package ru.otus.otuskotlin.marketplace.e2e.be.test.action.v1 + +import ru.otus.otuskotlin.marketplace.api.v1.models.* + +val debug = AdDebug(mode = AdRequestDebugMode.STUB, stub = AdRequestDebugStubs.SUCCESS) + +val someCreateAd = AdCreateObject( + title = "Требуется болт", + description = "Требуется болт 100x5 с шестигранной шляпкой", + adType = DealSide.DEMAND, + visibility = AdVisibility.PUBLIC +) diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/deleteAdV1.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/deleteAdV1.kt new file mode 100644 index 0000000..14cd2dd --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/deleteAdV1.kt @@ -0,0 +1,33 @@ +package ru.otus.otuskotlin.marketplace.e2e.be.test.action.v1 + +import io.kotest.assertions.asClue +import io.kotest.assertions.withClue +import io.kotest.matchers.should +import io.kotest.matchers.shouldBe +import ru.otus.otuskotlin.marketplace.api.v1.models.* +import ru.otus.otuskotlin.marketplace.blackbox.test.action.beValidId +import ru.otus.otuskotlin.marketplace.blackbox.test.action.beValidLock +import ru.otus.otuskotlin.marketplace.e2e.be.fixture.client.Client + +suspend fun Client.deleteAd(ad: AdResponseObject) { + val id = ad.id + val lock = ad.lock + withClue("deleteAdV2: $id, lock: $lock") { + id should beValidId + lock should beValidLock + + val response = sendAndReceive( + "ad/delete", + AdDeleteRequest( + debug = debug, + ad = AdDeleteObject(id = id, lock = lock) + ) + ) as AdDeleteResponse + + response.asClue { + response should haveSuccessResult + response.ad shouldBe ad +// response.ad?.id shouldBe id + } + } +} diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/matchers.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/matchers.kt new file mode 100644 index 0000000..ce61ca7 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/matchers.kt @@ -0,0 +1,44 @@ +package ru.otus.otuskotlin.marketplace.e2e.be.test.action.v1 + +import io.kotest.matchers.Matcher +import io.kotest.matchers.MatcherResult +import io.kotest.matchers.and +import ru.otus.otuskotlin.marketplace.api.v1.models.* + + +fun haveResult(result: ResponseResult) = Matcher { + MatcherResult( + it.result == result, + { "actual result ${it.result} but we expected $result" }, + { "result should not be $result" } + ) +} + +val haveNoErrors = Matcher { + MatcherResult( + it.errors.isNullOrEmpty(), + { "actual errors ${it.errors} but we expected no errors" }, + { "errors should not be empty" } + ) +} + +//fun haveError(code: String) = haveResult(ResponseResult.ERROR) +// .and(Matcher { +// MatcherResult( +// it.errors?.firstOrNull { e -> e.code == code } != null, +// { "actual errors ${it.errors} but we expected error with code $code" }, +// { "errors should not contain $code" } +// ) +// }) + +val haveSuccessResult = haveResult(ResponseResult.SUCCESS) and haveNoErrors + +val IResponse.ad: AdResponseObject? + get() = when (this) { + is AdCreateResponse -> ad + is AdReadResponse -> ad + is AdUpdateResponse -> ad + is AdDeleteResponse -> ad + is AdOffersResponse -> ad + else -> throw IllegalArgumentException("Invalid response type: ${this::class}") + } diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/offersAdV1.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/offersAdV1.kt new file mode 100644 index 0000000..f89a4c5 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/offersAdV1.kt @@ -0,0 +1,25 @@ +package ru.otus.otuskotlin.marketplace.e2e.be.test.action.v1 + +import io.kotest.assertions.asClue +import io.kotest.assertions.withClue +import io.kotest.matchers.should +import ru.otus.otuskotlin.marketplace.api.v1.models.* +import ru.otus.otuskotlin.marketplace.e2e.be.fixture.client.Client + +suspend fun Client.offersAd(id: String?): AdOffersResponse = offersAd(id) { + it should haveSuccessResult + it +} + +suspend fun Client.offersAd(id: String?, block: (AdOffersResponse) -> T): T = + withClue("searchOffersV1: $id") { + val response = sendAndReceive( + "ad/offers", + AdOffersRequest( + debug = debug, + ad = AdReadObject(id = id), + ) + ) as AdOffersResponse + + response.asClue(block) + } diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/readAdV1.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/readAdV1.kt new file mode 100644 index 0000000..81b0814 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/readAdV1.kt @@ -0,0 +1,34 @@ +package ru.otus.otuskotlin.marketplace.e2e.be.test.action.v1 + +import io.kotest.assertions.asClue +import io.kotest.assertions.withClue +import io.kotest.matchers.should +import io.kotest.matchers.shouldNotBe +import ru.otus.otuskotlin.marketplace.api.v1.models.AdReadObject +import ru.otus.otuskotlin.marketplace.api.v1.models.AdReadRequest +import ru.otus.otuskotlin.marketplace.api.v1.models.AdReadResponse +import ru.otus.otuskotlin.marketplace.api.v1.models.AdResponseObject +import ru.otus.otuskotlin.marketplace.blackbox.test.action.beValidId +import ru.otus.otuskotlin.marketplace.e2e.be.fixture.client.Client + +suspend fun Client.readAd(id: String?): AdResponseObject = readAd(id) { + it should haveSuccessResult + it.ad shouldNotBe null + it.ad!! +} + +suspend fun Client.readAd(id: String?, block: (AdReadResponse) -> T): T = + withClue("readAdV1: $id") { + id should beValidId + + val response = sendAndReceive( + "ad/read", + AdReadRequest( + requestType = "read", + debug = debug, + ad = AdReadObject(id = id) + ) + ) as AdReadResponse + + response.asClue(block) + } diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/searchAdV1.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/searchAdV1.kt new file mode 100644 index 0000000..3d50359 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/searchAdV1.kt @@ -0,0 +1,29 @@ +package ru.otus.otuskotlin.marketplace.e2e.be.test.action.v1 + +import io.kotest.assertions.asClue +import io.kotest.assertions.withClue +import io.kotest.matchers.should +import ru.otus.otuskotlin.marketplace.api.v1.models.AdResponseObject +import ru.otus.otuskotlin.marketplace.api.v1.models.AdSearchFilter +import ru.otus.otuskotlin.marketplace.api.v1.models.AdSearchRequest +import ru.otus.otuskotlin.marketplace.api.v1.models.AdSearchResponse +import ru.otus.otuskotlin.marketplace.e2e.be.fixture.client.Client + +suspend fun Client.searchAd(search: AdSearchFilter): List = searchAd(search) { + it should haveSuccessResult + it.ads ?: listOf() +} + +suspend fun Client.searchAd(search: AdSearchFilter, block: (AdSearchResponse) -> T): T = + withClue("searchAdV1: $search") { + val response = sendAndReceive( + "ad/search", + AdSearchRequest( + requestType = "search", + debug = debug, + adFilter = search, + ) + ) as AdSearchResponse + + response.asClue(block) + } diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/sendAndReceiveV1.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/sendAndReceiveV1.kt new file mode 100644 index 0000000..ae60222 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/sendAndReceiveV1.kt @@ -0,0 +1,20 @@ +package ru.otus.otuskotlin.marketplace.e2e.be.test.action.v1 + +import co.touchlab.kermit.Logger +import ru.otus.otuskotlin.marketplace.api.v1.apiV1RequestSerialize +import ru.otus.otuskotlin.marketplace.api.v1.apiV1ResponseDeserialize +import ru.otus.otuskotlin.marketplace.api.v1.models.IRequest +import ru.otus.otuskotlin.marketplace.api.v1.models.IResponse +import ru.otus.otuskotlin.marketplace.e2e.be.fixture.client.Client + +private val log = Logger + +suspend fun Client.sendAndReceive(path: String, request: IRequest): IResponse { + val requestBody = apiV1RequestSerialize(request) + log.i { "Send to v1/$path\n$requestBody" } + + val responseBody = sendAndReceive("v1", path, requestBody) + log.i { "Received\n$responseBody" } + + return apiV1ResponseDeserialize(responseBody) +} diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/updateAdV1.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/updateAdV1.kt new file mode 100644 index 0000000..3eabab4 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/updateAdV1.kt @@ -0,0 +1,46 @@ +package ru.otus.otuskotlin.marketplace.e2e.be.test.action.v1 + +import io.kotest.assertions.asClue +import io.kotest.assertions.withClue +import io.kotest.matchers.should +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import ru.otus.otuskotlin.marketplace.api.v1.models.* +import ru.otus.otuskotlin.marketplace.blackbox.test.action.beValidId +import ru.otus.otuskotlin.marketplace.blackbox.test.action.beValidLock +import ru.otus.otuskotlin.marketplace.e2e.be.fixture.client.Client + +suspend fun Client.updateAd(ad: AdUpdateObject): AdResponseObject = + updateAd(ad) { + it should haveSuccessResult + it.ad shouldNotBe null + it.ad?.apply { + if (ad.title != null) + title shouldBe ad.title + if (ad.description != null) + description shouldBe ad.description + if (ad.adType != null) + adType shouldBe ad.adType + if (ad.visibility != null) + visibility shouldBe ad.visibility + } + it.ad!! + } + +suspend fun Client.updateAd(ad: AdUpdateObject, block: (AdUpdateResponse) -> T): T { + val id = ad.id + val lock = ad.lock + return withClue("updatedV1: $id, lock: $lock, set: $ad") { + id should beValidId + lock should beValidLock + + val response = sendAndReceive( + "ad/update", AdUpdateRequest( + debug = debug, + ad = ad.copy(id = id, lock = lock) + ) + ) as AdUpdateResponse + + response.asClue(block) + } +} diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/createAdV2.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/createAdV2.kt new file mode 100644 index 0000000..eea62fc --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/createAdV2.kt @@ -0,0 +1,33 @@ +package ru.otus.otuskotlin.marketplace.e2e.be.test.action.v2 + +import io.kotest.assertions.asClue +import io.kotest.assertions.withClue +import io.kotest.matchers.should +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import ru.otus.otuskotlin.marketplace.api.v2.models.* +import ru.otus.otuskotlin.marketplace.e2e.be.fixture.client.Client + +suspend fun Client.createAd(ad: AdCreateObject = someCreateAd): AdResponseObject = createAd(ad) { + it should haveSuccessResult + it.ad shouldNotBe null + it.ad?.apply { + title shouldBe ad.title + description shouldBe ad.description + adType shouldBe ad.adType + visibility shouldBe ad.visibility + } + it.ad!! +} + +suspend fun Client.createAd(ad: AdCreateObject = someCreateAd, block: (AdCreateResponse) -> T): T = + withClue("createAdV2: $ad") { + val response = sendAndReceive( + "ad/create", AdCreateRequest( + debug = debug, + ad = ad + ) + ) as AdCreateResponse + + response.asClue(block) + } diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/dataV2.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/dataV2.kt new file mode 100644 index 0000000..88f3c75 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/dataV2.kt @@ -0,0 +1,13 @@ +package ru.otus.otuskotlin.marketplace.e2e.be.test.action.v2 + +import ru.otus.otuskotlin.marketplace.api.v2.models.* + + +val debug = AdDebug(mode = AdRequestDebugMode.STUB, stub = AdRequestDebugStubs.SUCCESS) + +val someCreateAd = AdCreateObject( + title = "Требуется болт", + description = "Требуется болт 100x5 с шестигранной шляпкой", + adType = DealSide.DEMAND, + visibility = AdVisibility.PUBLIC +) diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/deleteAdV2.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/deleteAdV2.kt new file mode 100644 index 0000000..c5e72fa --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/deleteAdV2.kt @@ -0,0 +1,36 @@ +package ru.otus.otuskotlin.marketplace.e2e.be.test.action.v2 + +import io.kotest.assertions.asClue +import io.kotest.assertions.withClue +import io.kotest.matchers.should +import io.kotest.matchers.shouldBe +import ru.otus.otuskotlin.marketplace.api.v2.models.AdDeleteObject +import ru.otus.otuskotlin.marketplace.api.v2.models.AdDeleteRequest +import ru.otus.otuskotlin.marketplace.api.v2.models.AdDeleteResponse +import ru.otus.otuskotlin.marketplace.api.v2.models.AdResponseObject +import ru.otus.otuskotlin.marketplace.blackbox.test.action.beValidId +import ru.otus.otuskotlin.marketplace.blackbox.test.action.beValidLock +import ru.otus.otuskotlin.marketplace.e2e.be.fixture.client.Client + +suspend fun Client.deleteAd(ad: AdResponseObject) { + val id = ad.id + val lock = ad.lock + withClue("deleteAdV2: $id, lock: $lock") { + id should beValidId + lock should beValidLock + + val response = sendAndReceive( + "ad/delete", + AdDeleteRequest( + debug = debug, + ad = AdDeleteObject(id = id, lock = lock) + ) + ) as AdDeleteResponse + + response.asClue { + response should haveSuccessResult + response.ad shouldBe ad +// response.ad?.id shouldBe id + } + } +} diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/matchers.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/matchers.kt new file mode 100644 index 0000000..6a58179 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/matchers.kt @@ -0,0 +1,44 @@ +package ru.otus.otuskotlin.marketplace.e2e.be.test.action.v2 + +import io.kotest.matchers.Matcher +import io.kotest.matchers.MatcherResult +import io.kotest.matchers.and +import ru.otus.otuskotlin.marketplace.api.v2.models.* + + +fun haveResult(result: ResponseResult) = Matcher { + MatcherResult( + it.result == result, + { "actual result ${it.result} but we expected $result" }, + { "result should not be $result" } + ) +} + +val haveNoErrors = Matcher { + MatcherResult( + it.errors.isNullOrEmpty(), + { "actual errors ${it.errors} but we expected no errors" }, + { "errors should not be empty" } + ) +} + +//fun haveError(code: String) = haveResult(ResponseResult.ERROR) +// .and(Matcher { +// MatcherResult( +// it.errors?.firstOrNull { e -> e.code == code } != null, +// { "actual errors ${it.errors} but we expected error with code $code" }, +// { "errors should not contain $code" } +// ) +// }) + +val haveSuccessResult = haveResult(ResponseResult.SUCCESS) and haveNoErrors + +val IResponse.ad: AdResponseObject? + get() = when (this) { + is AdCreateResponse -> ad + is AdReadResponse -> ad + is AdUpdateResponse -> ad + is AdDeleteResponse -> ad + is AdOffersResponse -> ad + else -> throw IllegalArgumentException("Invalid response type: ${this::class}") + } diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/offersAdV2.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/offersAdV2.kt new file mode 100644 index 0000000..8a42289 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/offersAdV2.kt @@ -0,0 +1,25 @@ +package ru.otus.otuskotlin.marketplace.e2e.be.test.action.v2 + +import io.kotest.assertions.asClue +import io.kotest.assertions.withClue +import io.kotest.matchers.should +import ru.otus.otuskotlin.marketplace.api.v2.models.* +import ru.otus.otuskotlin.marketplace.e2e.be.fixture.client.Client + +suspend fun Client.offersAd(id: String?): AdOffersResponse = offersAd(id) { + it should haveSuccessResult + it +} + +suspend fun Client.offersAd(id: String?, block: (AdOffersResponse) -> T): T = + withClue("searchOffersV2: $id") { + val response = sendAndReceive( + "ad/offers", + AdOffersRequest( + debug = debug, + ad = AdReadObject(id = id), + ) + ) as AdOffersResponse + + response.asClue(block) + } diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/readAdV2.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/readAdV2.kt new file mode 100644 index 0000000..b450862 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/readAdV2.kt @@ -0,0 +1,33 @@ +package ru.otus.otuskotlin.marketplace.e2e.be.test.action.v2 + +import io.kotest.assertions.asClue +import io.kotest.assertions.withClue +import io.kotest.matchers.should +import io.kotest.matchers.shouldNotBe +import ru.otus.otuskotlin.marketplace.api.v2.models.AdReadObject +import ru.otus.otuskotlin.marketplace.api.v2.models.AdReadRequest +import ru.otus.otuskotlin.marketplace.api.v2.models.AdReadResponse +import ru.otus.otuskotlin.marketplace.api.v2.models.AdResponseObject +import ru.otus.otuskotlin.marketplace.blackbox.test.action.beValidId +import ru.otus.otuskotlin.marketplace.e2e.be.fixture.client.Client + +suspend fun Client.readAd(id: String?): AdResponseObject = readAd(id) { + it should haveSuccessResult + it.ad shouldNotBe null + it.ad!! +} + +suspend fun Client.readAd(id: String?, block: (AdReadResponse) -> T): T = + withClue("readAdV1: $id") { + id should beValidId + + val response = sendAndReceive( + "ad/read", + AdReadRequest( + debug = debug, + ad = AdReadObject(id = id) + ) + ) as AdReadResponse + + response.asClue(block) + } diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/searchAdV2.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/searchAdV2.kt new file mode 100644 index 0000000..32a2283 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/searchAdV2.kt @@ -0,0 +1,28 @@ +package ru.otus.otuskotlin.marketplace.e2e.be.test.action.v2 + +import io.kotest.assertions.asClue +import io.kotest.assertions.withClue +import io.kotest.matchers.should +import ru.otus.otuskotlin.marketplace.api.v2.models.AdResponseObject +import ru.otus.otuskotlin.marketplace.api.v2.models.AdSearchFilter +import ru.otus.otuskotlin.marketplace.api.v2.models.AdSearchRequest +import ru.otus.otuskotlin.marketplace.api.v2.models.AdSearchResponse +import ru.otus.otuskotlin.marketplace.e2e.be.fixture.client.Client + +suspend fun Client.searchAd(search: AdSearchFilter): List = searchAd(search) { + it should haveSuccessResult + it.ads ?: listOf() +} + +suspend fun Client.searchAd(search: AdSearchFilter, block: (AdSearchResponse) -> T): T = + withClue("searchAdV2: $search") { + val response = sendAndReceive( + "ad/search", + AdSearchRequest( + debug = debug, + adFilter = search, + ) + ) as AdSearchResponse + + response.asClue(block) + } diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/sendAndReceiveV2.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/sendAndReceiveV2.kt new file mode 100644 index 0000000..9bfd262 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/sendAndReceiveV2.kt @@ -0,0 +1,20 @@ +package ru.otus.otuskotlin.marketplace.e2e.be.test.action.v2 + +import co.touchlab.kermit.Logger +import ru.otus.otuskotlin.marketplace.api.v2.apiV2RequestSerialize +import ru.otus.otuskotlin.marketplace.api.v2.apiV2ResponseDeserialize +import ru.otus.otuskotlin.marketplace.api.v2.models.IRequest +import ru.otus.otuskotlin.marketplace.api.v2.models.IResponse +import ru.otus.otuskotlin.marketplace.e2e.be.fixture.client.Client + +private val log = Logger + +suspend fun Client.sendAndReceive(path: String, request: IRequest): IResponse { + val requestBody = apiV2RequestSerialize(request) + log.w { "Send to v2/$path\n$requestBody" } + + val responseBody = sendAndReceive("v2", path, requestBody) + log.w { "Received\n$responseBody" } + + return apiV2ResponseDeserialize(responseBody) +} diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/updateAdV2.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/updateAdV2.kt new file mode 100644 index 0000000..f958aaa --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/updateAdV2.kt @@ -0,0 +1,46 @@ +package ru.otus.otuskotlin.marketplace.e2e.be.test.action.v2 + +import io.kotest.assertions.asClue +import io.kotest.assertions.withClue +import io.kotest.matchers.should +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import ru.otus.otuskotlin.marketplace.api.v2.models.* +import ru.otus.otuskotlin.marketplace.blackbox.test.action.beValidId +import ru.otus.otuskotlin.marketplace.blackbox.test.action.beValidLock +import ru.otus.otuskotlin.marketplace.e2e.be.fixture.client.Client + +suspend fun Client.updateAd(ad: AdUpdateObject): AdResponseObject = + updateAd(ad) { + it should haveSuccessResult + it.ad shouldNotBe null + it.ad?.apply { + if (ad.title != null) + title shouldBe ad.title + if (ad.description != null) + description shouldBe ad.description + if (ad.adType != null) + adType shouldBe ad.adType + if (ad.visibility != null) + visibility shouldBe ad.visibility + } + it.ad!! + } + +suspend fun Client.updateAd(ad: AdUpdateObject, block: (AdUpdateResponse) -> T): T { + val id = ad.id + val lock = ad.lock + return withClue("updatedV1: $id, lock: $lock, set: $ad") { + id should beValidId + lock should beValidLock + + val response = sendAndReceive( + "ad/update", AdUpdateRequest( + debug = debug, + ad = ad.copy(id = id, lock = lock) + ) + ) as AdUpdateResponse + + response.asClue(block) + } +} diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/testApiV1.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/testApiV1.kt new file mode 100644 index 0000000..acdc0eb --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/testApiV1.kt @@ -0,0 +1,80 @@ +package ru.otus.otuskotlin.marketplace.e2e.be.test + +import io.kotest.assertions.asClue +import io.kotest.assertions.fail +import io.kotest.assertions.withClue +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.collections.shouldExist +import io.kotest.matchers.collections.shouldExistInOrder +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.shouldBe +import ru.otus.otuskotlin.marketplace.api.v1.models.AdSearchFilter +import ru.otus.otuskotlin.marketplace.api.v1.models.AdUpdateObject +import ru.otus.otuskotlin.marketplace.api.v1.models.DealSide +import ru.otus.otuskotlin.marketplace.e2e.be.fixture.client.Client +import ru.otus.otuskotlin.marketplace.e2e.be.test.action.v1.* + +fun FunSpec.testApiV1(client: Client, prefix: String = "") { + context("${prefix}v1") { + test("Create Ad ok") { + client.createAd() + } + + test("Read Ad ok") { + val created = client.createAd() + client.readAd(created.id).asClue { + it shouldBe created + } + } + + test("Update Ad ok") { + val created = client.createAd() + val updateAd = AdUpdateObject( + id = created.id, + lock = created.lock, + title = "Selling Nut", + description = created.description, + adType = created.adType, + visibility = created.visibility, + ) + client.updateAd(updateAd) + } + + test("Delete Ad ok") { + val created = client.createAd() + client.deleteAd(created) +// client.readAd(created.id) { +// it should haveError("not-found") +// } + } + + test("Search Ad ok") { + val created1 = client.createAd(someCreateAd.copy(title = "Selling Bolt")) + val created2 = client.createAd(someCreateAd.copy(title = "Selling Nut")) + + withClue("Search Selling") { + val results = client.searchAd(search = AdSearchFilter(searchString = "Selling")) + results shouldHaveSize 2 + results shouldExist { it.title == created1.title } + results shouldExist { it.title == created2.title } + } + + withClue("Search Bolt") { + client.searchAd(search = AdSearchFilter(searchString = "Bolt")) + .shouldExistInOrder({ it.title == created1.title }) + } + } + + test("Offer Ad ok") { + val supply = client.createAd(someCreateAd.copy(title = "Some Bolt", adType = DealSide.SUPPLY)) + val demand = client.createAd(someCreateAd.copy(title = "Some Bolt", adType = DealSide.DEMAND)) + + withClue("Find offer for supply") { + val res1 = client.offersAd(supply.id) + res1.ad?.adType shouldBe supply.adType + res1.ads?.shouldExistInOrder({ it.adType == demand.adType }) ?: fail("Empty ads") + } + } + } + +} diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/testApiV2.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/testApiV2.kt new file mode 100644 index 0000000..6f548f3 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/testApiV2.kt @@ -0,0 +1,81 @@ +package ru.otus.otuskotlin.marketplace.e2e.be.test + +import io.kotest.assertions.asClue +import io.kotest.assertions.fail +import io.kotest.assertions.withClue +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.collections.shouldExist +import io.kotest.matchers.collections.shouldExistInOrder +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.shouldBe +import ru.otus.otuskotlin.marketplace.api.v2.models.AdSearchFilter +import ru.otus.otuskotlin.marketplace.api.v2.models.AdUpdateObject +import ru.otus.otuskotlin.marketplace.api.v2.models.DealSide +import ru.otus.otuskotlin.marketplace.e2e.be.fixture.client.Client +import ru.otus.otuskotlin.marketplace.e2e.be.test.action.v2.* + +fun FunSpec.testApiV2(client: Client, prefix: String = "") { + context("${prefix}v2") { + test("Create Ad ok") { + client.createAd() + } + + test("Read Ad ok") { + val created = client.createAd() + client.readAd(created.id).asClue { + it shouldBe created + } + } + + test("Update Ad ok") { + val created = client.createAd() + val updateAd = AdUpdateObject( + id = created.id, + lock = created.lock, + title = "Selling Nut", + description = created.description, + adType = created.adType, + visibility = created.visibility, + productId = created.productId, + ) + client.updateAd(updateAd) + } + + test("Delete Ad ok") { + val created = client.createAd() + client.deleteAd(created) +// client.readAd(created.id) { +// it should haveError("not-found") +// } + } + + test("Search Ad ok") { + val created1 = client.createAd(someCreateAd.copy(title = "Selling Bolt")) + val created2 = client.createAd(someCreateAd.copy(title = "Selling Nut")) + + withClue("Search Selling") { + val results = client.searchAd(search = AdSearchFilter(searchString = "Selling")) + results shouldHaveSize 2 + results shouldExist { it.title == created1.title } + results shouldExist { it.title == created2.title } + } + + withClue("Search Bolt") { + client.searchAd(search = AdSearchFilter(searchString = "Bolt")) + .shouldExistInOrder({ it.title == created1.title }) + } + } + + test("Offer Ad ok") { + val supply = client.createAd(someCreateAd.copy(title = "Some Bolt", adType = DealSide.SUPPLY)) + val demand = client.createAd(someCreateAd.copy(title = "Some Bolt", adType = DealSide.DEMAND)) + + withClue("Find offer for supply") { + val res1 = client.offersAd(supply.id) + res1.ad?.adType shouldBe supply.adType + res1.ads?.shouldExistInOrder({ it.adType == demand.adType }) ?: fail("Empty ads") + } + } + } + +} diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/resources/logback.xml b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/resources/logback.xml new file mode 100644 index 0000000..808ba2d --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/resources/logback.xml @@ -0,0 +1,14 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + \ No newline at end of file diff --git a/ok-marketplace-tests/settings.gradle.kts b/ok-marketplace-tests/settings.gradle.kts new file mode 100644 index 0000000..9e4e18c --- /dev/null +++ b/ok-marketplace-tests/settings.gradle.kts @@ -0,0 +1,29 @@ +rootProject.name = "ok-marketplace-tests" + +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } +} + +pluginManagement { + includeBuild("../build-plugin") + plugins { + id("build-jvm") apply false + id("build-kmp") apply false + } + repositories { + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0" +} + +//include(":ok-marketplace-api-v1-jackson") +//include(":ok-marketplace-api-v2-kmp") +include(":ok-marketplace-e2e-be") diff --git a/settings.gradle.kts b/settings.gradle.kts index 3565774..2109710 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -10,5 +10,7 @@ plugins { } rootProject.name = "ok-marketplace-202402" -//includeBuild("lessons") +includeBuild("lessons") includeBuild("ok-marketplace-be") + +includeBuild("ok-marketplace-tests")