# Проверка системы: Swagger UI, curl и проверочный скрипт на Python<no value>

## Что мы изучим?

Сервер, который не проверен, — сервер, в работе которого нельзя быть уверенным. Мы изучим три способа — от ручного к автоматическому:

| Инструмент | Когда |
|---|---|
| **Swagger UI** (`/docs`) | Удобная ручная проверка во время разработки |
| **curl** | Быстрая проверка из терминала и привычка людей бэкенда и DevOps |
| **Скрипт на Python** | Серия проверок, которая запускается одним щелчком и проверяет **всё**, каждый раз |

И **план проверок** (Test Plan): что вы проверяете и что ожидаете получить.

## Зачем это нужно?

Каждое изменение в коде может сломать то, что работало. Если проверка — «я открыл браузер, и вроде всё нормально», баги дойдут до пользователей. Фиксированный список проверок и особенно скрипт, который его выполняет, позволяют менять код **уверенно**. Это ещё и тренировка для интеграционного проекта, где целый день посвящён отладке и ручной проверке всех действий CRUD.

## Объяснение с основ

### Swagger UI

**Swagger UI** — страница, которую FastAPI **строит автоматически** по коду: по маршрутам, подсказкам типов и моделям Pydantic. Она показывает каждый Endpoint и позволяет отправлять настоящие запросы из браузера.

```text
http://127.0.0.1:8000/docs
```

**Процесс для каждого Endpoint-а:**

1. Щелчок по Endpoint-у (например, `POST /students`) открывает его.
2. **Try it out** → поля открываются для редактирования.
3. Заполните параметры и/или отредактируйте **Request body** (там уже есть пример JSON, построенный по модели!).
4. **Execute** → на сервер отправляется настоящий запрос.
5. Ниже: **Curl** (равносильная команда!), **Request URL**, **Response body**, **Response code** и **Response headers**.

**Бонус:** есть ещё `http://127.0.0.1:8000/openapi.json`. Это **описание** API в стандартном формате (OpenAPI), по которому Swagger строит страницу. Другие инструменты тоже умеют его читать.

### curl — напоминание

```bash
curl -i http://127.0.0.1:8000/students
curl -i -X POST http://127.0.0.1:8000/students -H "Content-Type: application/json" -d '{"name": "Tom", "age": 27, "email": "tom@mail.com"}'
curl -i -X DELETE http://127.0.0.1:8000/students/3
```

`-i` показывает ещё и строку состояния — именно её вы и проверяете.

{{< box "tip" "curl с JSON в PowerShell" >}}
В PowerShell кавычки внутри JSON — сложная история. Два удобных способа: (1) сохраните JSON в файл (`new_student.json`) и отправьте через `curl.exe -i -X POST URL -H "Content-Type: application/json" -d "@new_student.json"`. (2) Скопируйте команду curl, которую **показывает Swagger**, и выполните её в Git Bash.
{{< /box >}}

### Проверочный скрипт на Python

Скрипт, который отправляет запросы на **работающий** сервер и для каждого проверяет: **такой ли код, какой мы ожидали?** В конце он печатает, сколько проверок прошло. Такая проверка, которая запускает систему снаружи, как настоящий клиент, называется **сквозной** (End-to-End). (Позже в треке вы изучите профессиональные инструменты тестирования, например pytest. Идея та же.)

### План проверок — список проверок

Для каждого Endpoint-а: **«счастливый путь»** + **каждый** случай сбоя из таблицы соответствий. Если у каждой строки таблицы есть проверка, вы знаете, что контракт API соблюдается.

## Синтаксис

```python
import json
import urllib.error
import urllib.request

BASE_URL = "http://127.0.0.1:8000"

def call(method, path, body=None):
    data = json.dumps(body).encode() if body is not None else None
    request = urllib.request.Request(BASE_URL + path, data=data, method=method,
                                     headers={"Content-Type": "application/json"})
    try:
        response = urllib.request.urlopen(request)
        status, raw = response.status, response.read()
    except urllib.error.HTTPError as error:
        status, raw = error.code, error.read()
    return status, (json.loads(raw) if raw else None)
```

`call` возвращает **tuple** из (кода, тела ответа) — и при успехе, и при ошибке (`HTTPError`). Пустое тело (204) → `None`.

## Первый пример кода

**Swagger — создание студента шаг за шагом:**

1. Запустите сервер (`uvicorn main:app --reload`) и откройте `/docs`.
2. **POST /students** → **Try it out**. Request body показывает:

