diff --git a/REPORT.md b/REPORT.md
new file mode 100644
index 0000000..b6f58cd
--- /dev/null
+++ b/REPORT.md
@@ -0,0 +1,124 @@
+# REPORT
+
+## 1. Какой API выбран и почему
+
+Я выбрал **GitHub REST API** (`https://api.github.com`), потому что этот API нужен мне для курсового проекта. Подробнее я описал в `api-notes.md`
+
+Основной стек для реализации: REST Assured + JUnit 5 + AssertJ.
+
+---
+
+## 2. Покрытые сценарии
+
+| Категория | Сценарий | Реализация |
+|-----------|----------|------------|
+| Happy path | Получение существующего пользователя `/users/octocat` | Реализовано |
+| Happy path | Получение существующего репозитория `/repos/{owner}/{repo}` | Реализовано |
+| Контракт ответа | Проверка обязательных полей и типов у профиля пользователя | Реализовано |
+| Контракт ответа | Проверка полей репозитория (`id`, `name`, `full_name`, `owner.login`, `private`, `html_url`) | Реализовано |
+| Границы/особенности | Проверка логина без учёта регистра (`/users/OCTOCAT`) | Реализовано |
+| Параметры запроса | Проверка `per_page=2` и пагинации для `/users/{username}/repos` | Реализовано |
+| Контракт ответа | Проверка заголовков `Cache-Control`, `ETag`, `X-GitHub-Api-Version-Selected`, `X-RateLimit-Limit` | Реализовано |
+| Happy path / контракт | Проверка структуры `/repos/{owner}/{repo}/languages` | Реализовано |
+| Ошибки клиента | Несуществующий пользователь возвращает `404` и тело ошибки | Реализовано |
+| Ошибки клиента | Несуществующий репозиторий возвращает `404` и тело ошибки | Реализовано |
+| Перформанс | Базовый запрос к пользователю укладывается в порог времени | Реализовано |
+
+Итого в проекте:
+
+- **9 тестовых методов**
+- **11 выполнений тестов** с учётом 3 запусков параметризованного сценария
+- покрыто **4 эндпоинта**:
+ - `GET /users/{username}`
+ - `GET /users/{username}/repos`
+ - `GET /repos/{owner}/{repo}`
+ - `GET /repos/{owner}/{repo}/languages`
+- есть **параметризованный тест**
+- есть **негативные сценарии** на `404`
+
+---
+
+## 3. Что удалось узнать об API
+
+### 3.1. Полезные особенности GitHub API
+
+- Для большинства запросов рекомендуется передавать:
+ - `Accept: application/vnd.github+json`
+ - `User-Agent`
+ - `X-GitHub-Api-Version`
+- Публичные данные можно читать **без аутентификации**.
+- Для неаутентифицированных запросов действует лимит **60 запросов в час**.
+- Для search-эндпоинтов лимиты строже, поэтому их лучше не перегружать тестами.
+
+### 3.2. Неожиданные или важные наблюдения
+
+- `username` на практике обрабатывается **без учёта регистра**: `/users/OCTOCAT` возвращает `octocat`.
+- Ошибка `404` имеет достаточно стабильный формат:
+
+```json
+{
+ "message": "Not Found",
+ "documentation_url": "https://docs.github.com/rest",
+ "status": "404"
+}
+```
+
+- Ответы содержат полезные служебные заголовки:
+ - `ETag`
+ - `Cache-Control`
+ - `X-RateLimit-*`
+ - `X-GitHub-Api-Version-Selected`
+- Эндпоинт `/languages` возвращает не массив, а JSON-чик, где ключ - язык, значение - число байтов.
+- У некоторых репозиториев `/languages` может вернуть пустой объект `{}`.
+
+### 3.3. Неоднозначности документации
+
+- Документация GitHub очень подробная, но она огромная, и в ней легко потеряться.
+- На практике некоторые поведенческие детали удобнее подтвердить экспериментом через `curl`, чем искать в справке, например:
+ - чувствительность к регистру,
+ - реальный формат ошибки,
+ - конкретные заголовки кэширования и rate limit.
+
+---
+
+## 4. Инструмент: что использовал, что понравилось и не понравилось
+
+### Использованный инструмент
+
+Я использовал:
+
+- **REST Assured** - для HTTP-запросов и извлечения ответа
+- **JUnit 5** - для организации тестов
+- **AssertJ** - для читаемых и гибких проверок
+
+### Что понравилось
+
+- У REST Assured короткий и понятный DSL для API-тестов.
+- Удобно задавать базовые заголовки через `RequestSpecification`.
+- AssertJ делает проверки читаемыми: легко сравнивать поля, коллекции, заголовки и типы значений.
+- `@ParameterizedTest` хорошо подходит для проверки нескольких репозиториев без дублирования кода.
+
+### Что не понравилось / ограничения
+
+- Тесты к публичному интернет-API всегда немного более хрупкие, чем тесты к локально замоканному сервису:
+ - возможны сетевые задержки,
+ - меняются реальные данные,
+ - есть rate limit
+
+---
+
+## 5. Запуск
+
+Команда для прогона всех тестов:
+
+```bash
+mvn test
+```
+
+Или вот так лучше будет:
+
+```bash
+mvn -Dtest=GitHubApiTest test
+```
+
+Но это надо как-то обойти, что в репозитории есть файлы, которые не компилируются, поэтому проще ручками сходить в директорию с тестом и из IDE его запустить
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index de78097..216c7d0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -79,6 +79,13 @@
jackson-databind
2.17.2
+
+
+ io.rest-assured
+ rest-assured
+ 5.5.0
+ test
+
com.google.code.gson
@@ -111,4 +118,4 @@
-
+
\ No newline at end of file
diff --git a/src/test/java/hse/java/githubapi/GitHubApiTest.java b/src/test/java/hse/java/githubapi/GitHubApiTest.java
new file mode 100644
index 0000000..fc621ad
--- /dev/null
+++ b/src/test/java/hse/java/githubapi/GitHubApiTest.java
@@ -0,0 +1,184 @@
+package hse.java.githubapi;
+
+import io.restassured.RestAssured;
+import io.restassured.builder.RequestSpecBuilder;
+import io.restassured.path.json.JsonPath;
+import io.restassured.response.Response;
+import io.restassured.specification.RequestSpecification;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.time.Instant;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+
+import static io.restassured.RestAssured.given;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+
+class GitHubApiTest {
+
+ private static final String BASE_URI = "https://api.github.com";
+ private static final String API_VERSION = "2026-03-10";
+ private static RequestSpecification requestSpecification;
+
+ @BeforeAll
+ static void setUp() {
+ RestAssured.baseURI = BASE_URI;
+ requestSpecification = new RequestSpecBuilder()
+ .setBaseUri(BASE_URI)
+ .addHeader("Accept", "application/vnd.github+json")
+ .addHeader("User-Agent", "course-project-github-api-tests")
+ .addHeader("X-GitHub-Api-Version", API_VERSION)
+ .build();
+ }
+
+ @ParameterizedTest(name = "GET /repos/{0}/{1} возвращает публичный репозиторий")
+ @MethodSource("knownRepositories")
+ void knownRepositoriesShouldBeAvailable(String owner, String repo) {
+ Response response = get("/repos/{owner}/{repo}", owner, repo);
+ JsonPath json = response.jsonPath();
+
+ assertThat(response.statusCode()).isEqualTo(200);
+ assertThat(json.getInt("id")).isPositive();
+ assertThat(json.getString("name")).isEqualTo(repo);
+ assertThat(json.getString("owner.login")).isEqualToIgnoringCase(owner);
+ assertThat(json.getString("full_name")).isEqualTo(owner + "/" + repo);
+ assertThat(json.getBoolean("private")).isFalse();
+ assertThat(json.getString("html_url")).isEqualTo("https://github.com/" + owner + "/" + repo);
+ }
+
+ @Test
+ void getUserShouldReturnRequiredPublicProfileFields() {
+ Response response = get("/users/{username}", "octocat");
+ JsonPath json = response.jsonPath();
+
+ assertThat(response.statusCode()).isEqualTo(200);
+ assertThat(json.getString("login")).isEqualTo("octocat");
+ assertThat(json.getInt("id")).isPositive();
+ assertThat(json.getString("type")).isEqualTo("User");
+ assertThat(json.getBoolean("site_admin")).isFalse();
+ assertThat(json.getString("html_url")).isEqualTo("https://github.com/octocat");
+ assertThat(json.getString("followers_url")).contains("/users/octocat/followers");
+ assertThat(json.getInt("public_repos")).isGreaterThanOrEqualTo(0);
+ assertThatCode(() -> Instant.parse(json.getString("created_at"))).doesNotThrowAnyException();
+ assertThatCode(() -> Instant.parse(json.getString("updated_at"))).doesNotThrowAnyException();
+ }
+
+ @Test
+ @DisplayName("Логин пользователя обрабатывается без учёта регистра")
+ void getUserShouldBeCaseInsensitiveForLogin() {
+ Response response = get("/users/{username}", "OCTOCAT");
+ JsonPath json = response.jsonPath();
+
+ assertThat(response.statusCode()).isEqualTo(200);
+ assertThat(json.getString("login")).isEqualTo("octocat");
+ assertThat(json.getString("html_url")).isEqualTo("https://github.com/octocat");
+ }
+
+ @Test
+ void listUserRepositoriesShouldHonorPerPageParameter() {
+ Response response = given()
+ .spec(requestSpecification)
+ .queryParam("per_page", 2)
+ .queryParam("sort", "updated")
+ .when()
+ .get("/users/{username}/repos", "octocat");
+
+ List