Compare commits
2 Commits
main
...
homework_5
| Author | SHA1 | Date | |
|---|---|---|---|
| e145b1ad49 | |||
| 507e14bc2e |
115
README.md
115
README.md
@@ -1,65 +1,78 @@
|
||||
# OTUS Rest-assured Homework 3
|
||||
# OTUS Homework 5: Stub & API Testing (API-helpers)
|
||||
В этом проекте реализован stub-сервер на WireMock, API-helpers для HTTP/SOAP и автотесты контрактов/схем, плюс UI-проверка stub frontend через Cucumber.
|
||||
|
||||
## Цель проекта
|
||||
Автоматизировать API-сценарии для `https://petstore.swagger.io` с использованием Rest-assured, JUnit Jupiter и quality gates (Checkstyle + SpotBugs).
|
||||
## Что реализовано
|
||||
- Stub endpoints на WireMock:
|
||||
- `/user/get/{id}` - получение оценки пользователя
|
||||
- `/cource/get/all` - получение списка курсов
|
||||
- `/user/get/all` - получение списка пользователей
|
||||
- Контракты и схемы:
|
||||
- контрактные тесты для users/courses/score
|
||||
- JSON schema тесты для users/courses/score
|
||||
- API-helpers:
|
||||
- `HttpHelper`
|
||||
- `SoapHelper`
|
||||
- Дополнительно (optional):
|
||||
- `SqlHelper`
|
||||
- `MqHelper`
|
||||
- Stub frontend:
|
||||
- endpoint `/frontend`
|
||||
- Cucumber-сценарий проверки frontend
|
||||
- Selenoid support:
|
||||
- запуск UI-тестов через `-Dbrowser=selenoid`
|
||||
|
||||
## Стек технологий
|
||||
- Java 21
|
||||
- Maven
|
||||
- Rest-assured `6.0.0`
|
||||
- JUnit Jupiter `6.0.2`
|
||||
- Jackson Databind `2.21.0`
|
||||
- Commons Codec `1.21.0`
|
||||
- Maven Surefire Plugin `3.5.4`
|
||||
- Maven Compiler Plugin `3.15.0`
|
||||
- Checkstyle (`maven-checkstyle-plugin` `3.6.0`)
|
||||
- SpotBugs (`spotbugs-maven-plugin` `4.9.8.2`)
|
||||
## Что проверяется в Cucumber
|
||||
- Для homework_5 в раннер подключен только `features/stub_frontend.feature`.
|
||||
- `features/catalog.feature` и `CatalogSteps` сохранены в проекте, но в текущий раннер HW5 не входят.
|
||||
|
||||
## Реализованные сценарии
|
||||
1. `POST /pet` + `GET /pet/{id}`:
|
||||
- создание питомца;
|
||||
- повторное чтение по id;
|
||||
- проверка, что вернулся именно созданный объект.
|
||||
|
||||
2. `GET /pet/{id}`:
|
||||
- проверка схемы ответа через Json Schema Validator.
|
||||
|
||||
3. Негативные сценарии:
|
||||
- `POST /pet` с malformed JSON возвращает `4xx`;
|
||||
- `GET /pet/{id}` для удаленного/несуществующего id возвращает `404`;
|
||||
- `GET /pet/findByStatus` с невалидным статусом возвращает пустой список.
|
||||
|
||||
## Архитектура
|
||||
- 2-уровневый дизайн:
|
||||
- `service` слой (HTTP-вызовы + спецификации);
|
||||
- `tests` слой (сценарии).
|
||||
- Тесты создают сервис напрямую (`new PetService()`), без DI-фреймворка.
|
||||
|
||||
## Структура проекта
|
||||
- `src/main/java/ru/otus/petstore/config` — конфигурация
|
||||
- `src/main/java/ru/otus/petstore/model` — модели API (`Pet`, `Category`, `Tag`)
|
||||
- `src/main/java/ru/otus/petstore/service` — сервисы API
|
||||
- `src/test/java/ru/otus/petstore/tests` — автотесты (позитивные и негативные сценарии)
|
||||
- `src/test/java/ru/otus/petstore/util` — фабрика тестовых данных
|
||||
- `src/test/resources/schemas` — JSON schema
|
||||
|
||||
## Запуск
|
||||
### 1. Только тесты
|
||||
## Команды запуска
|
||||
Основной прогон (API + stub + helpers):
|
||||
```bash
|
||||
mvn test
|
||||
```
|
||||
|
||||
### 2. Полная проверка (тесты + Checkstyle + SpotBugs)
|
||||
Только ключевые тесты homework_5:
|
||||
```bash
|
||||
mvn "-Dtest=HttpHelperTest,SoapHelperTest,SqlHelperTest,MqHelperTest,StubContractTest,StubSchemaTest" test
|
||||
```
|
||||
|
||||
UI Cucumber (stub frontend):
|
||||
```bash
|
||||
mvn -Pui-tests test
|
||||
```
|
||||
|
||||
UI Cucumber через Selenoid:
|
||||
```bash
|
||||
mvn -Pui-tests "-Dbrowser=selenoid" "-Dselenoid.url=http://localhost:4444/wd/hub" test
|
||||
```
|
||||
|
||||
Полная проверка качества:
|
||||
```bash
|
||||
mvn verify
|
||||
```
|
||||
|
||||
## Параметры запуска
|
||||
- `base.uri` (по умолчанию `https://petstore.swagger.io`)
|
||||
- `base.path` (по умолчанию `/v2`)
|
||||
- значения читаются из `System properties` во время выполнения тестов.
|
||||
## Docker/Selenoid
|
||||
Если UI-тесты через Selenoid не нужны, этот раздел можно пропустить.
|
||||
|
||||
Пример:
|
||||
Предварительно:
|
||||
- Установить Docker Desktop.
|
||||
- Убедиться, что Docker запущен.
|
||||
|
||||
Поднять инфраструктуру:
|
||||
```bash
|
||||
mvn "-Dbase.uri=https://petstore.swagger.io" "-Dbase.path=/v2" test
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Проверка, что Selenoid поднялся:
|
||||
- Selenoid API: `http://localhost:4444/wd/hub`
|
||||
- Selenoid UI: `http://localhost:8081`
|
||||
|
||||
После этого можно запускать UI-тесты через Selenoid:
|
||||
```bash
|
||||
mvn -Pui-tests "-Dbrowser=selenoid" "-Dselenoid.url=http://localhost:4444/wd/hub" test
|
||||
```
|
||||
|
||||
Или выполнить полный пайплайн одной командой (поднимет Docker/Selenoid и запустит все проверки):
|
||||
```bash
|
||||
powershell -ExecutionPolicy Bypass -File .\scripts\run-full-pipeline.ps1
|
||||
```
|
||||
|
||||
32
docker-compose.yml
Normal file
32
docker-compose.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
name: hw3
|
||||
|
||||
services:
|
||||
selenoid:
|
||||
image: aerokube/selenoid:latest-release
|
||||
environment:
|
||||
DOCKER_API_VERSION: "1.44"
|
||||
ports:
|
||||
- "4444:4444"
|
||||
volumes:
|
||||
- "./selenoid/browsers.json:/etc/selenoid/browsers.json:ro"
|
||||
- "/var/run/docker.sock:/var/run/docker.sock"
|
||||
- "./selenoid/video:/opt/selenoid/video"
|
||||
command:
|
||||
- "-container-network"
|
||||
- "hw3_default"
|
||||
- "-limit"
|
||||
- "4"
|
||||
- "-timeout"
|
||||
- "5m"
|
||||
- "-video-output-dir"
|
||||
- "/opt/selenoid/video"
|
||||
|
||||
selenoid-ui:
|
||||
image: aerokube/selenoid-ui:latest-release
|
||||
depends_on:
|
||||
- selenoid
|
||||
command:
|
||||
- "--selenoid-uri"
|
||||
- "http://selenoid:4444"
|
||||
ports:
|
||||
- "8081:8080"
|
||||
105
pom.xml
105
pom.xml
@@ -16,10 +16,23 @@
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<base.uri>https://petstore.swagger.io</base.uri>
|
||||
<base.path>/v2</base.path>
|
||||
<base.url>http://localhost:8089</base.url>
|
||||
<course.name>Python Developer</course.name>
|
||||
<browser>chrome</browser>
|
||||
<selenoid.url>http://localhost:4444/wd/hub</selenoid.url>
|
||||
|
||||
<restassured.version>6.0.0</restassured.version>
|
||||
<junit.jupiter.version>6.0.2</junit.jupiter.version>
|
||||
<junit.jupiter.version>5.14.2</junit.jupiter.version>
|
||||
<junit.platform.suite.version>1.14.2</junit.platform.suite.version>
|
||||
<jackson.version>2.21.0</jackson.version>
|
||||
<selenium.version>4.40.0</selenium.version>
|
||||
<cucumber.version>7.34.2</cucumber.version>
|
||||
<guice.version>7.0.0</guice.version>
|
||||
<guava.version>33.5.0-jre</guava.version>
|
||||
<jsoup.version>1.22.1</jsoup.version>
|
||||
<slf4j.version>2.0.17</slf4j.version>
|
||||
<logback.version>1.5.31</logback.version>
|
||||
<wiremock.version>2.35.2</wiremock.version>
|
||||
|
||||
<maven.compiler.version>3.15.0</maven.compiler.version>
|
||||
<surefire.version>3.5.4</surefire.version>
|
||||
@@ -66,14 +79,69 @@
|
||||
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>${junit.jupiter.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<version>${junit.jupiter.version}</version>
|
||||
<groupId>org.junit.platform</groupId>
|
||||
<artifactId>junit-platform-suite</artifactId>
|
||||
<version>${junit.platform.suite.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.seleniumhq.selenium</groupId>
|
||||
<artifactId>selenium-java</artifactId>
|
||||
<version>${selenium.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.cucumber</groupId>
|
||||
<artifactId>cucumber-java</artifactId>
|
||||
<version>${cucumber.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.cucumber</groupId>
|
||||
<artifactId>cucumber-junit-platform-engine</artifactId>
|
||||
<version>${cucumber.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.inject</groupId>
|
||||
<artifactId>guice</artifactId>
|
||||
<version>${guice.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>${guava.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jsoup</groupId>
|
||||
<artifactId>jsoup</artifactId>
|
||||
<version>${jsoup.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>${slf4j.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<version>${logback.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.tomakehurst</groupId>
|
||||
<artifactId>wiremock-jre8</artifactId>
|
||||
<version>${wiremock.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<version>2.3.232</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -97,9 +165,15 @@
|
||||
<version>${surefire.version}</version>
|
||||
<configuration>
|
||||
<useModulePath>false</useModulePath>
|
||||
<excludes>
|
||||
<exclude>**/CucumberTest.java</exclude>
|
||||
</excludes>
|
||||
<systemPropertyVariables>
|
||||
<base.uri>${base.uri}</base.uri>
|
||||
<base.path>${base.path}</base.path>
|
||||
<course.name>${course.name}</course.name>
|
||||
<browser>${browser}</browser>
|
||||
<selenoid.url>${selenoid.url}</selenoid.url>
|
||||
</systemPropertyVariables>
|
||||
</configuration>
|
||||
</plugin>
|
||||
@@ -147,6 +221,27 @@
|
||||
<excludeFilterFile>spotbugs-exclude.xml</excludeFilterFile>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>ui-tests</id>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<excludes combine.self="override"/>
|
||||
<includes>
|
||||
<include>**/CucumberTest.java</include>
|
||||
</includes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
</project>
|
||||
|
||||
96
scripts/run-full-pipeline.ps1
Normal file
96
scripts/run-full-pipeline.ps1
Normal file
@@ -0,0 +1,96 @@
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Invoke-Step {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Name,
|
||||
[Parameter(Mandatory = $true)][string]$Command
|
||||
)
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== $Name ===" -ForegroundColor Cyan
|
||||
Write-Host $Command
|
||||
Invoke-Expression $Command
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Step '$Name' failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
}
|
||||
|
||||
function Start-DockerDesktopIfNeeded {
|
||||
function Test-DockerDaemonReady {
|
||||
cmd /c "docker info >nul 2>nul"
|
||||
return ($LASTEXITCODE -eq 0)
|
||||
}
|
||||
|
||||
$dockerInfoOk = Test-DockerDaemonReady
|
||||
|
||||
if ($dockerInfoOk) {
|
||||
return
|
||||
}
|
||||
|
||||
$candidatePaths = @(
|
||||
"$Env:ProgramFiles\Docker\Docker\Docker Desktop.exe",
|
||||
"$Env:ProgramFiles(x86)\Docker\Docker\Docker Desktop.exe"
|
||||
)
|
||||
|
||||
$dockerExe = $candidatePaths | Where-Object { Test-Path $_ } | Select-Object -First 1
|
||||
if (-not $dockerExe) {
|
||||
throw "Docker Desktop not found. Install Docker Desktop or start Docker manually."
|
||||
}
|
||||
|
||||
# Try to start Docker Desktop even if process exists; daemon may be down.
|
||||
Write-Host "Ensuring Docker Desktop is started..." -ForegroundColor Yellow
|
||||
Start-Process -FilePath $dockerExe | Out-Null
|
||||
|
||||
# If the process still does not appear, fail fast.
|
||||
$dockerProcess = Get-Process -Name "Docker Desktop" -ErrorAction SilentlyContinue
|
||||
if (-not $dockerProcess) {
|
||||
$dockerProcess = Get-Process -Name "Docker Desktop.exe" -ErrorAction SilentlyContinue
|
||||
}
|
||||
if (-not $dockerProcess) {
|
||||
Write-Host "Docker Desktop process is not detected yet; waiting for daemon..." -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
$deadline = (Get-Date).AddMinutes(3)
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
if (Test-DockerDaemonReady) {
|
||||
return
|
||||
}
|
||||
Start-Sleep -Seconds 3
|
||||
}
|
||||
|
||||
throw "Docker daemon is not available after 3 minutes."
|
||||
}
|
||||
|
||||
function Wait-Selenoid {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$StatusUrl
|
||||
)
|
||||
|
||||
$deadline = (Get-Date).AddMinutes(2)
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
try {
|
||||
$response = Invoke-RestMethod -Uri $StatusUrl -Method Get -TimeoutSec 5
|
||||
if ($null -ne $response.total -and $response.total -ge 1) {
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
Start-Sleep -Seconds 2
|
||||
continue
|
||||
}
|
||||
Start-Sleep -Seconds 2
|
||||
}
|
||||
|
||||
throw "Selenoid is not ready at $StatusUrl"
|
||||
}
|
||||
|
||||
Write-Host "Preparing Docker/Selenoid..." -ForegroundColor Green
|
||||
Start-DockerDesktopIfNeeded
|
||||
Invoke-Step -Name "Docker Compose Up" -Command "docker compose up -d --wait"
|
||||
Wait-Selenoid -StatusUrl "http://localhost:4444/status"
|
||||
|
||||
Invoke-Step -Name "Quality Gates" -Command "mvn -DskipTests verify"
|
||||
Invoke-Step -Name "API and Stub Tests" -Command "mvn test"
|
||||
Invoke-Step -Name "UI Tests via Selenoid" -Command "mvn -Pui-tests ""-Dbrowser=selenoid"" ""-Dselenoid.url=http://localhost:4444/wd/hub"" test"
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Full pipeline completed successfully." -ForegroundColor Green
|
||||
12
selenoid/browsers.json
Normal file
12
selenoid/browsers.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"chrome": {
|
||||
"default": "126.0",
|
||||
"versions": {
|
||||
"126.0": {
|
||||
"image": "selenoid/vnc_chrome:126.0",
|
||||
"port": "4444",
|
||||
"path": "/"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
selenoid/video/.gitkeep
Normal file
1
selenoid/video/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
16
src/main/java/ru/kovbasa/config/DriverModule.java
Normal file
16
src/main/java/ru/kovbasa/config/DriverModule.java
Normal file
@@ -0,0 +1,16 @@
|
||||
package ru.kovbasa.config;
|
||||
|
||||
import com.google.inject.AbstractModule;
|
||||
import com.google.inject.Singleton;
|
||||
import ru.kovbasa.driver.DriverFactory;
|
||||
import ru.kovbasa.driver.SelectableDriverFactory;
|
||||
import ru.kovbasa.driver.WebDriverProvider;
|
||||
|
||||
public class DriverModule extends AbstractModule {
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
bind(DriverFactory.class).to(SelectableDriverFactory.class).in(Singleton.class);
|
||||
bind(WebDriverProvider.class).in(Singleton.class);
|
||||
}
|
||||
}
|
||||
16
src/main/java/ru/kovbasa/config/InjectorProvider.java
Normal file
16
src/main/java/ru/kovbasa/config/InjectorProvider.java
Normal file
@@ -0,0 +1,16 @@
|
||||
package ru.kovbasa.config;
|
||||
|
||||
import com.google.inject.Guice;
|
||||
import com.google.inject.Injector;
|
||||
|
||||
public final class InjectorProvider {
|
||||
|
||||
private static final Injector INJECTOR = Guice.createInjector(new DriverModule());
|
||||
|
||||
private InjectorProvider() {
|
||||
}
|
||||
|
||||
public static Injector getInjector() {
|
||||
return INJECTOR;
|
||||
}
|
||||
}
|
||||
30
src/main/java/ru/kovbasa/config/TestConfig.java
Normal file
30
src/main/java/ru/kovbasa/config/TestConfig.java
Normal file
@@ -0,0 +1,30 @@
|
||||
package ru.kovbasa.config;
|
||||
|
||||
public final class TestConfig {
|
||||
|
||||
private TestConfig() {
|
||||
}
|
||||
|
||||
public static String getBaseUrl() {
|
||||
final String configuredBaseUrl = System.getProperty("base.url");
|
||||
if (configuredBaseUrl != null && !configuredBaseUrl.isBlank()) {
|
||||
return configuredBaseUrl;
|
||||
}
|
||||
if ("selenoid".equalsIgnoreCase(getBrowser())) {
|
||||
return "http://host.docker.internal:8089";
|
||||
}
|
||||
return "http://localhost:8089";
|
||||
}
|
||||
|
||||
public static String getCourseName() {
|
||||
return System.getProperty("course.name", "Python Developer");
|
||||
}
|
||||
|
||||
public static String getBrowser() {
|
||||
return System.getProperty("browser", "chrome");
|
||||
}
|
||||
|
||||
public static String getSelenoidUrl() {
|
||||
return System.getProperty("selenoid.url", "http://localhost:4444/wd/hub");
|
||||
}
|
||||
}
|
||||
16
src/main/java/ru/kovbasa/driver/ChromeDriverFactory.java
Normal file
16
src/main/java/ru/kovbasa/driver/ChromeDriverFactory.java
Normal file
@@ -0,0 +1,16 @@
|
||||
package ru.kovbasa.driver;
|
||||
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import org.openqa.selenium.chrome.ChromeDriver;
|
||||
import org.openqa.selenium.chrome.ChromeOptions;
|
||||
|
||||
public class ChromeDriverFactory implements DriverFactory {
|
||||
|
||||
@Override
|
||||
public WebDriver createDriver() {
|
||||
final ChromeOptions options = new ChromeOptions();
|
||||
options.addArguments("--start-maximized");
|
||||
options.addArguments("--disable-notifications");
|
||||
return new ChromeDriver(options);
|
||||
}
|
||||
}
|
||||
7
src/main/java/ru/kovbasa/driver/DriverFactory.java
Normal file
7
src/main/java/ru/kovbasa/driver/DriverFactory.java
Normal file
@@ -0,0 +1,7 @@
|
||||
package ru.kovbasa.driver;
|
||||
|
||||
import org.openqa.selenium.WebDriver;
|
||||
|
||||
public interface DriverFactory {
|
||||
WebDriver createDriver();
|
||||
}
|
||||
23
src/main/java/ru/kovbasa/driver/SelectableDriverFactory.java
Normal file
23
src/main/java/ru/kovbasa/driver/SelectableDriverFactory.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package ru.kovbasa.driver;
|
||||
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import ru.kovbasa.config.TestConfig;
|
||||
|
||||
public class SelectableDriverFactory implements DriverFactory {
|
||||
|
||||
private final DriverFactory chromeFactory = new ChromeDriverFactory();
|
||||
private final DriverFactory selenoidFactory = new SelenoidDriverFactory();
|
||||
|
||||
@Override
|
||||
public WebDriver createDriver() {
|
||||
final String browser = TestConfig.getBrowser().toLowerCase();
|
||||
if ("chrome".equals(browser)) {
|
||||
return chromeFactory.createDriver();
|
||||
}
|
||||
if ("selenoid".equals(browser)) {
|
||||
return selenoidFactory.createDriver();
|
||||
}
|
||||
throw new IllegalArgumentException(
|
||||
"Unsupported browser: " + browser + ". Supported browsers: chrome, selenoid");
|
||||
}
|
||||
}
|
||||
34
src/main/java/ru/kovbasa/driver/SelenoidDriverFactory.java
Normal file
34
src/main/java/ru/kovbasa/driver/SelenoidDriverFactory.java
Normal file
@@ -0,0 +1,34 @@
|
||||
package ru.kovbasa.driver;
|
||||
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import org.openqa.selenium.chrome.ChromeOptions;
|
||||
import org.openqa.selenium.remote.RemoteWebDriver;
|
||||
import ru.kovbasa.config.TestConfig;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
|
||||
public class SelenoidDriverFactory implements DriverFactory {
|
||||
|
||||
@Override
|
||||
public WebDriver createDriver() {
|
||||
final ChromeOptions options = new ChromeOptions();
|
||||
options.setCapability("browserName", "chrome");
|
||||
options.setCapability("browserVersion", "126.0");
|
||||
options.setCapability("selenoid:options", java.util.Map.of(
|
||||
"enableVNC", true,
|
||||
"enableVideo", false
|
||||
));
|
||||
try {
|
||||
return new RemoteWebDriver(URI.create(TestConfig.getSelenoidUrl()).toURL(), options);
|
||||
} catch (MalformedURLException exception) {
|
||||
throw new IllegalArgumentException("Invalid selenoid.url: " + TestConfig.getSelenoidUrl(), exception);
|
||||
} catch (RuntimeException exception) {
|
||||
throw new IllegalStateException(
|
||||
"Cannot create RemoteWebDriver session via selenoid.url=" + TestConfig.getSelenoidUrl()
|
||||
+ ". Ensure Docker is running and execute 'docker compose up -d' first.",
|
||||
exception
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
37
src/main/java/ru/kovbasa/driver/WebDriverProvider.java
Normal file
37
src/main/java/ru/kovbasa/driver/WebDriverProvider.java
Normal file
@@ -0,0 +1,37 @@
|
||||
package ru.kovbasa.driver;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import org.openqa.selenium.support.events.EventFiringDecorator;
|
||||
import ru.kovbasa.listeners.HighlightElementListener;
|
||||
|
||||
public final class WebDriverProvider {
|
||||
|
||||
private WebDriver driver;
|
||||
private final DriverFactory driverFactory;
|
||||
|
||||
@Inject
|
||||
public WebDriverProvider(DriverFactory driverFactory) {
|
||||
this.driverFactory = driverFactory;
|
||||
}
|
||||
|
||||
public WebDriver getDriver() {
|
||||
if (driver == null) {
|
||||
driver = createDecoratedDriver();
|
||||
}
|
||||
return driver;
|
||||
}
|
||||
|
||||
private WebDriver createDecoratedDriver() {
|
||||
final WebDriver raw = driverFactory.createDriver();
|
||||
return new EventFiringDecorator(new HighlightElementListener())
|
||||
.decorate(raw);
|
||||
}
|
||||
|
||||
public void quit() {
|
||||
if (driver != null) {
|
||||
driver.quit();
|
||||
driver = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
27
src/main/java/ru/kovbasa/elements/BaseElement.java
Normal file
27
src/main/java/ru/kovbasa/elements/BaseElement.java
Normal file
@@ -0,0 +1,27 @@
|
||||
package ru.kovbasa.elements;
|
||||
|
||||
import org.openqa.selenium.WebElement;
|
||||
|
||||
public abstract class BaseElement implements UIElement {
|
||||
|
||||
protected final WebElement element;
|
||||
|
||||
protected BaseElement(WebElement element) {
|
||||
this.element = element;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void click() {
|
||||
element.click();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText() {
|
||||
return element.getText();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDisplayed() {
|
||||
return element.isDisplayed();
|
||||
}
|
||||
}
|
||||
14
src/main/java/ru/kovbasa/elements/Button.java
Normal file
14
src/main/java/ru/kovbasa/elements/Button.java
Normal file
@@ -0,0 +1,14 @@
|
||||
package ru.kovbasa.elements;
|
||||
|
||||
import org.openqa.selenium.WebElement;
|
||||
|
||||
public class Button extends BaseElement {
|
||||
|
||||
public Button(WebElement element) {
|
||||
super(element);
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return element.isEnabled();
|
||||
}
|
||||
}
|
||||
77
src/main/java/ru/kovbasa/elements/CourseCard.java
Normal file
77
src/main/java/ru/kovbasa/elements/CourseCard.java
Normal file
@@ -0,0 +1,77 @@
|
||||
package ru.kovbasa.elements;
|
||||
|
||||
import org.openqa.selenium.By;
|
||||
import org.openqa.selenium.WebElement;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.Year;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class CourseCard extends BaseElement {
|
||||
|
||||
private final By titleLocator = By.cssSelector("h6 .sc-hrqzy3-1");
|
||||
private final By dateLocator = By.cssSelector(".sc-157icee-1 .sc-hrqzy3-1");
|
||||
private static final Pattern PRICE_PATTERN = Pattern.compile("(\\d[\\d\\s]*)\\s*[₽р]");
|
||||
|
||||
public CourseCard(WebElement element) {
|
||||
super(element);
|
||||
}
|
||||
|
||||
public String title() {
|
||||
return element.findElement(titleLocator).getText().trim();
|
||||
}
|
||||
|
||||
public LocalDate startDate() {
|
||||
String raw = element.findElement(dateLocator).getText().trim();
|
||||
if (raw.isEmpty()) {
|
||||
throw new RuntimeException("Пустая дата на карточке курса: " + title());
|
||||
}
|
||||
|
||||
String[] parts = raw.split("·");
|
||||
String datePart = parts[0].trim();
|
||||
|
||||
if (datePart.startsWith("С ")) {
|
||||
datePart = datePart.substring(2).trim();
|
||||
}
|
||||
|
||||
datePart = datePart.replace(",", "").replace("года", "").trim();
|
||||
|
||||
boolean hasYear = datePart.matches(".*\\d{4}.*");
|
||||
|
||||
String normalized;
|
||||
DateTimeFormatter formatter;
|
||||
|
||||
if (hasYear) {
|
||||
normalized = datePart;
|
||||
formatter = DateTimeFormatter.ofPattern("d MMMM yyyy", Locale.forLanguageTag("ru"));
|
||||
} else {
|
||||
normalized = datePart + " " + Year.now().getValue();
|
||||
formatter = DateTimeFormatter.ofPattern("d MMMM yyyy", Locale.forLanguageTag("ru"));
|
||||
}
|
||||
|
||||
return LocalDate.parse(normalized, formatter);
|
||||
}
|
||||
|
||||
public int price() {
|
||||
final String text = element.getText();
|
||||
final Matcher matcher = PRICE_PATTERN.matcher(text);
|
||||
|
||||
int maxPrice = -1;
|
||||
while (matcher.find()) {
|
||||
final String raw = matcher.group(1).replace(" ", "");
|
||||
final int parsed = Integer.parseInt(raw);
|
||||
if (parsed > maxPrice) {
|
||||
maxPrice = parsed;
|
||||
}
|
||||
}
|
||||
|
||||
if (maxPrice < 0) {
|
||||
throw new RuntimeException("Цена курса не найдена в карточке: " + title());
|
||||
}
|
||||
|
||||
return maxPrice;
|
||||
}
|
||||
}
|
||||
14
src/main/java/ru/kovbasa/elements/Link.java
Normal file
14
src/main/java/ru/kovbasa/elements/Link.java
Normal file
@@ -0,0 +1,14 @@
|
||||
package ru.kovbasa.elements;
|
||||
|
||||
import org.openqa.selenium.WebElement;
|
||||
|
||||
public class Link extends BaseElement {
|
||||
|
||||
public Link(WebElement element) {
|
||||
super(element);
|
||||
}
|
||||
|
||||
public String getHref() {
|
||||
return element.getAttribute("href");
|
||||
}
|
||||
}
|
||||
7
src/main/java/ru/kovbasa/elements/UIElement.java
Normal file
7
src/main/java/ru/kovbasa/elements/UIElement.java
Normal file
@@ -0,0 +1,7 @@
|
||||
package ru.kovbasa.elements;
|
||||
|
||||
public interface UIElement {
|
||||
void click();
|
||||
String getText();
|
||||
boolean isDisplayed();
|
||||
}
|
||||
158
src/main/java/ru/kovbasa/listeners/HighlightElementListener.java
Normal file
158
src/main/java/ru/kovbasa/listeners/HighlightElementListener.java
Normal file
@@ -0,0 +1,158 @@
|
||||
package ru.kovbasa.listeners;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.openqa.selenium.JavascriptExecutor;
|
||||
import org.openqa.selenium.StaleElementReferenceException;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import org.openqa.selenium.WrapsDriver;
|
||||
import org.openqa.selenium.support.events.WebDriverListener;
|
||||
|
||||
public class HighlightElementListener implements WebDriverListener {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HighlightElementListener.class);
|
||||
|
||||
private static final String HIGHLIGHT_STYLE =
|
||||
"outline: 4px solid red !important; " +
|
||||
"box-shadow: 0 0 0 4px rgba(255,0,0,0.20) !important; " +
|
||||
"background: rgba(255,0,0,0.06) !important;";
|
||||
private WebElement highlightedElement;
|
||||
private String highlightedOriginalStyle;
|
||||
|
||||
@Override
|
||||
public void beforeClick(WebElement element) {
|
||||
try {
|
||||
clearHighlight();
|
||||
final WebElement tile = findTileContainer(element);
|
||||
applyHighlight(tile);
|
||||
pauseForVisibility();
|
||||
} catch (RuntimeException e) {
|
||||
LOG.debug("beforeClick highlight skipped", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterClick(WebElement element) {
|
||||
clearHighlight();
|
||||
}
|
||||
|
||||
private void applyHighlight(WebElement element) {
|
||||
if (element == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final WebDriver raw = ((WrapsDriver) element).getWrappedDriver();
|
||||
if (!(raw instanceof JavascriptExecutor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final JavascriptExecutor js = (JavascriptExecutor) raw;
|
||||
|
||||
String original = safeGetAttr(element, "style");
|
||||
if (original == null) {
|
||||
original = "";
|
||||
}
|
||||
|
||||
this.highlightedElement = element;
|
||||
this.highlightedOriginalStyle = original;
|
||||
|
||||
js.executeScript("arguments[0].setAttribute('style', arguments[1] + '; ' + arguments[2]);",
|
||||
element, original, HIGHLIGHT_STYLE);
|
||||
} catch (StaleElementReferenceException ignored) {
|
||||
} catch (RuntimeException e) {
|
||||
LOG.debug("applyHighlight skipped", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void clearHighlight() {
|
||||
if (highlightedElement == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final WebDriver raw = ((WrapsDriver) highlightedElement).getWrappedDriver();
|
||||
if (!(raw instanceof JavascriptExecutor)) {
|
||||
highlightedElement = null;
|
||||
highlightedOriginalStyle = null;
|
||||
return;
|
||||
}
|
||||
|
||||
final JavascriptExecutor js = (JavascriptExecutor) raw;
|
||||
final String original = highlightedOriginalStyle == null ? "" : highlightedOriginalStyle;
|
||||
js.executeScript("arguments[0].setAttribute('style', arguments[1]);", highlightedElement, original);
|
||||
} catch (StaleElementReferenceException ignored) {
|
||||
} catch (RuntimeException e) {
|
||||
LOG.debug("clearHighlight skipped", e);
|
||||
} finally {
|
||||
highlightedElement = null;
|
||||
highlightedOriginalStyle = null;
|
||||
}
|
||||
}
|
||||
|
||||
private WebElement findTileContainer(WebElement element) {
|
||||
try {
|
||||
final String tag = safeGetTag(element);
|
||||
final String href = safeGetAttr(element, "href");
|
||||
final String cls = safeGetAttr(element, "class");
|
||||
|
||||
if ("a".equalsIgnoreCase(tag) && href != null &&
|
||||
(href.contains("/lessons/") || href.contains("/categories/"))) {
|
||||
return element;
|
||||
}
|
||||
if (cls != null && cls.contains("sc-zzdkm7-0")) {
|
||||
return element;
|
||||
}
|
||||
|
||||
final WebDriver rawDriver = ((WrapsDriver) element).getWrappedDriver();
|
||||
if (!(rawDriver instanceof JavascriptExecutor)) {
|
||||
return element;
|
||||
}
|
||||
|
||||
final JavascriptExecutor js = (JavascriptExecutor) rawDriver;
|
||||
|
||||
WebElement current = element;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
final Object parentObj = js.executeScript("return arguments[0].parentElement;", current);
|
||||
if (!(parentObj instanceof WebElement)) {
|
||||
break;
|
||||
}
|
||||
|
||||
current = (WebElement) parentObj;
|
||||
|
||||
final String t = safeGetTag(current);
|
||||
final String h = safeGetAttr(current, "href");
|
||||
final String c = safeGetAttr(current, "class");
|
||||
|
||||
if ("a".equalsIgnoreCase(t) && h != null &&
|
||||
(h.contains("/lessons/") || h.contains("/categories/"))) {
|
||||
return current;
|
||||
}
|
||||
if (c != null && c.contains("sc-zzdkm7-0")) {
|
||||
return current;
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
LOG.debug("findTileContainer fallback to original element", e);
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
private String safeGetAttr(WebElement el, String name) {
|
||||
try { return el.getAttribute(name); }
|
||||
catch (Exception e) { return null; }
|
||||
}
|
||||
|
||||
private String safeGetTag(WebElement el) {
|
||||
try { return el.getTagName(); }
|
||||
catch (Exception e) { return null; }
|
||||
}
|
||||
|
||||
private void pauseForVisibility() {
|
||||
try {
|
||||
Thread.sleep(120);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
355
src/main/java/ru/kovbasa/pages/CatalogPage.java
Normal file
355
src/main/java/ru/kovbasa/pages/CatalogPage.java
Normal file
@@ -0,0 +1,355 @@
|
||||
package ru.kovbasa.pages;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
import org.openqa.selenium.By;
|
||||
import org.openqa.selenium.JavascriptExecutor;
|
||||
import org.openqa.selenium.NoSuchElementException;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import org.openqa.selenium.interactions.Actions;
|
||||
import org.openqa.selenium.support.ui.ExpectedCondition;
|
||||
import org.openqa.selenium.support.ui.WebDriverWait;
|
||||
|
||||
import ru.kovbasa.config.TestConfig;
|
||||
import ru.kovbasa.driver.WebDriverProvider;
|
||||
import ru.kovbasa.elements.CourseCard;
|
||||
import ru.kovbasa.utils.PageUtils;
|
||||
|
||||
public class CatalogPage {
|
||||
|
||||
private final WebDriver driver;
|
||||
|
||||
private final By courseCards = By.cssSelector("a.sc-zzdkm7-0");
|
||||
private final By courseLinks = By.cssSelector("a[href*='/lessons/'], a[href*='/online/']");
|
||||
private final By learningMenu = By.cssSelector("span[title='Обучение']");
|
||||
private final By prepCoursesLink = By.xpath(
|
||||
"//a[contains(normalize-space(),'Подготовительные курсы')]");
|
||||
private final By showMoreButton = By.xpath("//button[contains(normalize-space(),'Показать еще')]");
|
||||
|
||||
@Inject
|
||||
public CatalogPage(WebDriverProvider provider) {
|
||||
this.driver = provider.getDriver();
|
||||
}
|
||||
|
||||
public CatalogPage open() {
|
||||
driver.get(TestConfig.getBaseUrl() + "/catalog/courses");
|
||||
return this;
|
||||
}
|
||||
|
||||
public WebElement findCourseByName(String name) {
|
||||
final WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(15));
|
||||
wait.until((ExpectedCondition<Boolean>) drv -> !drv.findElements(courseLinks).isEmpty());
|
||||
|
||||
final List<WebElement> links = driver.findElements(courseLinks);
|
||||
return links.stream()
|
||||
.filter(e -> e.getText().toLowerCase().contains(name.toLowerCase()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new NoSuchElementException(
|
||||
"Course not found in catalog by name: " + name));
|
||||
}
|
||||
|
||||
public List<WebElement> findCoursesByName(String name) {
|
||||
final WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(15));
|
||||
wait.until((ExpectedCondition<Boolean>) drv -> !drv.findElements(courseLinks).isEmpty());
|
||||
|
||||
return driver.findElements(courseLinks).stream()
|
||||
.filter(e -> e.getText().toLowerCase().contains(name.toLowerCase()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
public CoursePage clickCourseByName(String name) {
|
||||
PageUtils.removeBottomBanner(driver);
|
||||
|
||||
final WebElement course = findCourseByName(name);
|
||||
|
||||
((JavascriptExecutor) driver)
|
||||
.executeScript("arguments[0].scrollIntoView({block:'center'});", course);
|
||||
|
||||
new Actions(driver)
|
||||
.moveToElement(course)
|
||||
.perform();
|
||||
course.click();
|
||||
|
||||
return new CoursePage(driver);
|
||||
}
|
||||
|
||||
public CoursePage clickRandomCourseByName(String name) {
|
||||
PageUtils.removeBottomBanner(driver);
|
||||
|
||||
final List<WebElement> courses = findCoursesByName(name);
|
||||
if (courses.isEmpty()) {
|
||||
throw new NoSuchElementException("Course not found in catalog by name: " + name);
|
||||
}
|
||||
|
||||
final WebElement chosen = courses.get(ThreadLocalRandom.current().nextInt(courses.size()));
|
||||
|
||||
((JavascriptExecutor) driver)
|
||||
.executeScript("arguments[0].scrollIntoView({block:'center'});", chosen);
|
||||
|
||||
new Actions(driver)
|
||||
.moveToElement(chosen)
|
||||
.perform();
|
||||
chosen.click();
|
||||
|
||||
return new CoursePage(driver);
|
||||
}
|
||||
|
||||
public List<CourseCard> getAllCourseCards() {
|
||||
return driver.findElements(courseCards).stream()
|
||||
.map(CourseCard::new)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<CourseItem> getAllCourses() {
|
||||
return getAllCourseCards().stream()
|
||||
.map(card -> {
|
||||
try {
|
||||
return new CourseItem(card.title(), card.startDate());
|
||||
} catch (RuntimeException e) {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<CourseItem> findEarliestCourses() {
|
||||
final List<CourseItem> all = getAllCourses();
|
||||
|
||||
final LocalDate minDate = all.stream()
|
||||
.map(CourseItem::startDate)
|
||||
.reduce((left, right) -> left.isBefore(right) ? left : right)
|
||||
.orElseThrow();
|
||||
|
||||
return all.stream()
|
||||
.filter(c -> c.startDate().isEqual(minDate))
|
||||
.sorted(Comparator.comparing(CourseItem::title))
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<CourseItem> findLatestCourses() {
|
||||
final List<CourseItem> all = getAllCourses();
|
||||
|
||||
final LocalDate maxDate = all.stream()
|
||||
.map(CourseItem::startDate)
|
||||
.reduce((left, right) -> left.isAfter(right) ? left : right)
|
||||
.orElseThrow();
|
||||
|
||||
return all.stream()
|
||||
.filter(c -> c.startDate().isEqual(maxDate))
|
||||
.sorted(Comparator.comparing(CourseItem::title))
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<CourseItem> findCoursesStartingFrom(LocalDate dateFrom) {
|
||||
return getAllCourses().stream()
|
||||
.filter(course -> !course.startDate().isBefore(dateFrom))
|
||||
.sorted(Comparator.comparing(CourseItem::startDate).thenComparing(CourseItem::title))
|
||||
.toList();
|
||||
}
|
||||
|
||||
public CatalogPage openPreparatoryCourses() {
|
||||
driver.get(TestConfig.getBaseUrl());
|
||||
PageUtils.removeBottomBanner(driver);
|
||||
|
||||
final WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(15));
|
||||
final WebElement menu = wait.until(drv -> drv.findElement(learningMenu));
|
||||
menu.click();
|
||||
|
||||
final WebElement prep = wait.until(drv -> drv.findElements(prepCoursesLink).stream()
|
||||
.filter(WebElement::isDisplayed)
|
||||
.findFirst()
|
||||
.orElse(null));
|
||||
|
||||
((JavascriptExecutor) driver)
|
||||
.executeScript("arguments[0].scrollIntoView({block:'center'});", prep);
|
||||
prep.click();
|
||||
|
||||
wait.until(drv -> drv.findElements(
|
||||
By.xpath("//*[contains(normalize-space(),'Подготовительные курсы')]")
|
||||
).stream().anyMatch(WebElement::isDisplayed));
|
||||
|
||||
expandAllCourses();
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<PricedCourseItem> getAllCoursesWithPrice() {
|
||||
return getAllCourseCards().stream()
|
||||
.map(card -> {
|
||||
try {
|
||||
return new PricedCourseItem(card.title(), card.price());
|
||||
} catch (RuntimeException e) {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<CourseLinkItem> getPreparatoryCourseLinks() {
|
||||
final WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(15));
|
||||
wait.until((ExpectedCondition<Boolean>) drv -> !drv.findElements(courseLinks).isEmpty());
|
||||
|
||||
final Map<String, String> titleByUrl = new LinkedHashMap<>();
|
||||
for (WebElement link : driver.findElements(courseLinks)) {
|
||||
if (!link.isDisplayed()) {
|
||||
continue;
|
||||
}
|
||||
final String title = link.getText().trim();
|
||||
final String url = link.getAttribute("href");
|
||||
if (title.isEmpty() || url == null || !isCourseUrl(url)) {
|
||||
continue;
|
||||
}
|
||||
titleByUrl.putIfAbsent(url, title);
|
||||
}
|
||||
|
||||
final List<Map.Entry<String, String>> onlineEntries = titleByUrl.entrySet().stream()
|
||||
.filter(entry -> entry.getKey().contains("/online/"))
|
||||
.toList();
|
||||
|
||||
final List<CourseLinkItem> items = new ArrayList<>();
|
||||
if (!onlineEntries.isEmpty()) {
|
||||
onlineEntries.forEach(entry -> items.add(new CourseLinkItem(cleanTitle(entry.getValue()), entry.getKey())));
|
||||
return items;
|
||||
}
|
||||
|
||||
titleByUrl.forEach((url, title) -> items.add(new CourseLinkItem(cleanTitle(title), url)));
|
||||
return items;
|
||||
}
|
||||
|
||||
public List<PricedCourseItem> getPreparatoryCoursesWithDiscountedFullPrice() {
|
||||
final List<CourseLinkItem> links = getPreparatoryCourseLinks();
|
||||
return links.stream()
|
||||
.map(link -> {
|
||||
driver.get(link.url());
|
||||
final CoursePage coursePage = new CoursePage(driver);
|
||||
final int price = coursePage.getPriceForComparison();
|
||||
String title = link.title();
|
||||
try {
|
||||
title = coursePage.getCourseTitle();
|
||||
} catch (RuntimeException ignored) { }
|
||||
return new PricedCourseItem(title, price);
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<CourseLinkItem> getCatalogLessonCourseLinks() {
|
||||
open();
|
||||
|
||||
final WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(15));
|
||||
wait.until((ExpectedCondition<Boolean>) drv -> !drv.findElements(courseLinks).isEmpty());
|
||||
|
||||
final Map<String, String> titleByUrl = new LinkedHashMap<>();
|
||||
for (WebElement link : driver.findElements(courseLinks)) {
|
||||
if (!link.isDisplayed()) {
|
||||
continue;
|
||||
}
|
||||
final String title = link.getText().trim();
|
||||
final String url = link.getAttribute("href");
|
||||
if (title.isEmpty() || url == null || !url.contains("/lessons/")) {
|
||||
continue;
|
||||
}
|
||||
titleByUrl.putIfAbsent(url, cleanTitle(title));
|
||||
}
|
||||
|
||||
final List<CourseLinkItem> items = new ArrayList<>();
|
||||
titleByUrl.forEach((url, title) -> items.add(new CourseLinkItem(title, url)));
|
||||
return items;
|
||||
}
|
||||
|
||||
public List<PricedCourseItem> getCatalogCoursesWithDiscountedFullPrice() {
|
||||
final List<CourseLinkItem> links = getCatalogLessonCourseLinks();
|
||||
return links.stream()
|
||||
.map(link -> {
|
||||
driver.get(link.url());
|
||||
final CoursePage coursePage = new CoursePage(driver);
|
||||
final int price = coursePage.getPriceForComparison();
|
||||
String title = link.title();
|
||||
try {
|
||||
title = coursePage.getCourseTitle();
|
||||
} catch (RuntimeException ignored) { }
|
||||
return new PricedCourseItem(title, price);
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
private void expandAllCourses() {
|
||||
final WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(5));
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
final WebElement button = wait.until(drv -> drv.findElements(showMoreButton).stream()
|
||||
.filter(WebElement::isDisplayed)
|
||||
.findFirst()
|
||||
.orElse(null));
|
||||
|
||||
((JavascriptExecutor) driver)
|
||||
.executeScript("arguments[0].scrollIntoView({block:'center'});", button);
|
||||
button.click();
|
||||
wait.until(drv -> !drv.findElements(courseLinks).isEmpty());
|
||||
} catch (RuntimeException ignored) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isCourseUrl(String url) {
|
||||
return url.contains("/lessons/") || url.contains("/online/");
|
||||
}
|
||||
|
||||
private String cleanTitle(String rawTitle) {
|
||||
final String[] lines = rawTitle.split("\\R");
|
||||
for (int i = lines.length - 1; i >= 0; i--) {
|
||||
final String line = lines[i].trim();
|
||||
if (!line.isEmpty()) {
|
||||
return line;
|
||||
}
|
||||
}
|
||||
return rawTitle.trim();
|
||||
}
|
||||
|
||||
public PricedCourseItem findMostExpensiveCourse() {
|
||||
return getAllCoursesWithPrice().stream()
|
||||
.reduce((left, right) -> left.price() >= right.price() ? left : right)
|
||||
.orElseThrow(() -> new NoSuchElementException("Priced course cards are not found"));
|
||||
}
|
||||
|
||||
public PricedCourseItem findCheapestCourse() {
|
||||
return getAllCoursesWithPrice().stream()
|
||||
.reduce((left, right) -> left.price() <= right.price() ? left : right)
|
||||
.orElseThrow(() -> new NoSuchElementException("Priced course cards are not found"));
|
||||
}
|
||||
|
||||
public CoursePage openCourse(CourseItem course) {
|
||||
PageUtils.removeBottomBanner(driver);
|
||||
|
||||
final WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
|
||||
|
||||
final WebElement card = wait.until(drv ->
|
||||
drv.findElements(courseCards).stream()
|
||||
.filter(c -> new CourseCard(c).title().equals(course.title()))
|
||||
.findFirst()
|
||||
.orElse(null)
|
||||
);
|
||||
|
||||
((JavascriptExecutor) driver)
|
||||
.executeScript("arguments[0].scrollIntoView({block:'center'});", card);
|
||||
|
||||
new Actions(driver)
|
||||
.moveToElement(card)
|
||||
.perform();
|
||||
card.click();
|
||||
|
||||
return new CoursePage(driver);
|
||||
}
|
||||
}
|
||||
24
src/main/java/ru/kovbasa/pages/CategoryPage.java
Normal file
24
src/main/java/ru/kovbasa/pages/CategoryPage.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package ru.kovbasa.pages;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
|
||||
public class CategoryPage {
|
||||
|
||||
private final WebDriver driver;
|
||||
|
||||
public CategoryPage(WebDriver driver) {
|
||||
this.driver = driver;
|
||||
}
|
||||
|
||||
public String getHeader() {
|
||||
Document doc = Jsoup.parse(driver.getPageSource());
|
||||
return doc.select("h1")
|
||||
.stream()
|
||||
.map(e -> e.text().trim())
|
||||
.filter(t -> !t.isEmpty())
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new RuntimeException("Category header not found"));
|
||||
}
|
||||
}
|
||||
6
src/main/java/ru/kovbasa/pages/CourseItem.java
Normal file
6
src/main/java/ru/kovbasa/pages/CourseItem.java
Normal file
@@ -0,0 +1,6 @@
|
||||
package ru.kovbasa.pages;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
public record CourseItem(String title, LocalDate startDate) {
|
||||
}
|
||||
4
src/main/java/ru/kovbasa/pages/CourseLinkItem.java
Normal file
4
src/main/java/ru/kovbasa/pages/CourseLinkItem.java
Normal file
@@ -0,0 +1,4 @@
|
||||
package ru.kovbasa.pages;
|
||||
|
||||
public record CourseLinkItem(String title, String url) {
|
||||
}
|
||||
139
src/main/java/ru/kovbasa/pages/CoursePage.java
Normal file
139
src/main/java/ru/kovbasa/pages/CoursePage.java
Normal file
@@ -0,0 +1,139 @@
|
||||
package ru.kovbasa.pages;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.openqa.selenium.By;
|
||||
import org.openqa.selenium.JavascriptExecutor;
|
||||
import org.openqa.selenium.NoSuchElementException;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import org.openqa.selenium.interactions.Actions;
|
||||
import org.openqa.selenium.support.ui.WebDriverWait;
|
||||
import ru.kovbasa.elements.Button;
|
||||
import ru.kovbasa.utils.PageUtils;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class CoursePage {
|
||||
private static final Pattern PRICE_PATTERN = Pattern.compile("(\\d[\\d\\s\\u00A0]*)\\s*₽");
|
||||
|
||||
|
||||
private final WebDriver driver;
|
||||
|
||||
private final By enrollButton = By.cssSelector("button[data-testid='enroll-button']");
|
||||
private final By fullPriceTab = By.xpath("//div[normalize-space()='Полная']/parent::div");
|
||||
private final By fullDiscountLabel = By.xpath("//p[contains(normalize-space(),'Полная стоимость со скидкой')]");
|
||||
private final By anyPriceText = By.xpath("//*[contains(normalize-space(),'₽')]");
|
||||
|
||||
public CoursePage(WebDriver driver) {
|
||||
this.driver = driver;
|
||||
}
|
||||
|
||||
public String getCourseTitle() {
|
||||
return getFirstH1();
|
||||
}
|
||||
|
||||
public String getFirstH1() {
|
||||
Document doc = Jsoup.parse(driver.getPageSource());
|
||||
return doc.select("h1")
|
||||
.stream()
|
||||
.map(e -> e.text().trim())
|
||||
.filter(t -> !t.isEmpty())
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new RuntimeException("Course title not found"));
|
||||
}
|
||||
|
||||
public LocalDate getCourseStartDate(LocalDate catalogDate) {
|
||||
Document doc = Jsoup.parse(driver.getPageSource());
|
||||
|
||||
Element dateElement = doc.selectFirst("p.sc-3cb1l3-0");
|
||||
if (dateElement == null) {
|
||||
throw new RuntimeException("Start date <p> not found on course page");
|
||||
}
|
||||
|
||||
String text = dateElement.text().trim();
|
||||
String fullDate = text + ", " + catalogDate.getYear();
|
||||
|
||||
DateTimeFormatter formatter =
|
||||
DateTimeFormatter.ofPattern("d MMMM, yyyy", Locale.forLanguageTag("ru"));
|
||||
|
||||
return LocalDate.parse(fullDate, formatter);
|
||||
}
|
||||
|
||||
public Button getEnrollButton() {
|
||||
return new Button(driver.findElement(enrollButton));
|
||||
}
|
||||
|
||||
public void clickEnroll() {
|
||||
getEnrollButton().click();
|
||||
}
|
||||
|
||||
public int getDiscountedFullPrice() {
|
||||
PageUtils.removeBottomBanner(driver);
|
||||
|
||||
final WebDriverWait wait = new WebDriverWait(driver, java.time.Duration.ofSeconds(10));
|
||||
final WebElement tab = wait.until(drv -> drv.findElements(fullPriceTab).stream()
|
||||
.filter(WebElement::isDisplayed)
|
||||
.findFirst()
|
||||
.orElse(null));
|
||||
|
||||
((JavascriptExecutor) driver).executeScript("arguments[0].scrollIntoView({block:'center'});", tab);
|
||||
try {
|
||||
tab.click();
|
||||
} catch (RuntimeException ignored) {
|
||||
new Actions(driver).moveToElement(tab).click().perform();
|
||||
}
|
||||
|
||||
final WebElement label = wait.until(drv -> drv.findElements(fullDiscountLabel).stream()
|
||||
.filter(WebElement::isDisplayed)
|
||||
.findFirst()
|
||||
.orElse(null));
|
||||
|
||||
final WebElement priceElement = findPriceElementNearLabel(label);
|
||||
final String rawPrice = priceElement.getText();
|
||||
return parsePrice(rawPrice);
|
||||
}
|
||||
|
||||
public int getPriceForComparison() {
|
||||
if (hasDisplayed(fullPriceTab)) {
|
||||
try {
|
||||
return getDiscountedFullPrice();
|
||||
} catch (RuntimeException ignored) { }
|
||||
}
|
||||
|
||||
final WebDriverWait wait = new WebDriverWait(driver, java.time.Duration.ofSeconds(10));
|
||||
final WebElement priceElement = wait.until(drv -> drv.findElements(anyPriceText).stream()
|
||||
.filter(WebElement::isDisplayed)
|
||||
.filter(el -> PRICE_PATTERN.matcher(el.getText()).find())
|
||||
.findFirst()
|
||||
.orElse(null));
|
||||
|
||||
return parsePrice(priceElement.getText());
|
||||
}
|
||||
|
||||
private WebElement findPriceElementNearLabel(WebElement label) {
|
||||
try {
|
||||
return label.findElement(By.xpath("following-sibling::div[1]"));
|
||||
} catch (NoSuchElementException ignored) {
|
||||
return label.findElement(By.xpath("following::div[contains(normalize-space(),'₽')][1]"));
|
||||
}
|
||||
}
|
||||
|
||||
private int parsePrice(String rawPrice) {
|
||||
final Matcher matcher = PRICE_PATTERN.matcher(rawPrice);
|
||||
if (!matcher.find()) {
|
||||
throw new IllegalArgumentException("Не удалось распарсить цену из строки: " + rawPrice);
|
||||
}
|
||||
final String normalized = matcher.group(1).replace('\u00A0', ' ').replace(" ", "");
|
||||
return Integer.parseInt(normalized);
|
||||
}
|
||||
|
||||
private boolean hasDisplayed(By by) {
|
||||
return driver.findElements(by).stream().anyMatch(WebElement::isDisplayed);
|
||||
}
|
||||
}
|
||||
70
src/main/java/ru/kovbasa/pages/MainPage.java
Normal file
70
src/main/java/ru/kovbasa/pages/MainPage.java
Normal file
@@ -0,0 +1,70 @@
|
||||
package ru.kovbasa.pages;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import org.openqa.selenium.By;
|
||||
import org.openqa.selenium.ElementClickInterceptedException;
|
||||
import org.openqa.selenium.JavascriptExecutor;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import org.openqa.selenium.interactions.Actions;
|
||||
import org.openqa.selenium.support.ui.WebDriverWait;
|
||||
import ru.kovbasa.config.TestConfig;
|
||||
import ru.kovbasa.driver.WebDriverProvider;
|
||||
import ru.kovbasa.utils.PageUtils;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
public class MainPage {
|
||||
|
||||
private final WebDriver driver;
|
||||
|
||||
private final By menuLearning = By.cssSelector("span[title='Обучение']");
|
||||
private final By categories = By.cssSelector("a[href*='/categories/']");
|
||||
|
||||
@Inject
|
||||
public MainPage(WebDriverProvider provider) {
|
||||
this.driver = provider.getDriver();
|
||||
}
|
||||
|
||||
public MainPage open() {
|
||||
driver.get(TestConfig.getBaseUrl());
|
||||
return this;
|
||||
}
|
||||
|
||||
public String clickRandomCategory() {
|
||||
PageUtils.removeBottomBanner(driver);
|
||||
|
||||
final WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
|
||||
|
||||
final WebElement menu = wait.until(drv -> drv.findElement(menuLearning));
|
||||
menu.click();
|
||||
|
||||
final List<WebElement> els = wait.until(drv -> {
|
||||
final List<WebElement> found = drv.findElements(categories).stream()
|
||||
.filter(WebElement::isDisplayed)
|
||||
.toList();
|
||||
return found.isEmpty() ? null : found;
|
||||
});
|
||||
|
||||
final WebElement chosen = els.get(ThreadLocalRandom.current().nextInt(els.size()));
|
||||
final String href = chosen.getAttribute("href");
|
||||
|
||||
((JavascriptExecutor) driver)
|
||||
.executeScript("arguments[0].scrollIntoView({block:'center'});", chosen);
|
||||
|
||||
try {
|
||||
chosen.click();
|
||||
} catch (ElementClickInterceptedException ignored) {
|
||||
new Actions(driver).moveToElement(chosen).click().perform();
|
||||
}
|
||||
|
||||
final String categorySlug = href.substring(href.lastIndexOf('/') + 1);
|
||||
if (!driver.getCurrentUrl().contains(categorySlug)) {
|
||||
driver.get(TestConfig.getBaseUrl() + "/catalog/courses?categories=" + categorySlug);
|
||||
}
|
||||
|
||||
return href;
|
||||
}
|
||||
}
|
||||
4
src/main/java/ru/kovbasa/pages/PricedCourseItem.java
Normal file
4
src/main/java/ru/kovbasa/pages/PricedCourseItem.java
Normal file
@@ -0,0 +1,4 @@
|
||||
package ru.kovbasa.pages;
|
||||
|
||||
public record PricedCourseItem(String title, int price) {
|
||||
}
|
||||
22
src/main/java/ru/kovbasa/utils/PageUtils.java
Normal file
22
src/main/java/ru/kovbasa/utils/PageUtils.java
Normal file
@@ -0,0 +1,22 @@
|
||||
package ru.kovbasa.utils;
|
||||
|
||||
import org.openqa.selenium.JavascriptExecutor;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
|
||||
public final class PageUtils {
|
||||
|
||||
private PageUtils() {
|
||||
}
|
||||
|
||||
public static void removeBottomBanner(WebDriver driver) {
|
||||
try {
|
||||
((JavascriptExecutor) driver)
|
||||
.executeScript(
|
||||
"document.querySelectorAll('.sc-11pdrud-1.cmIXWc')" +
|
||||
".forEach(e => e.remove());"
|
||||
);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
56
src/main/java/ru/otus/stub/helper/HttpHelper.java
Normal file
56
src/main/java/ru/otus/stub/helper/HttpHelper.java
Normal file
@@ -0,0 +1,56 @@
|
||||
package ru.otus.stub.helper;
|
||||
|
||||
import io.restassured.RestAssured;
|
||||
import io.restassured.common.mapper.TypeRef;
|
||||
import ru.otus.stub.model.Course;
|
||||
import ru.otus.stub.model.User;
|
||||
import ru.otus.stub.model.UserScore;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class HttpHelper {
|
||||
|
||||
private final String baseUri;
|
||||
private final String basePath;
|
||||
|
||||
public HttpHelper(String baseUri, String basePath) {
|
||||
this.baseUri = baseUri;
|
||||
this.basePath = basePath;
|
||||
}
|
||||
|
||||
public List<User> getAllUsers() {
|
||||
return RestAssured.given()
|
||||
.baseUri(baseUri)
|
||||
.basePath(basePath)
|
||||
.when()
|
||||
.get("/user/get/all")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.extract()
|
||||
.as(new TypeRef<>() { });
|
||||
}
|
||||
|
||||
public List<Course> getAllCourses() {
|
||||
return RestAssured.given()
|
||||
.baseUri(baseUri)
|
||||
.basePath(basePath)
|
||||
.when()
|
||||
.get("/cource/get/all")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.extract()
|
||||
.as(new TypeRef<>() { });
|
||||
}
|
||||
|
||||
public UserScore getUserScore(long id) {
|
||||
return RestAssured.given()
|
||||
.baseUri(baseUri)
|
||||
.basePath(basePath)
|
||||
.when()
|
||||
.get("/user/get/{id}", id)
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.extract()
|
||||
.as(UserScore.class);
|
||||
}
|
||||
}
|
||||
38
src/main/java/ru/otus/stub/helper/MqHelper.java
Normal file
38
src/main/java/ru/otus/stub/helper/MqHelper.java
Normal file
@@ -0,0 +1,38 @@
|
||||
package ru.otus.stub.helper;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class MqHelper {
|
||||
|
||||
private final Map<String, BlockingQueue<String>> queues = new ConcurrentHashMap<>();
|
||||
|
||||
public void send(String queueName, String payload) {
|
||||
queue(queueName).add(payload);
|
||||
}
|
||||
|
||||
public String receive(String queueName, Duration timeout) {
|
||||
try {
|
||||
return queue(queueName).poll(timeout.toMillis(), TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException exception) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException("Interrupted while receiving from queue: " + queueName, exception);
|
||||
}
|
||||
}
|
||||
|
||||
public int size(String queueName) {
|
||||
return queue(queueName).size();
|
||||
}
|
||||
|
||||
public void clear(String queueName) {
|
||||
queue(queueName).clear();
|
||||
}
|
||||
|
||||
private BlockingQueue<String> queue(String queueName) {
|
||||
return queues.computeIfAbsent(queueName, key -> new LinkedBlockingQueue<>());
|
||||
}
|
||||
}
|
||||
48
src/main/java/ru/otus/stub/helper/SoapHelper.java
Normal file
48
src/main/java/ru/otus/stub/helper/SoapHelper.java
Normal file
@@ -0,0 +1,48 @@
|
||||
package ru.otus.stub.helper;
|
||||
|
||||
import io.restassured.RestAssured;
|
||||
import io.restassured.path.xml.XmlPath;
|
||||
import ru.otus.stub.model.UserScore;
|
||||
|
||||
public class SoapHelper {
|
||||
|
||||
private final String baseUri;
|
||||
private final String basePath;
|
||||
|
||||
public SoapHelper(String baseUri, String basePath) {
|
||||
this.baseUri = baseUri;
|
||||
this.basePath = basePath;
|
||||
}
|
||||
|
||||
public UserScore getUserScore(long id) {
|
||||
final String request = """
|
||||
<soapenv:Envelope
|
||||
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
|
||||
xmlns:stu="http://otus.ru/stub">
|
||||
<soapenv:Header/>
|
||||
<soapenv:Body>
|
||||
<stu:GetUserScoreRequest>
|
||||
<stu:id>${id}</stu:id>
|
||||
</stu:GetUserScoreRequest>
|
||||
</soapenv:Body>
|
||||
</soapenv:Envelope>
|
||||
""".replace("${id}", String.valueOf(id));
|
||||
|
||||
final String response = RestAssured.given()
|
||||
.baseUri(baseUri)
|
||||
.basePath(basePath)
|
||||
.contentType("text/xml; charset=UTF-8")
|
||||
.body(request)
|
||||
.when()
|
||||
.post("/soap/user/score")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.extract()
|
||||
.asString();
|
||||
|
||||
final XmlPath xml = new XmlPath(response);
|
||||
final String name = xml.getString("Envelope.Body.GetUserScoreResponse.name");
|
||||
final int score = xml.getInt("Envelope.Body.GetUserScoreResponse.score");
|
||||
return new UserScore(name, score);
|
||||
}
|
||||
}
|
||||
63
src/main/java/ru/otus/stub/helper/SqlHelper.java
Normal file
63
src/main/java/ru/otus/stub/helper/SqlHelper.java
Normal file
@@ -0,0 +1,63 @@
|
||||
package ru.otus.stub.helper;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.ResultSetMetaData;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class SqlHelper {
|
||||
|
||||
private final String jdbcUrl;
|
||||
private final String username;
|
||||
private final String password;
|
||||
|
||||
public SqlHelper(String jdbcUrl, String username, String password) {
|
||||
this.jdbcUrl = jdbcUrl;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public int executeUpdate(String sql) {
|
||||
try (Connection connection = openConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
return statement.executeUpdate();
|
||||
} catch (SQLException exception) {
|
||||
throw new IllegalStateException("SQL update failed: " + sql, exception);
|
||||
}
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> selectRows(String sql) {
|
||||
try (Connection connection = openConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(sql);
|
||||
ResultSet resultSet = statement.executeQuery()) {
|
||||
return mapRows(resultSet);
|
||||
} catch (SQLException exception) {
|
||||
throw new IllegalStateException("SQL query failed: " + sql, exception);
|
||||
}
|
||||
}
|
||||
|
||||
private Connection openConnection() throws SQLException {
|
||||
return DriverManager.getConnection(jdbcUrl, username, password);
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> mapRows(ResultSet resultSet) throws SQLException {
|
||||
final List<Map<String, Object>> rows = new ArrayList<>();
|
||||
final ResultSetMetaData metaData = resultSet.getMetaData();
|
||||
final int columnsCount = metaData.getColumnCount();
|
||||
|
||||
while (resultSet.next()) {
|
||||
final Map<String, Object> row = new LinkedHashMap<>();
|
||||
for (int column = 1; column <= columnsCount; column++) {
|
||||
row.put(metaData.getColumnLabel(column), resultSet.getObject(column));
|
||||
}
|
||||
rows.add(row);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
4
src/main/java/ru/otus/stub/model/Course.java
Normal file
4
src/main/java/ru/otus/stub/model/Course.java
Normal file
@@ -0,0 +1,4 @@
|
||||
package ru.otus.stub.model;
|
||||
|
||||
public record Course(String name, int price) {
|
||||
}
|
||||
4
src/main/java/ru/otus/stub/model/User.java
Normal file
4
src/main/java/ru/otus/stub/model/User.java
Normal file
@@ -0,0 +1,4 @@
|
||||
package ru.otus.stub.model;
|
||||
|
||||
public record User(String name, String cource, String email, int age) {
|
||||
}
|
||||
4
src/main/java/ru/otus/stub/model/UserScore.java
Normal file
4
src/main/java/ru/otus/stub/model/UserScore.java
Normal file
@@ -0,0 +1,4 @@
|
||||
package ru.otus.stub.model;
|
||||
|
||||
public record UserScore(String name, int score) {
|
||||
}
|
||||
17
src/test/java/ru/kovbasa/bdd/CucumberTest.java
Normal file
17
src/test/java/ru/kovbasa/bdd/CucumberTest.java
Normal file
@@ -0,0 +1,17 @@
|
||||
package ru.kovbasa.bdd;
|
||||
|
||||
import org.junit.platform.suite.api.ConfigurationParameter;
|
||||
import org.junit.platform.suite.api.IncludeEngines;
|
||||
import org.junit.platform.suite.api.SelectClasspathResource;
|
||||
import org.junit.platform.suite.api.Suite;
|
||||
|
||||
import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME;
|
||||
import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PROPERTY_NAME;
|
||||
|
||||
@Suite
|
||||
@IncludeEngines("cucumber")
|
||||
@SelectClasspathResource("features/stub_frontend.feature")
|
||||
@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "ru.kovbasa.bdd")
|
||||
@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty")
|
||||
public class CucumberTest {
|
||||
}
|
||||
60
src/test/java/ru/kovbasa/bdd/hooks/Hooks.java
Normal file
60
src/test/java/ru/kovbasa/bdd/hooks/Hooks.java
Normal file
@@ -0,0 +1,60 @@
|
||||
package ru.kovbasa.bdd.hooks;
|
||||
|
||||
import com.google.inject.Injector;
|
||||
import io.cucumber.java.After;
|
||||
import io.cucumber.java.AfterAll;
|
||||
import io.cucumber.java.BeforeAll;
|
||||
import com.github.tomakehurst.wiremock.WireMockServer;
|
||||
import ru.kovbasa.config.InjectorProvider;
|
||||
import ru.kovbasa.driver.WebDriverProvider;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.get;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
|
||||
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
|
||||
|
||||
public class Hooks {
|
||||
|
||||
private static WireMockServer frontendStubServer;
|
||||
|
||||
@BeforeAll
|
||||
public static void beforeAll() {
|
||||
if (frontendStubServer == null) {
|
||||
frontendStubServer = new WireMockServer(wireMockConfig().port(8089));
|
||||
frontendStubServer.start();
|
||||
frontendStubServer.stubFor(get(urlPathEqualTo("/frontend"))
|
||||
.willReturn(aResponse()
|
||||
.withStatus(200)
|
||||
.withHeader("Content-Type", "text/html; charset=UTF-8")
|
||||
.withBody("""
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Stub Frontend</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1 id="title">Stub Frontend</h1>
|
||||
<p id="description">UI connected to WireMock stub backend</p>
|
||||
</body>
|
||||
</html>
|
||||
""")));
|
||||
frontendStubServer.stubFor(get(urlPathEqualTo("/favicon.ico"))
|
||||
.willReturn(aResponse().withStatus(204)));
|
||||
}
|
||||
}
|
||||
|
||||
@After
|
||||
public void afterScenario() {
|
||||
final Injector injector = InjectorProvider.getInjector();
|
||||
injector.getInstance(WebDriverProvider.class).quit();
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void afterAll() {
|
||||
if (frontendStubServer != null) {
|
||||
frontendStubServer.stop();
|
||||
frontendStubServer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
159
src/test/java/ru/kovbasa/bdd/steps/CatalogSteps.java
Normal file
159
src/test/java/ru/kovbasa/bdd/steps/CatalogSteps.java
Normal file
@@ -0,0 +1,159 @@
|
||||
package ru.kovbasa.bdd.steps;
|
||||
|
||||
import com.google.inject.Injector;
|
||||
import io.cucumber.java.ru.Дано;
|
||||
import io.cucumber.java.ru.И;
|
||||
import io.cucumber.java.ru.Когда;
|
||||
import io.cucumber.java.ru.Тогда;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import ru.kovbasa.config.InjectorProvider;
|
||||
import ru.kovbasa.pages.CatalogPage;
|
||||
import ru.kovbasa.pages.CourseItem;
|
||||
import ru.kovbasa.pages.CoursePage;
|
||||
import ru.kovbasa.pages.PricedCourseItem;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class CatalogSteps {
|
||||
|
||||
private static final DateTimeFormatter DATE_FORMATTER =
|
||||
DateTimeFormatter.ofPattern("dd.MM.yyyy", Locale.forLanguageTag("ru"));
|
||||
|
||||
private CoursePage openedCoursePage;
|
||||
private List<CourseItem> coursesStartingFromDate;
|
||||
private LocalDate searchDate;
|
||||
private PricedCourseItem mostExpensiveCourse;
|
||||
private PricedCourseItem cheapestCourse;
|
||||
private PricedCourseItem mostExpensiveCatalogCourse;
|
||||
private PricedCourseItem cheapestCatalogCourse;
|
||||
|
||||
@Дано("Открыт браузер {word}")
|
||||
public void openBrowser(String browser) {
|
||||
System.setProperty("browser", browser.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
@Когда("Открыт каталог курсов")
|
||||
public void openCatalog() {
|
||||
getCatalogPage().open();
|
||||
}
|
||||
|
||||
@Когда("Найден курс {string} и выбран случайный из найденных")
|
||||
public void findCourseAndOpenRandom(String courseName) {
|
||||
openedCoursePage = getCatalogPage().clickRandomCourseByName(courseName);
|
||||
}
|
||||
|
||||
@Тогда("Заголовок страницы курса содержит {string}")
|
||||
public void coursePageTitleContains(String expectedCourseName) {
|
||||
final String title = openedCoursePage.getCourseTitle();
|
||||
Assertions.assertTrue(
|
||||
title.toLowerCase(Locale.ROOT).contains(expectedCourseName.toLowerCase(Locale.ROOT)),
|
||||
"Заголовок страницы курса должен содержать: " + expectedCourseName + ", фактически: " + title
|
||||
);
|
||||
}
|
||||
|
||||
@Когда("Найдены курсы со стартом {string} или позже")
|
||||
public void findCoursesFromDate(String date) {
|
||||
searchDate = LocalDate.parse(date, DATE_FORMATTER);
|
||||
coursesStartingFromDate = getCatalogPage().findCoursesStartingFrom(searchDate);
|
||||
}
|
||||
|
||||
@Тогда("В консоль выведены найденные курсы и даты старта")
|
||||
public void printCoursesAndStartDates() {
|
||||
Assertions.assertFalse(coursesStartingFromDate.isEmpty(), "Не найдено курсов по заданной дате");
|
||||
|
||||
coursesStartingFromDate.forEach(course -> System.out.println(
|
||||
"Курс: " + course.title() + ", дата старта: " + course.startDate().format(DATE_FORMATTER)
|
||||
));
|
||||
|
||||
final boolean allAfterOrEqual = coursesStartingFromDate.stream()
|
||||
.allMatch(course -> !course.startDate().isBefore(searchDate));
|
||||
|
||||
Assertions.assertTrue(
|
||||
allAfterOrEqual,
|
||||
"Среди найденных есть курсы с датой старта раньше " + searchDate.format(DATE_FORMATTER)
|
||||
);
|
||||
}
|
||||
|
||||
@Когда("Открыт раздел Обучение и Подготовительные курсы")
|
||||
public void openPreparatoryCourses() {
|
||||
getCatalogPage().openPreparatoryCourses();
|
||||
}
|
||||
|
||||
@И("Выбраны самый дорогой и самый дешевый курсы с помощью filter")
|
||||
public void findMostExpensiveAndCheapestByFilter() {
|
||||
final List<PricedCourseItem> pricedCourses = getCatalogPage().getPreparatoryCoursesWithDiscountedFullPrice();
|
||||
Assertions.assertFalse(pricedCourses.isEmpty(), "Не найдено курсов с ценой");
|
||||
|
||||
final int maxPrice = pricedCourses.stream().mapToInt(PricedCourseItem::price).max().orElseThrow();
|
||||
final int minPrice = pricedCourses.stream().mapToInt(PricedCourseItem::price).min().orElseThrow();
|
||||
|
||||
mostExpensiveCourse = pricedCourses.stream()
|
||||
.filter(course -> course.price() == maxPrice)
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
|
||||
cheapestCourse = pricedCourses.stream()
|
||||
.filter(course -> course.price() == minPrice)
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
}
|
||||
|
||||
@Тогда("В консоль выведена информация о самом дорогом и самом дешевом курсе")
|
||||
public void printMostExpensiveAndCheapestCourse() {
|
||||
System.out.println(
|
||||
"Самый дорогой курс: " + mostExpensiveCourse.title() + ", цена: " + mostExpensiveCourse.price()
|
||||
);
|
||||
System.out.println(
|
||||
"Самый дешевый курс: " + cheapestCourse.title() + ", цена: " + cheapestCourse.price()
|
||||
);
|
||||
|
||||
Assertions.assertTrue(
|
||||
mostExpensiveCourse.price() >= cheapestCourse.price(),
|
||||
"Цена самого дорогого курса должна быть не меньше цены самого дешевого"
|
||||
);
|
||||
}
|
||||
|
||||
@Когда("В каталоге курсов выбраны самый дорогой и самый дешевый курсы по полной стоимости со скидкой с помощью filter")
|
||||
public void findMostExpensiveAndCheapestInCatalogByFullDiscountedPrice() {
|
||||
final List<PricedCourseItem> pricedCourses = getCatalogPage().getCatalogCoursesWithDiscountedFullPrice();
|
||||
Assertions.assertFalse(pricedCourses.isEmpty(), "Не найдено курсов с ценой в общем каталоге");
|
||||
|
||||
final int maxPrice = pricedCourses.stream().mapToInt(PricedCourseItem::price).max().orElseThrow();
|
||||
final int minPrice = pricedCourses.stream().mapToInt(PricedCourseItem::price).min().orElseThrow();
|
||||
|
||||
mostExpensiveCatalogCourse = pricedCourses.stream()
|
||||
.filter(course -> course.price() == maxPrice)
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
|
||||
cheapestCatalogCourse = pricedCourses.stream()
|
||||
.filter(course -> course.price() == minPrice)
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
}
|
||||
|
||||
@Тогда("В консоль выведена информация о самом дорогом и самом дешевом курсе в каталоге")
|
||||
public void printMostExpensiveAndCheapestCourseInCatalog() {
|
||||
System.out.println(
|
||||
"Самый дорогой курс в каталоге: "
|
||||
+ mostExpensiveCatalogCourse.title() + ", цена: " + mostExpensiveCatalogCourse.price()
|
||||
);
|
||||
System.out.println(
|
||||
"Самый дешевый курс в каталоге: "
|
||||
+ cheapestCatalogCourse.title() + ", цена: " + cheapestCatalogCourse.price()
|
||||
);
|
||||
|
||||
Assertions.assertTrue(
|
||||
mostExpensiveCatalogCourse.price() >= cheapestCatalogCourse.price(),
|
||||
"Цена самого дорогого курса в каталоге должна быть не меньше цены самого дешевого"
|
||||
);
|
||||
}
|
||||
|
||||
private CatalogPage getCatalogPage() {
|
||||
final Injector injector = InjectorProvider.getInjector();
|
||||
return injector.getInstance(CatalogPage.class);
|
||||
}
|
||||
}
|
||||
53
src/test/java/ru/kovbasa/bdd/steps/StubFrontendSteps.java
Normal file
53
src/test/java/ru/kovbasa/bdd/steps/StubFrontendSteps.java
Normal file
@@ -0,0 +1,53 @@
|
||||
package ru.kovbasa.bdd.steps;
|
||||
|
||||
import com.google.inject.Injector;
|
||||
import io.cucumber.java.ru.Дано;
|
||||
import io.cucumber.java.ru.Когда;
|
||||
import io.cucumber.java.ru.Тогда;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.openqa.selenium.By;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import ru.kovbasa.config.InjectorProvider;
|
||||
import ru.kovbasa.config.TestConfig;
|
||||
import ru.kovbasa.driver.WebDriverProvider;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class StubFrontendSteps {
|
||||
|
||||
@Дано("Открыт браузер для frontend {word}")
|
||||
public void openBrowser(String browser) {
|
||||
final String existingBrowser = System.getProperty("browser");
|
||||
if (existingBrowser == null || existingBrowser.isBlank()) {
|
||||
System.setProperty("browser", browser.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
}
|
||||
|
||||
@Когда("Открыта страница stub frontend")
|
||||
public void openStubFrontend() {
|
||||
getDriver().get(TestConfig.getBaseUrl() + "/frontend");
|
||||
}
|
||||
|
||||
@Тогда("Заголовок страницы равен {string}")
|
||||
public void titleShouldBe(String expectedTitle) {
|
||||
final String actualTitle = getDriver().getTitle();
|
||||
Assertions.assertEquals(expectedTitle, actualTitle);
|
||||
}
|
||||
|
||||
@Тогда("На странице есть заголовок {string}")
|
||||
public void pageHasHeading(String expectedHeading) {
|
||||
final String actualHeading = getDriver().findElement(By.id("title")).getText();
|
||||
Assertions.assertEquals(expectedHeading, actualHeading);
|
||||
}
|
||||
|
||||
@Тогда("На странице есть описание {string}")
|
||||
public void pageHasDescription(String expectedDescription) {
|
||||
final String actualDescription = getDriver().findElement(By.id("description")).getText();
|
||||
Assertions.assertEquals(expectedDescription, actualDescription);
|
||||
}
|
||||
|
||||
private WebDriver getDriver() {
|
||||
final Injector injector = InjectorProvider.getInjector();
|
||||
return injector.getInstance(WebDriverProvider.class).getDriver();
|
||||
}
|
||||
}
|
||||
92
src/test/java/ru/otus/stub/tests/BaseWireMockStubTest.java
Normal file
92
src/test/java/ru/otus/stub/tests/BaseWireMockStubTest.java
Normal file
@@ -0,0 +1,92 @@
|
||||
package ru.otus.stub.tests;
|
||||
|
||||
import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.get;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.post;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching;
|
||||
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
|
||||
|
||||
abstract class BaseWireMockStubTest {
|
||||
|
||||
@RegisterExtension
|
||||
protected static final WireMockExtension WIREMOCK = WireMockExtension.newInstance()
|
||||
.options(wireMockConfig().dynamicPort())
|
||||
.build();
|
||||
|
||||
@BeforeEach
|
||||
void registerContracts() {
|
||||
WIREMOCK.resetAll();
|
||||
|
||||
WIREMOCK.stubFor(get(urlPathEqualTo("/user/get/all"))
|
||||
.willReturn(aResponse()
|
||||
.withStatus(200)
|
||||
.withHeader("Content-Type", "application/json")
|
||||
.withBody("""
|
||||
[
|
||||
{ "name": "Test user", "cource": "QA", "email": "test@test.test", "age": 23 },
|
||||
{ "name": "Test user 2", "cource": "QA java", "email": "test2@test.test", "age": 27 }
|
||||
]
|
||||
""")));
|
||||
|
||||
WIREMOCK.stubFor(get(urlPathEqualTo("/cource/get/all"))
|
||||
.willReturn(aResponse()
|
||||
.withStatus(200)
|
||||
.withHeader("Content-Type", "application/json")
|
||||
.withBody("""
|
||||
[
|
||||
{ "name": "QA java", "price": 15000 },
|
||||
{ "name": "Java", "price": 12000 }
|
||||
]
|
||||
""")));
|
||||
|
||||
WIREMOCK.stubFor(get(urlPathMatching("/user/get/\\d+"))
|
||||
.willReturn(aResponse()
|
||||
.withStatus(200)
|
||||
.withHeader("Content-Type", "application/json")
|
||||
.withBody("""
|
||||
{ "name": "Test user", "score": 78 }
|
||||
""")));
|
||||
|
||||
WIREMOCK.stubFor(post(urlPathEqualTo("/soap/user/score"))
|
||||
.withRequestBody(com.github.tomakehurst.wiremock.client.WireMock.containing("GetUserScoreRequest"))
|
||||
.withRequestBody(com.github.tomakehurst.wiremock.client.WireMock.containing("<stu:id>"))
|
||||
.willReturn(aResponse()
|
||||
.withStatus(200)
|
||||
.withHeader("Content-Type", "text/xml; charset=UTF-8")
|
||||
.withBody("""
|
||||
<Envelope>
|
||||
<Body>
|
||||
<GetUserScoreResponse>
|
||||
<name>Test user</name>
|
||||
<score>78</score>
|
||||
</GetUserScoreResponse>
|
||||
</Body>
|
||||
</Envelope>
|
||||
""")));
|
||||
|
||||
WIREMOCK.stubFor(get(urlPathEqualTo("/frontend"))
|
||||
.withQueryParam("active", equalTo("true"))
|
||||
.willReturn(aResponse()
|
||||
.withStatus(200)
|
||||
.withHeader("Content-Type", "text/html; charset=UTF-8")
|
||||
.withBody("""
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Stub Frontend</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1 id="title">Stub Frontend</h1>
|
||||
<p id="description">UI connected to WireMock stub backend</p>
|
||||
</body>
|
||||
</html>
|
||||
""")));
|
||||
}
|
||||
}
|
||||
45
src/test/java/ru/otus/stub/tests/HttpHelperTest.java
Normal file
45
src/test/java/ru/otus/stub/tests/HttpHelperTest.java
Normal file
@@ -0,0 +1,45 @@
|
||||
package ru.otus.stub.tests;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import ru.otus.stub.helper.HttpHelper;
|
||||
import ru.otus.stub.model.Course;
|
||||
import ru.otus.stub.model.User;
|
||||
import ru.otus.stub.model.UserScore;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
|
||||
class HttpHelperTest extends BaseWireMockStubTest {
|
||||
|
||||
private HttpHelper httpHelper;
|
||||
|
||||
@BeforeEach
|
||||
void setUpHelper() {
|
||||
httpHelper = new HttpHelper(WIREMOCK.baseUrl(), "");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGetAllUsers() {
|
||||
final List<User> users = httpHelper.getAllUsers();
|
||||
assertFalse(users.isEmpty());
|
||||
assertEquals("Test user", users.getFirst().name());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGetAllCourses() {
|
||||
final List<Course> courses = httpHelper.getAllCourses();
|
||||
assertFalse(courses.isEmpty());
|
||||
assertEquals("QA java", courses.getFirst().name());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGetUserScore() {
|
||||
final UserScore userScore = httpHelper.getUserScore(1);
|
||||
assertEquals("Test user", userScore.name());
|
||||
assertEquals(78, userScore.score());
|
||||
}
|
||||
}
|
||||
23
src/test/java/ru/otus/stub/tests/MqHelperTest.java
Normal file
23
src/test/java/ru/otus/stub/tests/MqHelperTest.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package ru.otus.stub.tests;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import ru.otus.stub.helper.MqHelper;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
class MqHelperTest {
|
||||
|
||||
@Test
|
||||
void shouldSendAndReceiveMessage() {
|
||||
final MqHelper mqHelper = new MqHelper();
|
||||
final String queue = "scores";
|
||||
final String payload = "{\"name\":\"Test user\",\"score\":78}";
|
||||
|
||||
mqHelper.send(queue, payload);
|
||||
final String received = mqHelper.receive(queue, Duration.ofSeconds(1));
|
||||
|
||||
Assertions.assertEquals(payload, received);
|
||||
Assertions.assertEquals(0, mqHelper.size(queue));
|
||||
}
|
||||
}
|
||||
24
src/test/java/ru/otus/stub/tests/SoapHelperTest.java
Normal file
24
src/test/java/ru/otus/stub/tests/SoapHelperTest.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package ru.otus.stub.tests;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import ru.otus.stub.helper.SoapHelper;
|
||||
import ru.otus.stub.model.UserScore;
|
||||
|
||||
class SoapHelperTest extends BaseWireMockStubTest {
|
||||
|
||||
private SoapHelper soapHelper;
|
||||
|
||||
@BeforeEach
|
||||
void setUpHelper() {
|
||||
soapHelper = new SoapHelper(WIREMOCK.baseUrl(), "");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGetUserScoreBySoap() {
|
||||
final UserScore userScore = soapHelper.getUserScore(1);
|
||||
Assertions.assertEquals("Test user", userScore.name());
|
||||
Assertions.assertEquals(78, userScore.score());
|
||||
}
|
||||
}
|
||||
31
src/test/java/ru/otus/stub/tests/SqlHelperTest.java
Normal file
31
src/test/java/ru/otus/stub/tests/SqlHelperTest.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package ru.otus.stub.tests;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import ru.otus.stub.helper.SqlHelper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
class SqlHelperTest {
|
||||
|
||||
@Test
|
||||
void shouldReadDataFromDatabase() {
|
||||
final String jdbcUrl = "jdbc:h2:mem:stubdb;DB_CLOSE_DELAY=-1";
|
||||
final SqlHelper sqlHelper = new SqlHelper(jdbcUrl, "sa", "");
|
||||
|
||||
sqlHelper.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id BIGINT PRIMARY KEY,
|
||||
name VARCHAR(100),
|
||||
score INT
|
||||
)
|
||||
""");
|
||||
sqlHelper.executeUpdate("INSERT INTO users(id, name, score) VALUES (1, 'Test user', 78)");
|
||||
|
||||
final List<Map<String, Object>> rows = sqlHelper.selectRows("SELECT name, score FROM users WHERE id = 1");
|
||||
Assertions.assertEquals(1, rows.size());
|
||||
Assertions.assertEquals("Test user", rows.getFirst().get("NAME"));
|
||||
Assertions.assertEquals(78, ((Number) rows.getFirst().get("SCORE")).intValue());
|
||||
}
|
||||
}
|
||||
50
src/test/java/ru/otus/stub/tests/StubContractTest.java
Normal file
50
src/test/java/ru/otus/stub/tests/StubContractTest.java
Normal file
@@ -0,0 +1,50 @@
|
||||
package ru.otus.stub.tests;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import ru.otus.stub.helper.HttpHelper;
|
||||
import ru.otus.stub.model.Course;
|
||||
import ru.otus.stub.model.User;
|
||||
import ru.otus.stub.model.UserScore;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
class StubContractTest extends BaseWireMockStubTest {
|
||||
|
||||
private HttpHelper httpHelper;
|
||||
|
||||
@BeforeEach
|
||||
void setUpHelper() {
|
||||
httpHelper = new HttpHelper(WIREMOCK.baseUrl(), "");
|
||||
}
|
||||
|
||||
@Test
|
||||
void usersContractShouldContainRequiredFieldsAndExpectedValues() {
|
||||
final List<User> users = httpHelper.getAllUsers();
|
||||
Assertions.assertFalse(users.isEmpty(), "Users list must not be empty");
|
||||
|
||||
final User first = users.getFirst();
|
||||
Assertions.assertEquals("Test user", first.name());
|
||||
Assertions.assertEquals("QA", first.cource());
|
||||
Assertions.assertEquals("test@test.test", first.email());
|
||||
Assertions.assertEquals(23, first.age());
|
||||
}
|
||||
|
||||
@Test
|
||||
void coursesContractShouldContainExpectedItems() {
|
||||
final List<Course> courses = httpHelper.getAllCourses();
|
||||
Assertions.assertTrue(courses.size() >= 2, "Courses list must contain at least 2 items");
|
||||
|
||||
final Course first = courses.getFirst();
|
||||
Assertions.assertEquals("QA java", first.name());
|
||||
Assertions.assertEquals(15000, first.price());
|
||||
}
|
||||
|
||||
@Test
|
||||
void userScoreContractShouldContainExpectedValues() {
|
||||
final UserScore score = httpHelper.getUserScore(1);
|
||||
Assertions.assertEquals("Test user", score.name());
|
||||
Assertions.assertEquals(78, score.score());
|
||||
}
|
||||
}
|
||||
42
src/test/java/ru/otus/stub/tests/StubSchemaTest.java
Normal file
42
src/test/java/ru/otus/stub/tests/StubSchemaTest.java
Normal file
@@ -0,0 +1,42 @@
|
||||
package ru.otus.stub.tests;
|
||||
|
||||
import io.restassured.RestAssured;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath;
|
||||
|
||||
class StubSchemaTest extends BaseWireMockStubTest {
|
||||
|
||||
@Test
|
||||
void usersResponseShouldMatchSchema() {
|
||||
RestAssured.given()
|
||||
.baseUri(WIREMOCK.baseUrl())
|
||||
.when()
|
||||
.get("/user/get/all")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.body(matchesJsonSchemaInClasspath("schemas/users-schema.json"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void coursesResponseShouldMatchSchema() {
|
||||
RestAssured.given()
|
||||
.baseUri(WIREMOCK.baseUrl())
|
||||
.when()
|
||||
.get("/cource/get/all")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.body(matchesJsonSchemaInClasspath("schemas/courses-schema.json"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void scoreResponseShouldMatchSchema() {
|
||||
RestAssured.given()
|
||||
.baseUri(WIREMOCK.baseUrl())
|
||||
.when()
|
||||
.get("/user/get/{id}", 1)
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.body(matchesJsonSchemaInClasspath("schemas/score-schema.json"));
|
||||
}
|
||||
}
|
||||
29
src/test/resources/features/catalog.feature
Normal file
29
src/test/resources/features/catalog.feature
Normal file
@@ -0,0 +1,29 @@
|
||||
# language: ru
|
||||
|
||||
Функция: Каталог курсов OTUS
|
||||
Как пользователь
|
||||
Я хочу работать с каталогом курсов через BDD
|
||||
Чтобы проверять поиск, фильтрацию по датам и выбор по цене
|
||||
|
||||
Сценарий: Выбор браузера и поиск курса по названию со случайным выбором
|
||||
Дано Открыт браузер Chrome
|
||||
Когда Открыт каталог курсов
|
||||
И Найден курс "Python" и выбран случайный из найденных
|
||||
Тогда Заголовок страницы курса содержит "Python"
|
||||
|
||||
Сценарий: Поиск курсов, стартующих в указанную дату или позже
|
||||
Дано Открыт браузер Chrome
|
||||
Когда Открыт каталог курсов
|
||||
И Найдены курсы со стартом "01.01.2025" или позже
|
||||
Тогда В консоль выведены найденные курсы и даты старта
|
||||
|
||||
Сценарий: Поиск самого дорогого и самого дешевого подготовительного курса
|
||||
Дано Открыт браузер Chrome
|
||||
Когда Открыт раздел Обучение и Подготовительные курсы
|
||||
И Выбраны самый дорогой и самый дешевый курсы с помощью filter
|
||||
Тогда В консоль выведена информация о самом дорогом и самом дешевом курсе
|
||||
|
||||
Сценарий: Поиск самого дорогого и самого дешевого курса в каталоге по полной стоимости со скидкой
|
||||
Дано Открыт браузер Chrome
|
||||
Когда В каталоге курсов выбраны самый дорогой и самый дешевый курсы по полной стоимости со скидкой с помощью filter
|
||||
Тогда В консоль выведена информация о самом дорогом и самом дешевом курсе в каталоге
|
||||
10
src/test/resources/features/stub_frontend.feature
Normal file
10
src/test/resources/features/stub_frontend.feature
Normal file
@@ -0,0 +1,10 @@
|
||||
# language: ru
|
||||
|
||||
Функция: Stub Frontend
|
||||
|
||||
Сценарий: Проверка подключения frontend к stub-серверу
|
||||
Дано Открыт браузер для frontend chrome
|
||||
Когда Открыта страница stub frontend
|
||||
Тогда Заголовок страницы равен "Stub Frontend"
|
||||
И На странице есть заголовок "Stub Frontend"
|
||||
И На странице есть описание "UI connected to WireMock stub backend"
|
||||
15
src/test/resources/logback-test.xml
Normal file
15
src/test/resources/logback-test.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss} %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<logger name="io.github.bonigarcia.wdm" level="WARN"/>
|
||||
<logger name="org.openqa.selenium" level="WARN"/>
|
||||
|
||||
<root level="WARN">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
</root>
|
||||
</configuration>
|
||||
8
src/test/resources/logging.properties
Normal file
8
src/test/resources/logging.properties
Normal file
@@ -0,0 +1,8 @@
|
||||
handlers= java.util.logging.ConsoleHandler
|
||||
.level= WARNING
|
||||
|
||||
java.util.logging.ConsoleHandler.level = SEVERE
|
||||
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
|
||||
|
||||
org.openqa.selenium.devtools.level = SEVERE
|
||||
org.openqa.selenium.devtools.CdpVersionFinder.level = SEVERE
|
||||
14
src/test/resources/schemas/courses-schema.json
Normal file
14
src/test/resources/schemas/courses-schema.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["name", "price"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"price": { "type": "integer", "minimum": 0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
10
src/test/resources/schemas/score-schema.json
Normal file
10
src/test/resources/schemas/score-schema.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"required": ["name", "score"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"score": { "type": "integer", "minimum": 0, "maximum": 100 }
|
||||
}
|
||||
}
|
||||
16
src/test/resources/schemas/users-schema.json
Normal file
16
src/test/resources/schemas/users-schema.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["name", "cource", "email", "age"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"cource": { "type": "string" },
|
||||
"email": { "type": "string" },
|
||||
"age": { "type": "integer", "minimum": 0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user