```json
{
  "name": "string",
  "age": 0,
  "email": "string"
}
```

3. Замените на:

```json
{
  "name": "Tom",
  "age": 27,
  "email": "tom@mail.com"
}
```

4. **Execute**. Результат:

```text
Code: 201
Response body:
{
  "id": 3,
  "name": "Tom",
  "age": 27,
  "email": "tom@mail.com"
}
```

5. Нажмите **Execute** ещё раз с тем же телом. Теперь: **409**, `"a student with email tom@mail.com already exists"`.

## Разбор кода построчно

| Что вы видите в Swagger | Откуда это берётся в коде |
|---|---|
| Список всех Endpoint-ов | Декораторы `@app.get/post/put/delete` |
| Имя API вверху («Students API») | `FastAPI(title="Students API")` |
| Пример JSON в теле | Модель `StudentIn` (поля и типы) |
| Поле `student_id` с `integer` | Подсказка типа `student_id: int` |
| Код 201 как «Successful Response» в POST | `status_code=201` |
| id 3 в ответе | `db.create_student` → AUTO_INCREMENT |

**Всё выводится из кода.** Когда вы меняете маршрут, документация обновляется сама. Поэтому она всегда точна.

## Ещё один пример

**Сквозной проверочный скрипт.** `smoke_test.py` (сервер должен работать в другом терминале, на **пустой** таблице: в Database Client выполните `DROP TABLE students;` и перезапустите сервер — `init_db` создаст таблицу снова):

```python
import json
import urllib.error
import urllib.request

BASE_URL = "http://127.0.0.1:8000"

def call(method, path, body=None):
    data = json.dumps(body).encode() if body is not None else None
    request = urllib.request.Request(BASE_URL + path, data=data, method=method,
                                     headers={"Content-Type": "application/json"})
    try:
        response = urllib.request.urlopen(request)
        status, raw = response.status, response.read()
    except urllib.error.HTTPError as error:
        status, raw = error.code, error.read()
    return status, (json.loads(raw) if raw else None)

CHECKS = [
    ("create Dana", "POST", "/students", {"name": "Dana", "age": 24, "email": "dana@mail.com"}, 201),
    ("create Avi", "POST", "/students", {"name": "Avi", "age": 31, "email": "avi@mail.com"}, 201),
    ("duplicate email", "POST", "/students", {"name": "Dana Copy", "age": 20, "email": "dana@mail.com"}, 409),
    ("too young", "POST", "/students", {"name": "Kid", "age": 7, "email": "kid@mail.com"}, 400),
    ("missing fields", "POST", "/students", {"name": "Noa"}, 422),
    ("list", "GET", "/students", None, 200),
    ("get existing", "GET", "/students/1", None, 200),
    ("get missing", "GET", "/students/99", None, 404),
    ("bad id", "GET", "/students/abc", None, 422),
    ("update", "PUT", "/students/2", {"name": "Avi Levi", "age": 32, "email": "avi@mail.com"}, 200),
    ("update missing", "PUT", "/students/99", {"name": "Ghost", "age": 30, "email": "g@mail.com"}, 404),
    ("update clash", "PUT", "/students/2", {"name": "Avi", "age": 32, "email": "dana@mail.com"}, 409),
    ("delete", "DELETE", "/students/1", None, 204),
    ("delete again", "DELETE", "/students/1", None, 404),
]

def main():
    passed = 0
    for name, method, path, body, expected in CHECKS:
        status, data = call(method, path, body)
        ok = status == expected
        passed = passed + (1 if ok else 0)
        mark = "PASS" if ok else "FAIL"
        print(mark, "|", name, "|", method, path, "| expected", expected, "got", status)
    print(passed, "/", len(CHECKS), "checks passed")

if __name__ == "__main__":
    main()
```

**Вывод:**

```text
PASS | create Dana | POST /students | expected 201 got 201
PASS | create Avi | POST /students | expected 201 got 201
PASS | duplicate email | POST /students | expected 409 got 409
PASS | too young | POST /students | expected 400 got 400
PASS | missing fields | POST /students | expected 422 got 422
PASS | list | GET /students | expected 200 got 200
PASS | get existing | GET /students/1 | expected 200 got 200
PASS | get missing | GET /students/99 | expected 404 got 404
PASS | bad id | GET /students/abc | expected 422 got 422
PASS | update | PUT /students/2 | expected 200 got 200
PASS | update missing | PUT /students/99 | expected 404 got 404
PASS | update clash | PUT /students/2 | expected 409 got 409
PASS | delete | DELETE /students/1 | expected 204 got 204
PASS | delete again | DELETE /students/1 | expected 404 got 404
14 / 14 checks passed
```

