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"
+ }
+ }
+}