From b97c369f466bdcfdcac23b2861d1bdf653488abc Mon Sep 17 00:00:00 2001 From: spawn Date: Sun, 15 Feb 2026 14:02:49 +0300 Subject: [PATCH] Refactor petstore tests and refresh build setup --- .gitignore | 39 +--- README.md | 130 ++++++------- checkstyle.xml | 34 ++++ pom.xml | 120 ++++++++---- spotbugs-exclude.xml | 6 + .../ru/otus/petstore/config/TestConfig.java | 21 +++ .../java/ru/otus/petstore/model/Category.java | 23 +++ src/main/java/ru/otus/petstore/model/Pet.java | 58 ++++++ src/main/java/ru/otus/petstore/model/Tag.java | 23 +++ .../ru/otus/petstore/service/BaseService.java | 36 ++++ .../ru/otus/petstore/service/PetService.java | 72 ++++++++ .../java/ru/otus/petstore/PetStoreTests.java | 174 ------------------ .../tests/PetNegativeScenariosTest.java | 69 +++++++ .../tests/PetPositiveScenariosTest.java | 84 +++++++++ .../ru/otus/petstore/util/PetFactory.java | 30 +++ src/test/resources/schemas/pet-schema.json | 52 ++++++ 16 files changed, 655 insertions(+), 316 deletions(-) create mode 100644 checkstyle.xml create mode 100644 spotbugs-exclude.xml create mode 100644 src/main/java/ru/otus/petstore/config/TestConfig.java create mode 100644 src/main/java/ru/otus/petstore/model/Category.java create mode 100644 src/main/java/ru/otus/petstore/model/Pet.java create mode 100644 src/main/java/ru/otus/petstore/model/Tag.java create mode 100644 src/main/java/ru/otus/petstore/service/BaseService.java create mode 100644 src/main/java/ru/otus/petstore/service/PetService.java delete mode 100644 src/test/java/ru/otus/petstore/PetStoreTests.java create mode 100644 src/test/java/ru/otus/petstore/tests/PetNegativeScenariosTest.java create mode 100644 src/test/java/ru/otus/petstore/tests/PetPositiveScenariosTest.java create mode 100644 src/test/java/ru/otus/petstore/util/PetFactory.java create mode 100644 src/test/resources/schemas/pet-schema.json diff --git a/.gitignore b/.gitignore index 04eafc6..b9124b1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,41 +1,6 @@ target/ -!.mvn/wrapper/maven-wrapper.jar -!**/src/main/**/target/ -!**/src/test/**/target/ - -### IntelliJ IDEA ### -.idea/modules.xml -.idea/jarRepositories.xml -.idea/compiler.xml -.idea/libraries/ -*.iws -*.iml -*.ipr - -### Eclipse ### -.apt_generated -.classpath -.factorypath -.project -.settings -.springBeans -.sts4-cache - -### NetBeans ### -/nbproject/private/ -/nbbuild/ -/dist/ -/nbdist/ -/.nb-gradle/ build/ -!**/src/main/**/build/ -!**/src/test/**/build/ - -### VS Code ### +.idea/ .vscode/ - -### Mac OS ### +*.iml .DS_Store -/.idea/ -/.mvn/ -/.gitignore diff --git a/README.md b/README.md index 3988049..f4efcaf 100644 --- a/README.md +++ b/README.md @@ -1,80 +1,64 @@ -# Автотесты для Petstore API +# OTUS Rest-assured Homework 3 -Этот проект содержит набор автотестов для [Swagger Petstore API](https://petstore.swagger.io/v2) с использованием библиотек **Rest-assured** и **JUnit 5**. Проект оформлен как Maven-проект с поддержкой JDK 24. +## Цель проекта +Автоматизировать API-сценарии для `https://petstore.swagger.io` с использованием Rest-assured, JUnit Jupiter и quality gates (Checkstyle + SpotBugs). + +## Стек технологий +- 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`) + +## Реализованные сценарии +1. `POST /pet` + `GET /pet/{id}`: + - создание питомца; + - повторное чтение по id; + - проверка, что вернулся именно созданный объект. + +2. `GET /pet/{id}`: + - проверка схемы ответа через Json Schema Validator. + +3. Негативные сценарии: + - `POST /pet` с malformed JSON возвращает ошибку; + - `GET /pet/{id}` для удаленного/несуществующего id возвращает `404`; + - `GET /pet/findByStatus` с невалидным статусом возвращает пустой список. + +## Архитектура +- 2-уровневый дизайн: + - `service` слой (HTTP-вызовы + спецификации); + - `tests` слой (сценарии). +- Тесты создают сервис напрямую (`new PetService()`), без DI-фреймворка. ## Структура проекта -```plaintext -hw3/ -├── pom.xml # Maven-конфигурация -├── README.md # Readme файл -└── src - └── test - └── java - └── ru - └── otus - └── petstore - └── PetStoreTests.java # Автотесты +- `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. Только тесты +```bash +mvn test ``` -## Обзор +### 2. Полная проверка (тесты + Checkstyle + SpotBugs) +```bash +mvn verify +``` -**Rest-assured** — это библиотека для тестирования REST API, предоставляющая удобный DSL (domain-specific language) -для формирования HTTP-запросов, их отправки и проверки ответов. В данном проекте с её помощью отправляются GET и POST -запросы к Petstore API, а также производятся проверки кодов ответа и содержимого JSON. +## Параметры запуска +- `base.uri` (по умолчанию `https://petstore.swagger.io`) +- `base.path` (по умолчанию `/v2`) -### Реализованные сценарии тестирования - -1. **Тест: Поиск питомцев со статусом "available"** - Отправляется GET-запрос на `/pet/findByStatus` с параметром `status=available`. - **Ожидаемый результат:** HTTP 200 и у всех питомцев в ответе значение поля `status` равно `"available"`. - -2. **Тест: Поиск питомцев по несуществующему статусу** - Отправляется GET-запрос с параметром `status=invalidStatus`. - **Ожидаемый результат:** HTTP 200 и пустой массив в ответе. - -3. **Тест: Успешное создание питомца** - Отправляется POST-запрос с корректными данными для создания питомца. - **Ожидаемый результат:** HTTP 200 и возвращаемое поле `name` совпадает с переданным значением. - -4. **Тест: Ошибка при создании питомца без поля "name"** - Отправляется POST-запрос, в котором отсутствует обязательное поле `name`. - **Ожидаемый результат:** API возвращает ошибку (код 400 или 405). - **PS.** Почему-то сервис отдает ответ 200 я решил оставить данный тест. - Показать что вся цепочка тестов не останавливается и продолжает работать дальше - -5. **Тест: Ошибка при передаче некорректного JSON** - Отправляется POST-запрос с ошибочным JSON (без закрывающей фигурной скобки). - **Ожидаемый результат:** API возвращает ошибку (код 400 или 405). - -## Как запустить тесты - -1. **Клонируйте репозиторий:** - ```bash - git clone https://git.kovbasa.ru/otus-autotests/hw3.git - -2. **Перейдите в папку проекта:** - - ```bash - cd hw3 - -3. **Запустите тесты командой:** - ```bash - mvn test - -После выполнения тестов в консоли вы увидите нумерованные сообщения с результатами (например, "1. Test: Find pets by -'available' status - Passed"). - -## Зависимости -- **Rest-assured**: 4.5.1 - -- **JUnit Jupiter (API & Engine)**: 5.9.1 - -## Лицензия - -Проект предназначен для образовательных целей. - ``` - --- - Файл REDME.md содержит краткое описание проекта, объясняет суть реализации тестов с Rest-assured, описывает - основные сценарии тестирования, структуру проекта и шаги для запуска автотестов. Вы можете его адаптировать под - свои нужды или дополнить дополнительной информацией. - ``` \ No newline at end of file +Пример: +```bash +mvn "-Dbase.uri=https://petstore.swagger.io" "-Dbase.path=/v2" test +``` diff --git a/checkstyle.xml b/checkstyle.xml new file mode 100644 index 0000000..4306867 --- /dev/null +++ b/checkstyle.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml index 48ef708..f25e8a7 100644 --- a/pom.xml +++ b/pom.xml @@ -1,37 +1,38 @@ - - + 4.0.0 - ru.otus - petstore + hw3 1.0-SNAPSHOT jar - petstore + hw3 - - - 24 - 24 + 21 UTF-8 - - 4.5.1 - 5.9.1 + https://petstore.swagger.io + /v2 + + 6.0.0 + 6.0.2 + 2.21.0 + + 3.15.0 + 3.5.4 + 3.6.0 + 4.9.8.2 + 4.9.8 - - io.rest-assured rest-assured ${restassured.version} - test commons-codec @@ -40,56 +41,111 @@ - commons-codec commons-codec - 1.15 - test + 1.21.0 - io.rest-assured json-path ${restassured.version} + + + io.rest-assured + json-schema-validator + ${restassured.version} test + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + - org.junit.jupiter junit-jupiter-api ${junit.jupiter.version} test - - org.junit.jupiter junit-jupiter-engine ${junit.jupiter.version} test + - - + org.apache.maven.plugins maven-compiler-plugin - 3.11.0 + ${maven.compiler.version} - ${maven.compiler.source} - ${maven.compiler.target} + ${java.version} + ${project.build.sourceEncoding} - + org.apache.maven.plugins maven-surefire-plugin - 2.22.2 + ${surefire.version} + + false + + ${base.uri} + ${base.path} + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + ${checkstyle.plugin.version} + + + checkstyle-validation + verify + + check + + + + + checkstyle.xml + true + true + + + + + com.github.spotbugs + spotbugs-maven-plugin + ${spotbugs.plugin.version} + + + com.github.spotbugs + spotbugs + ${spotbugs.version} + + + + + verify + + check + + + + + spotbugs-exclude.xml + diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml new file mode 100644 index 0000000..72806a0 --- /dev/null +++ b/spotbugs-exclude.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/main/java/ru/otus/petstore/config/TestConfig.java b/src/main/java/ru/otus/petstore/config/TestConfig.java new file mode 100644 index 0000000..fed7d62 --- /dev/null +++ b/src/main/java/ru/otus/petstore/config/TestConfig.java @@ -0,0 +1,21 @@ +package ru.otus.petstore.config; + +public final class TestConfig { + + private static final String BASE_URI = + System.getProperty("base.uri", "https://petstore.swagger.io"); + + private static final String BASE_PATH = + System.getProperty("base.path", "/v2"); + + private TestConfig() { + } + + public static String getBaseUri() { + return BASE_URI; + } + + public static String getBasePath() { + return BASE_PATH; + } +} diff --git a/src/main/java/ru/otus/petstore/model/Category.java b/src/main/java/ru/otus/petstore/model/Category.java new file mode 100644 index 0000000..2faf47e --- /dev/null +++ b/src/main/java/ru/otus/petstore/model/Category.java @@ -0,0 +1,23 @@ +// Category.java +package ru.otus.petstore.model; + +public class Category { + private long id; + private String name; + + public Category() { } + + public Category(long id, String name) { + this.id = id; + this.name = name; + } + + public Category(Category other) { + this.id = other.id; + this.name = other.name; + } + public long getId() { return id; } + public void setId(long id) { this.id = id; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } +} diff --git a/src/main/java/ru/otus/petstore/model/Pet.java b/src/main/java/ru/otus/petstore/model/Pet.java new file mode 100644 index 0000000..44e7eba --- /dev/null +++ b/src/main/java/ru/otus/petstore/model/Pet.java @@ -0,0 +1,58 @@ +// Pet.java +package ru.otus.petstore.model; + +import java.util.ArrayList; +import java.util.List; + +public class Pet { + private long id; + private Category category; + private String name; + private List photoUrls; + private List tags; + private String status; + + public Pet() { } + + public Pet(long id, Category category, String name, + List photoUrls, List tags, String status) { + this.id = id; + this.category = copyCategory(category); + this.name = name; + this.photoUrls = copyPhotoUrls(photoUrls); + this.tags = copyTags(tags); + this.status = status; + } + + public long getId() { return id; } + public void setId(long id) { this.id = id; } + public Category getCategory() { return copyCategory(category); } + public void setCategory(Category category) { this.category = copyCategory(category); } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public List getPhotoUrls() { return copyPhotoUrls(photoUrls); } + public void setPhotoUrls(List photoUrls) { this.photoUrls = copyPhotoUrls(photoUrls); } + public List getTags() { return copyTags(tags); } + public void setTags(List tags) { this.tags = copyTags(tags); } + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + + private static Category copyCategory(Category source) { + return source == null ? null : new Category(source); + } + + private static List copyPhotoUrls(List source) { + return source == null ? null : List.copyOf(source); + } + + private static List copyTags(List source) { + if (source == null) { + return null; + } + List target = new ArrayList<>(source.size()); + for (Tag tag : source) { + target.add(new Tag(tag)); + } + return target; + } +} diff --git a/src/main/java/ru/otus/petstore/model/Tag.java b/src/main/java/ru/otus/petstore/model/Tag.java new file mode 100644 index 0000000..6a6c688 --- /dev/null +++ b/src/main/java/ru/otus/petstore/model/Tag.java @@ -0,0 +1,23 @@ +// Tag.java +package ru.otus.petstore.model; + +public class Tag { + private long id; + private String name; + + public Tag() { } + + public Tag(long id, String name) { + this.id = id; + this.name = name; + } + + public Tag(Tag other) { + this.id = other.id; + this.name = other.name; + } + public long getId() { return id; } + public void setId(long id) { this.id = id; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } +} diff --git a/src/main/java/ru/otus/petstore/service/BaseService.java b/src/main/java/ru/otus/petstore/service/BaseService.java new file mode 100644 index 0000000..6250bd2 --- /dev/null +++ b/src/main/java/ru/otus/petstore/service/BaseService.java @@ -0,0 +1,36 @@ +package ru.otus.petstore.service; + +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.builder.ResponseSpecBuilder; +import io.restassured.filter.log.LogDetail; +import io.restassured.http.ContentType; +import io.restassured.specification.RequestSpecification; +import io.restassured.specification.ResponseSpecification; +import ru.otus.petstore.config.TestConfig; + +import static io.restassured.RestAssured.given; + +public abstract class BaseService { + + protected final RequestSpecification requestSpec = new RequestSpecBuilder() + .setBaseUri(TestConfig.getBaseUri()) + .setBasePath(TestConfig.getBasePath()) + .setContentType(ContentType.JSON) + .log(LogDetail.URI) + .log(LogDetail.METHOD) + .log(LogDetail.PARAMS) + .log(LogDetail.BODY) + .build(); + + protected final ResponseSpecification okSpec = new ResponseSpecBuilder() + .expectStatusCode(200) + .build(); + + protected final ResponseSpecification notFoundSpec = new ResponseSpecBuilder() + .expectStatusCode(404) + .build(); + + protected RequestSpecification givenBase() { + return given().spec(requestSpec); + } +} diff --git a/src/main/java/ru/otus/petstore/service/PetService.java b/src/main/java/ru/otus/petstore/service/PetService.java new file mode 100644 index 0000000..ac19e72 --- /dev/null +++ b/src/main/java/ru/otus/petstore/service/PetService.java @@ -0,0 +1,72 @@ +package ru.otus.petstore.service; + +import io.restassured.response.Response; +import ru.otus.petstore.model.Pet; + +public class PetService extends BaseService { + + public Response findByStatus(String status) { + return givenBase() + .queryParam("status", status) + .when() + .get("/pet/findByStatus"); + } + + public Response createPet(Pet pet) { + return givenBase() + .body(pet) + .when() + .post("/pet"); + } + + public Response createPetExpectSuccess(Pet pet) { + return createPet(pet) + .then() + .spec(okSpec) + .extract() + .response(); + } + + public Response createPetRaw(String rawJson) { + return givenBase() + .body(rawJson) + .when() + .post("/pet"); + } + + public Response getPetById(long id) { + return givenBase() + .when() + .get("/pet/{id}", id); + } + + public Response getPetByIdExpectSuccess(long id) { + return getPetById(id) + .then() + .spec(okSpec) + .extract() + .response(); + } + + public Response getPetByIdExpectNotFound(long id) { + return getPetById(id) + .then() + .spec(notFoundSpec) + .extract() + .response(); + } + + public Response deletePet(long id) { + return givenBase() + .when() + .delete("/pet/{id}", id); + } + + public void deletePetIfExists(long id) { + Response response = deletePet(id); + int statusCode = response.statusCode(); + if (statusCode != 200 && statusCode != 404) { + throw new IllegalStateException("Unexpected status for DELETE /pet/{id}: " + statusCode); + } + } +} diff --git a/src/test/java/ru/otus/petstore/PetStoreTests.java b/src/test/java/ru/otus/petstore/PetStoreTests.java deleted file mode 100644 index a429a34..0000000 --- a/src/test/java/ru/otus/petstore/PetStoreTests.java +++ /dev/null @@ -1,174 +0,0 @@ -package ru.otus.petstore; - -import io.restassured.RestAssured; -import io.restassured.http.ContentType; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestMethodOrder; -import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.extension.TestWatcher; -import org.junit.jupiter.api.extension.ExtensionContext; - -import java.util.Optional; - -import static io.restassured.RestAssured.given; -import static org.hamcrest.Matchers.*; - -@ExtendWith(PetStoreTests.TestResultLogger.class) -@TestMethodOrder(OrderAnnotation.class) -public class PetStoreTests { - - /** - * Выполняется один раз перед запуском всех тестов. - * Устанавливаем базовый URI для запросов к Swagger Petstore API. - */ - @BeforeAll - public static void setup() { - RestAssured.baseURI = "https://petstore.swagger.io/v2"; - } - - /** - * Тест 1: GET /pet/findByStatus (status = available). - * Ожидается статус 200 и все объекты в ответе должны иметь поле "status" равное "available". - */ - @Test - @Order(1) - @DisplayName("1. Test: Find pets by 'available' status") - public void testFindPetsByAvailableStatus() { - given() - .queryParam("status", "available") - .when() - .get("/pet/findByStatus") - .then() - .statusCode(200) - .body("status", everyItem(equalTo("available"))); - } - - /** - * Тест 2: GET /pet/findByStatus с несуществующим статусом "invalidStatus". - * Ожидается статус 200 и пустой массив в ответе. - */ - @Test - @Order(2) - @DisplayName("2. Test: Find pets by invalid status") - public void testFindPetsByInvalidStatus() { - given() - .queryParam("status", "invalidStatus") - .when() - .get("/pet/findByStatus") - .then() - .statusCode(200) - .body("", hasSize(0)); - } - - /** - * Тест 3: POST /pet для создания питомца с валидными данными. - * Ожидается статус 200 и возвращаемое поле "name" совпадает с переданным. - */ - @Test - @Order(3) - @DisplayName("3. Test: Create pet successfully") - public void testCreatePetSuccess() { - int petId = (int) (System.currentTimeMillis() % Integer.MAX_VALUE); - String petName = "Rex"; - String requestBody = "{\n" + - " \"id\": " + petId + ",\n" + - " \"category\": { \"id\": 1, \"name\": \"Dogs\" },\n" + - " \"name\": \"" + petName + "\",\n" + - " \"photoUrls\": [ \"http://example.com/photo.jpg\" ],\n" + - " \"tags\": [ { \"id\": 0, \"name\": \"string\" } ],\n" + - " \"status\": \"available\"\n" + - "}"; - given() - .contentType(ContentType.JSON) - .body(requestBody) - .when() - .post("/pet") - .then() - .statusCode(200) - .body("name", equalTo(petName)); - } - - /** - * Тест 4: POST /pet для создания питомца с невалидными данными (без поля "name"). - * Ожидается, что API вернёт ошибку (400 или 405). Почему-то сервис отдает ответ 200 - * я решил оставить данный тест. Показать что вся цепочка тестов не останавливается и - * продолжает работать дальше - */ - @Test - @Order(4) - @DisplayName("4. Test: Fail to create pet without 'name' field") - public void testCreatePetMissingName() { - int petId = (int) (System.currentTimeMillis() % Integer.MAX_VALUE); - String requestBody = "{\n" + - " \"id\": " + petId + ",\n" + - " \"category\": { \"id\": 1, \"name\": \"Dogs\" },\n" + - " \"photoUrls\": [ \"http://example.com/photo.jpg\" ],\n" + - " \"tags\": [ { \"id\": 0, \"name\": \"string\" } ],\n" + - " \"status\": \"available\"\n" + - "}"; - given() - .contentType(ContentType.JSON) - .body(requestBody) - .when() - .post("/pet") - .then() - .statusCode(anyOf(is(400), is(405))); - } - - /** - * Тест 5: POST /pet с некорректным JSON (отсутствует закрывающая фигурная скобка). - * Ожидается, что API вернёт ошибку (400 или 405). - */ - @Test - @Order(5) - @DisplayName("5. Test: Create pet with invalid JSON") - public void testCreatePetInvalidJson() { - String invalidJson = """ - { - "id": 123456, - "name": "Invalid Pet", - "category": { "id": 1, "name": "Dogs" }, - "photoUrls": [ "http://example.com/photo.jpg" ], - "tags": [ { "id": 0, "name": "string" } ], - "status": "available" - """; // отсутствует закрывающая фигурная скобка - given() - .contentType(ContentType.JSON) - .body(invalidJson) - .when() - .post("/pet") - .then() - .statusCode(anyOf(is(400), is(405))); - } - - /** - * Реализация TestWatcher для логирования результата каждого теста. - * Выводит сообщения: если тест пройден – "Test Passed", иначе "Test Failed", "Test Aborted" или "Test Disabled". - */ - public static class TestResultLogger implements TestWatcher { - - @Override - public void testSuccessful(ExtensionContext context) { - System.out.println(context.getDisplayName() + " - Passed"); - } - - @Override - public void testFailed(ExtensionContext context, Throwable cause) { - System.out.println(context.getDisplayName() + " - Failed: " + cause.getMessage()); - } - - @Override - public void testAborted(ExtensionContext context, Throwable cause) { - System.out.println(context.getDisplayName() + " - Aborted"); - } - - @Override - public void testDisabled(ExtensionContext context, Optional reason) { - System.out.println(context.getDisplayName() + " - Disabled: " + reason.orElse("No reason provided")); - } - } -} diff --git a/src/test/java/ru/otus/petstore/tests/PetNegativeScenariosTest.java b/src/test/java/ru/otus/petstore/tests/PetNegativeScenariosTest.java new file mode 100644 index 0000000..376b8b4 --- /dev/null +++ b/src/test/java/ru/otus/petstore/tests/PetNegativeScenariosTest.java @@ -0,0 +1,69 @@ +package ru.otus.petstore.tests; + +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import ru.otus.petstore.model.Pet; +import ru.otus.petstore.service.PetService; +import ru.otus.petstore.util.PetFactory; + +public class PetNegativeScenariosTest { + + private final PetService petService = new PetService(); + + /** + * Проверяем POST /pet с некорректным JSON: + * - API не принимает сломанное тело запроса; + * - возвращается клиентская/серверная ошибка вместо 200. + */ + @Test + @DisplayName("POST /pet with malformed JSON returns error status") + void createPetWithMalformedJsonReturnsError() { + String brokenJson = """ + { + "id": 123456, + "name": "Broken pet", + "photoUrls": ["https://petstore.test/photo.jpg"] + """; + + int status = petService.createPetRaw(brokenJson).statusCode(); + MatcherAssert.assertThat(status, Matchers.anyOf(Matchers.is(400), Matchers.is(405), Matchers.is(500))); + } + + /** + * Проверяем GET /pet/{id} для несуществующего ресурса: + * - сервер возвращает 404; + * - сообщение об ошибке соответствует "Pet not found". + */ + @Test + @DisplayName("GET /pet/{id} returns 404 for unknown id") + void getMissingPetReturnsNotFound() { + long unknownId = PetFactory.nextId(); + Pet createdPet = PetFactory.newAvailablePet(unknownId); + + // Удаляем id на старте теста, чтобы гарантировать отсутствие сущности. + petService.deletePetIfExists(unknownId); + + // Создаем и сразу удаляем питомца, чтобы не пересекаться с возможными данными окружения. + petService.createPetExpectSuccess(createdPet); + petService.deletePetIfExists(unknownId); + + var response = petService.getPetByIdExpectNotFound(unknownId); + MatcherAssert.assertThat(response.path("message"), Matchers.equalTo("Pet not found")); + } + + /** + * Проверяем GET /pet/findByStatus с невалидным фильтром: + * - API возвращает успешный код; + * - выдача пустая. + */ + @Test + @DisplayName("GET /pet/findByStatus with unknown status returns empty list") + void findByInvalidStatusReturnsEmpty() { + petService.findByStatus("invalidStatus") + .then() + .statusCode(200) + .body("$", Matchers.empty()); + } +} diff --git a/src/test/java/ru/otus/petstore/tests/PetPositiveScenariosTest.java b/src/test/java/ru/otus/petstore/tests/PetPositiveScenariosTest.java new file mode 100644 index 0000000..bea19cc --- /dev/null +++ b/src/test/java/ru/otus/petstore/tests/PetPositiveScenariosTest.java @@ -0,0 +1,84 @@ +package ru.otus.petstore.tests; + +import io.restassured.response.Response; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import ru.otus.petstore.model.Pet; +import ru.otus.petstore.service.PetService; +import ru.otus.petstore.util.PetFactory; + +import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +public class PetPositiveScenariosTest { + + private final PetService petService = new PetService(); + + /** + * Проверяем сценарий POST /pet + GET /pet/{id}: + * - питомец создается успешно; + * - по id возвращается именно созданный объект. + */ + @Test + @DisplayName("POST /pet creates resource and GET /pet/{id} returns created pet") + void createPetAndGetById() { + long petId = PetFactory.nextId(); + Pet pet = PetFactory.newAvailablePet(petId); + + try { + Response createResponse = petService.createPetExpectSuccess(pet); + assertThat(createResponse.path("id"), equalTo((int) petId)); + assertThat(createResponse.path("name"), equalTo(pet.getName())); + + Response getResponse = retryGetPetById(petService, petId); + assertThat(getResponse.path("id"), equalTo((int) petId)); + assertThat(getResponse.path("name"), equalTo(pet.getName())); + } finally { + petService.deletePetIfExists(petId); + } + } + + /** + * Проверяем GET /pet/{id}: + * - успешный ответ для существующего питомца; + * - тело ответа соответствует JSON-схеме Pet. + */ + @Test + @DisplayName("GET /pet/{id} returns valid payload by schema") + void getPetByIdMatchesSchema() { + long petId = PetFactory.nextId(); + Pet pet = PetFactory.newAvailablePet(petId); + + try { + petService.createPetExpectSuccess(pet); + + petService.getPetByIdExpectSuccess(petId) + .then() + .body(matchesJsonSchemaInClasspath("schemas/pet-schema.json")); + } finally { + petService.deletePetIfExists(petId); + } + } + + private Response retryGetPetById(PetService petService, long petId) { + int maxAttempts = 5; + for (int i = 0; i < maxAttempts; i++) { + Response response = petService.getPetById(petId); + if (response.statusCode() == 200) { + return response; + } + sleep(300L); + } + return petService.getPetByIdExpectSuccess(petId); + } + + private void sleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException interruptedException) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Sleep interrupted", interruptedException); + } + } +} diff --git a/src/test/java/ru/otus/petstore/util/PetFactory.java b/src/test/java/ru/otus/petstore/util/PetFactory.java new file mode 100644 index 0000000..bfef256 --- /dev/null +++ b/src/test/java/ru/otus/petstore/util/PetFactory.java @@ -0,0 +1,30 @@ +package ru.otus.petstore.util; + +import ru.otus.petstore.model.Category; +import ru.otus.petstore.model.Pet; +import ru.otus.petstore.model.Tag; + +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +public final class PetFactory { + private static final String TEST_PHOTO_URL = "https://petstore.test/photo.jpg"; + + private PetFactory() { + } + + public static long nextId() { + return ThreadLocalRandom.current().nextLong(1_000_000L, Integer.MAX_VALUE); + } + + public static Pet newAvailablePet(long id) { + return new Pet( + id, + new Category(1L, "Dogs"), + "Rex-" + id, + List.of(TEST_PHOTO_URL), + List.of(new Tag(1L, "api-test")), + "available" + ); + } +} diff --git a/src/test/resources/schemas/pet-schema.json b/src/test/resources/schemas/pet-schema.json new file mode 100644 index 0000000..41a4774 --- /dev/null +++ b/src/test/resources/schemas/pet-schema.json @@ -0,0 +1,52 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "required": [ + "id", + "name", + "photoUrls" + ], + "properties": { + "id": { + "type": "integer" + }, + "category": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "name": { + "type": "string" + }, + "photoUrls": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "tags": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + } + }, + "status": { + "type": "string" + } + } +}