**Разбор:**

- **`CHECKS`** — список tuple: имя проверки, метод, путь, тело и ожидаемый код. **Данные** отделены от **логики** (разделение ответственности!). Добавить проверку = добавить строку.
- **Порядок важен**: проверки зависят друг от друга (сначала создают, потом удаляют). Поэтому начинают с пустой базы.
- **Цикл** — отправляет, сравнивает с `expected` и печатает PASS/FAIL.
- **Магия:** вы меняете что-то в коде (рефакторинг), запускаете — и через секунду видите, не сломалось ли что-нибудь. **Страховочная сетка.**

**Попробуйте сломать:** в `main.py` поменяйте `status_code=204` на `200` в DELETE и сохраните (`--reload`). Удалите таблицу (`DROP TABLE students;` в Database Client), перезапустите и выполните скрипт:

```text
FAIL | delete | DELETE /students/1 | expected 204 got 200
13 / 14 checks passed
```

Скрипт **поймал** изменение. Верните код обратно.

## Типичная ошибка

**Ошибка 1: проверять только «счастливый путь».** «Я создал студента, и это сработало» ≠ «сервер работает». Большинство багов в серверном коде — в случаях сбоя: 404, который возвращается как 200, 409, который возвращается как 500. **План проверок = «счастливый путь» + каждая строка сбоя из таблицы соответствий.**

**Ошибка 2: запускать проверочный скрипт на базе с прежними данными.** «create Dana» упадёт с 409, потому что Дана уже есть с прошлого запуска. **Пустая база перед каждым запуском** (или, позже в треке, отдельная проверочная база).

**Ошибка 3: забыть, что сервер должен работать.**

```text
urllib.error.URLError: <urlopen error [WinError 10061] No connection could be made because the target machine actively refused it>
```

«Connection refused» = порт никто не слушает (вспомните тему сетей!). Запустите uvicorn в отдельном терминале.

**Ошибка 4: проверять только код, а не тело.** Код 200 с неправильным телом (имя не обновилось) — всё равно баг. В важных проверках проверяйте и значения: `data["name"] == "Avi Levi"`. (См. самостоятельную практику.)

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## Практика

Упражнения опираются только на то, что пройдено в этом уроке и в предыдущих. Сначала попробуйте сами, и только потом открывайте решение.

### Упражнение 1: План проверок (Test Plan)

Составьте план проверок для API студентов: не меньше 8 проверок с методом, путём, тем, что вы отправляете, и кодом, который ожидаете получить. Включите «счастливый путь» **и** случаи сбоя.

{{< solution >}}
| # | Проверка | Запрос | Ожидаемый код |
|---|---|---|---|
| 1 | Создание Даны | `POST /students` с допустимым телом | 201 |
| 2 | Дублирующийся email | `POST /students` с email Даны | 409 |
| 3 | Слишком молод | `POST /students` с возрастом 7 | 400 |
| 4 | Отсутствуют поля | `POST /students` только с именем | 422 |
| 5 | Существующий студент | `GET /students/1` | 200 |
| 6 | Несуществующий студент | `GET /students/99` | 404 |
| 7 | Нечисловой id | `GET /students/abc` | 422 |
| 8 | Удаление | `DELETE /students/1` | 204 |
| 9 | Повторное удаление | `DELETE /students/1` | 404 |

Первая проверка проходит всегда — **большинство багов в случаях сбоя**: 404, который возвращается как 200, или 409, который возвращается как 500.
{{< /solution >}}

### Упражнение 2: curl

Напишите команды curl для сервера, работающего на `http://127.0.0.1:8000`:

1. Создание студента (Dana, 24, dana@mail.com) — с показом кода.
2. Чтение студента 1.
3. Удаление студента 1 — с показом кода.

{{< solution >}}
```powershell
curl -i -X POST http://127.0.0.1:8000/students -H "Content-Type: application/json" -d '{"name": "Dana", "age": 24, "email": "dana@mail.com"}'
curl http://127.0.0.1:8000/students/1
curl -i -X DELETE http://127.0.0.1:8000/students/1
```

`-i` показывает код состояния — при проверке это самое важное. В Swagger (`/docs`) те же запросы можно выполнить из браузера.
{{< /solution >}}

### Упражнение 3: Функция call

Напишите функцию `call(method, path, body=None)`, которая отправляет запрос на сервер с помощью `urllib.request` и возвращает tuple из (кода, тела в виде словаря или `None`). Ошибки HTTP (4xx, 5xx) не должны приводить к падению — они тоже возвращают код.

{{< solution >}}
```py
import json
import urllib.error
import urllib.request

BASE_URL = "http://127.0.0.1:8000"


def call(method, path, body=None):
    data = None
    if body is not None:
        data = json.dumps(body).encode()
    request = urllib.request.Request(BASE_URL + path, data=data, method=method,
                                     headers={"Content-Type": "application/json"})
    try:
        response = urllib.request.urlopen(request)
        status, raw = response.status, response.read()
    except urllib.error.HTTPError as error:
        status, raw = error.code, error.read()
    if raw:
        return status, json.loads(raw)
    return status, None
```

- `urlopen` выбрасывает `HTTPError` при 4xx и 5xx — а при проверке нас интересуют именно эти ответы, поэтому их ловят и возвращают код.
- У ответа 204 нет тела — поэтому `None`.
{{< /solution >}}

### Упражнение 4: Скрипт, который печатает PASS / FAIL

Напишите часть, которая выполняет список проверок и печатает результат. Чтобы потренироваться без сервера, результаты уже есть в списке: имя проверки, ожидаемый код и полученный код.

```py
results = [
    ("create Dana", 201, 201),
    ("duplicate email", 409, 409),
    ("get missing", 404, 200),
    ("delete", 204, 204),
]
```

**Ожидаемый вывод:**

```text
PASS | create Dana | expected 201 got 201
PASS | duplicate email | expected 409 got 409
FAIL | get missing | expected 404 got 200
PASS | delete | expected 204 got 204
3 / 4 checks passed
```

{{< solution >}}
```python
results = [
    ("create Dana", 201, 201),
    ("duplicate email", 409, 409),
    ("get missing", 404, 200),
    ("delete", 204, 204),
]

passed = 0
for name, expected, got in results:
    if expected == got:
        status = "PASS"
        passed = passed + 1
    else:
        status = "FAIL"
    print(status, "|", name, "| expected", expected, "got", got)
print(passed, "/", len(results), "checks passed")
```

```text
PASS | create Dana | expected 201 got 201
PASS | duplicate email | expected 409 got 409
FAIL | get missing | expected 404 got 200
PASS | delete | expected 204 got 204
3 / 4 checks passed
```

С настоящим сервером `got` приходит из `call(...)`. FAIL здесь выявляет настоящий баг: несуществующий студент возвращается как 200 вместо 404.
{{< /solution >}}

### Упражнение 5: Сервер не запущен

Вы запускаете проверочный скрипт и получаете:

```text
urllib.error.URLError: <urlopen error [WinError 10061] No connection could be made
because the target machine actively refused it>
```

1. Что это значит?
2. Как правильно запускать проверки?
3. Почему стоит начинать с пустой базы?

{{< solution >}}
1. **Адрес и порт никто не слушает** — соединение отклонено. Скрипт — это **клиент**, и ему нужен работающий сервер.
2. **Два терминала**: в одном запустите `uvicorn main:app`, а в другом — проверочный скрипт.
3. Чтобы результаты были **предсказуемыми**: id первого студента — 1, а список начинается пустым. На базе со старыми данными «создание Даны» может вернуть 409, потому что она уже есть, — и проверка не пройдёт, хотя бага нет.
{{< /solution >}}

<!-- exercises:end -->

## Итоги

- **Swagger UI** (`/docs`): Try it out → тело → Execute. Строится автоматически по коду и показывает равносильную команду curl.
- **curl -i**: быстрая проверка из терминала. В PowerShell — JSON из файла (`-d "@file.json"`) или Git Bash.
- **Скрипт на Python (End-to-End)**: список проверок (данные) + цикл (логика) → PASS/FAIL. Страховочная сетка для каждого изменения.
- **План проверок** = «счастливый путь» + **каждый** случай сбоя из таблицы соответствий. Проверяйте коды **и** тело.
- Запускайте проверки на пустой базе и с работающим сервером.

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

## Официальная документация

- [FastAPI — Interactive API docs](https://fastapi.tiangolo.com/tutorial/first-steps/#interactive-api-docs)
- [curl — manual](https://curl.se/docs/manpage.html)
- [urllib.request](https://docs.python.org/3/library/urllib.request.html)

<!-- docs:end -->
