# Тестирование API — рабочая тетрадь

Редакция QA Orbit · 2026-10-28.1

API-CORE-1 — независимая спецификация локального учебного стенда QA Orbit. Все данные синтетические. Она не описывает production-портал и не заменяет OD-1/OD-API-1 из курсов ИИ.
A1. Bearer-токены стенда: demo-anna → u-a/north/user; demo-boris → u-b/north/user; demo-vera → u-v/north/editor; demo-oleg → u-o/south/editor. Отсутствующий/неверный токен → 401, error.code=UNAUTHENTICATED, WWW-Authenticate: Bearer realm="qa-orbit-lab". Это публичные учебные значения, не способ аутентификации реального продукта.
A2. POST /tasks: Content-Type application/json, JSON object только с title, description, priority. Title обязателен: строка 1–80 кодовых точек, без trim при сохранении. Description необязателен, по умолчанию "", строка 0–500 кодовых точек Unicode. Priority необязателен, по умолчанию normal, enum low | normal | high. null, неверный тип/длина, неизвестное поле → 422 INVALID_TASK. Невалидный JSON → 400 MALFORMED_JSON. Другой/отсутствующий Content-Type → 415 JSON_REQUIRED. Проверяется сначала A1, затем media type, JSON syntax, поля.
A3. Успешное создание → 201, Location: /tasks/{id}; body={id,team,authorId,title,description,priority,state}. ID выдаёт сервер, team/authorId берутся из токена, state=draft. Все поля сохраняются точно, кроме явно указанных defaults. ID уникален внутри процесса. Пример начальных ID не является обещанием последующих ID.
A4. GET /tasks/{id}: автор читает свой draft; published читают все участники той же команды. Чужой draft, другая команда и неизвестный ID → 404 NOT_FOUND. Успех → 200 и полное тело задачи. Чужой editor не получает права читать draft.
A5. POST /tasks/{id}/publish без тела: порядок A1 → существование/команда (404 NOT_FOUND) → автор ИЛИ editor (403 FORBIDDEN) → state=draft (иначе 409 ALREADY_PUBLISHED) → title.trim() непуст (иначе 422 BLANK_TITLE). Успех → 200, тот же ID, меняется только state на published. Результат предыдущей проверки определяет ошибку при нескольких нарушениях.
A6. Все отказы сохраняют набор задач и поля без изменений. GET не изменяет задачи. Все ответы JSON, Content-Type application/json; charset=utf-8, Cache-Control:no-store. Ошибка имеет форму {"error":{"code":"..."}}.
A7. У POST /tasks нет ключа идемпотентности и автоматического устранения дублей: два одинаковых успешных запроса создают две записи. Потеря ответа не доказывает, что создание не состоялось. SLA/нагрузка/пагинация/удаление не входят в контракт.
A8. Управление только учебной моделью: GET /__lab/snapshot и POST /__lab/reset требуют X-Lab-Key:local-only. Снимок содержит все синтетические задачи; reset восстанавливает 4 начальные и счётчик. Это средства проверки лаборатории, не публичные методы продуктового API. Стенд слушает только 127.0.0.1; данные исчезают при остановке.
Начальные данные после reset: t1 north/u-a/title="План"/draft/normal; t2 north/u-a/title="Релиз"/published/high; t3 north/u-a/title="   "/draft/normal; t4 south/u-o/title="Юг"/draft/low. Description у всех "".

Практика на выбор: (1) анализировать выданные трассы прямо на сайте — запуск не нужен; (2) скачать api-lab.mjs, check-lab.mjs и README.md в отдельную папку, запустить node api-lab.mjs в терминале, затем node check-lab.mjs во втором. Нужен поддерживаемый Node.js 22.13+ (подойдёт актуальный 24/26), без npm install. Адрес по умолчанию http://127.0.0.1:4177. Остановка Ctrl+C. Если порт занят, QA_ORBIT_API_PORT=4178 node api-lab.mjs и QA_ORBIT_API_URL=http://127.0.0.1:4178 node check-lab.mjs. Скачанные скрипты — обычный исходный код: его можно прочитать перед запуском. Сайт не запускает этот код в фоне.

## HTTP-запрос, ответ и проверяемый контракт

Вам дали запрос и ответ. Сначала проверьте его по контракту, затем при желании повторите в локальной лаборатории.

### Входные данные

API-CORE-1 — независимая спецификация локального учебного стенда QA Orbit. Все данные синтетические. Она не описывает production-портал и не заменяет OD-1/OD-API-1 из курсов ИИ.
A1. Bearer-токены стенда: demo-anna → u-a/north/user; demo-boris → u-b/north/user; demo-vera → u-v/north/editor; demo-oleg → u-o/south/editor. Отсутствующий/неверный токен → 401, error.code=UNAUTHENTICATED, WWW-Authenticate: Bearer realm="qa-orbit-lab". Это публичные учебные значения, не способ аутентификации реального продукта.
A2. POST /tasks: Content-Type application/json, JSON object только с title, description, priority. Title обязателен: строка 1–80 кодовых точек, без trim при сохранении. Description необязателен, по умолчанию "", строка 0–500 кодовых точек Unicode. Priority необязателен, по умолчанию normal, enum low | normal | high. null, неверный тип/длина, неизвестное поле → 422 INVALID_TASK. Невалидный JSON → 400 MALFORMED_JSON. Другой/отсутствующий Content-Type → 415 JSON_REQUIRED. Проверяется сначала A1, затем media type, JSON syntax, поля.
A3. Успешное создание → 201, Location: /tasks/{id}; body={id,team,authorId,title,description,priority,state}. ID выдаёт сервер, team/authorId берутся из токена, state=draft. Все поля сохраняются точно, кроме явно указанных defaults. ID уникален внутри процесса. Пример начальных ID не является обещанием последующих ID.
A4. GET /tasks/{id}: автор читает свой draft; published читают все участники той же команды. Чужой draft, другая команда и неизвестный ID → 404 NOT_FOUND. Успех → 200 и полное тело задачи. Чужой editor не получает права читать draft.
A5. POST /tasks/{id}/publish без тела: порядок A1 → существование/команда (404 NOT_FOUND) → автор ИЛИ editor (403 FORBIDDEN) → state=draft (иначе 409 ALREADY_PUBLISHED) → title.trim() непуст (иначе 422 BLANK_TITLE). Успех → 200, тот же ID, меняется только state на published. Результат предыдущей проверки определяет ошибку при нескольких нарушениях.
A6. Все отказы сохраняют набор задач и поля без изменений. GET не изменяет задачи. Все ответы JSON, Content-Type application/json; charset=utf-8, Cache-Control:no-store. Ошибка имеет форму {"error":{"code":"..."}}.
A7. У POST /tasks нет ключа идемпотентности и автоматического устранения дублей: два одинаковых успешных запроса создают две записи. Потеря ответа не доказывает, что создание не состоялось. SLA/нагрузка/пагинация/удаление не входят в контракт.
A8. Управление только учебной моделью: GET /__lab/snapshot и POST /__lab/reset требуют X-Lab-Key:local-only. Снимок содержит все синтетические задачи; reset восстанавливает 4 начальные и счётчик. Это средства проверки лаборатории, не публичные методы продуктового API. Стенд слушает только 127.0.0.1; данные исчезают при остановке.
Начальные данные после reset: t1 north/u-a/title="План"/draft/normal; t2 north/u-a/title="Релиз"/published/high; t3 north/u-a/title="   "/draft/normal; t4 south/u-o/title="Юг"/draft/low. Description у всех "".

Запрос: POST /tasks, Bearer demo-anna, Content-Type:application/json, body={"title":" План "}. Выданный ответ: 201, Location:/tasks/t9, Content-Type:application/json; charset=utf-8, body={"id":"t9","team":"north","authorId":"u-b","title":"План","description":"","priority":"normal","state":"draft"}.

### Задание

1. Найдите два противоречия A3, даже несмотря на 201.
2. Перечислите пять частей успешного ответа, которые стоит проверять.
3. Спроектируйте GET созданного ID и проверку количества записей.
4. Выберите режим: статический анализ либо фактический локальный запуск; явно отметьте его.

### Критерии

- Автор ожидается u-a, а не u-b.
- Title сохраняет начальный и конечный пробел.
- ID берётся из ответа и согласован с Location.
- Статический ответ не выдан за собственный запуск.

### Разбор после своей попытки

Два нарушения: authorId должен быть u-a из demo-anna; title должен оставаться " План " без trim. Остальное в показанном ответе не снимает этих проблем. Проверки успеха: статус 201; JSON Content-Type; непустой строковый id; Location=/tasks/{тот же id}; точные team/authorId/title/state и defaults. Затем GET /tasks/t9 под Анной ожидает 200 с теми же полями. Чтобы доказать ровно одну запись, сравните лабораторные snapshot до/после и множество ID, а не только GET t9. По выданному материалу статус — статический анализ трассы, не динамическое исполнение. При локальном повторе используйте новый фактический ID; t9 не обещан.

### Типичные ошибки

- Считать любой 2xx достаточным.
- Ожидать произвольный ID из учебного ответа при каждом запуске.
- Переносить порядок ошибок A5 на любой HTTP API.

## Позитивные и негативные проверки данных

Коллега проверяет только {title:null,priority:"urgent"} и говорит, что вся валидация покрыта.

### Входные данные

API-CORE-1 — независимая спецификация локального учебного стенда QA Orbit. Все данные синтетические. Она не описывает production-портал и не заменяет OD-1/OD-API-1 из курсов ИИ.
A1. Bearer-токены стенда: demo-anna → u-a/north/user; demo-boris → u-b/north/user; demo-vera → u-v/north/editor; demo-oleg → u-o/south/editor. Отсутствующий/неверный токен → 401, error.code=UNAUTHENTICATED, WWW-Authenticate: Bearer realm="qa-orbit-lab". Это публичные учебные значения, не способ аутентификации реального продукта.
A2. POST /tasks: Content-Type application/json, JSON object только с title, description, priority. Title обязателен: строка 1–80 кодовых точек, без trim при сохранении. Description необязателен, по умолчанию "", строка 0–500 кодовых точек Unicode. Priority необязателен, по умолчанию normal, enum low | normal | high. null, неверный тип/длина, неизвестное поле → 422 INVALID_TASK. Невалидный JSON → 400 MALFORMED_JSON. Другой/отсутствующий Content-Type → 415 JSON_REQUIRED. Проверяется сначала A1, затем media type, JSON syntax, поля.
A3. Успешное создание → 201, Location: /tasks/{id}; body={id,team,authorId,title,description,priority,state}. ID выдаёт сервер, team/authorId берутся из токена, state=draft. Все поля сохраняются точно, кроме явно указанных defaults. ID уникален внутри процесса. Пример начальных ID не является обещанием последующих ID.
A4. GET /tasks/{id}: автор читает свой draft; published читают все участники той же команды. Чужой draft, другая команда и неизвестный ID → 404 NOT_FOUND. Успех → 200 и полное тело задачи. Чужой editor не получает права читать draft.
A5. POST /tasks/{id}/publish без тела: порядок A1 → существование/команда (404 NOT_FOUND) → автор ИЛИ editor (403 FORBIDDEN) → state=draft (иначе 409 ALREADY_PUBLISHED) → title.trim() непуст (иначе 422 BLANK_TITLE). Успех → 200, тот же ID, меняется только state на published. Результат предыдущей проверки определяет ошибку при нескольких нарушениях.
A6. Все отказы сохраняют набор задач и поля без изменений. GET не изменяет задачи. Все ответы JSON, Content-Type application/json; charset=utf-8, Cache-Control:no-store. Ошибка имеет форму {"error":{"code":"..."}}.
A7. У POST /tasks нет ключа идемпотентности и автоматического устранения дублей: два одинаковых успешных запроса создают две записи. Потеря ответа не доказывает, что создание не состоялось. SLA/нагрузка/пагинация/удаление не входят в контракт.
A8. Управление только учебной моделью: GET /__lab/snapshot и POST /__lab/reset требуют X-Lab-Key:local-only. Снимок содержит все синтетические задачи; reset восстанавливает 4 начальные и счётчик. Это средства проверки лаборатории, не публичные методы продуктового API. Стенд слушает только 127.0.0.1; данные исчезают при остановке.
Начальные данные после reset: t1 north/u-a/title="План"/draft/normal; t2 north/u-a/title="Релиз"/published/high; t3 north/u-a/title="   "/draft/normal; t4 south/u-o/title="Юг"/draft/low. Description у всех "".

Базовый запрос: POST /tasks; Bearer demo-anna; Content-Type:application/json; {"title":"План","description":"","priority":"normal"}. Каждый сценарий независим. Выданная трасса V1: запрос {"title":null} →422 INVALID_TASK; snapshot до содержит t1–t4, snapshot после содержит t1–t5, t5 имеет title="Новая задача".

### Задание

1. Составьте восемь независимых негативных запросов и два позитивных для defaults/границы.
2. Разделите expected status и error.code.
3. Найдите нарушение в V1 по побочному эффекту.
4. Объясните, почему смешанный запрос коллеги не подтверждает оба правила.

### Критерии

- Есть отдельные 415,400,422 примеры с валидной аутентификацией.
- null не равен отсутствию и пустой строке.
- В позитивных есть 80 кодовых точек и отсутствие необязательных полей.
- V1 признаётся нарушением A6 несмотря на 422.

### Разбор после своей попытки

Восемь негативов: Content-Type text/plain→415 JSON_REQUIRED; обрезанный JSON→400 MALFORMED_JSON; title отсутствует→422 INVALID_TASK; title:null→422; title:42→422; title:""→422; title=А×81→422; priority:"urgent" при валидном title→422. Все с demo-anna; кроме media type и синтаксиса меняется одно поле. Позитивы: {title:"План"}→201 с defaults; {title:А×80}→201 с точным title. Добавочные полезные классы — description:null, description × 501, unknown authorId. В V1 ошибка по ответу верна, но новый t5 нарушает A6: отказ не должен создавать запись. Комбинированный null+urgent доказывает лишь один общий отказ; неизвестно, какая ветвь сработала. При локальном выполнении snapshot сравнивается целиком до/после каждого негатива.

### Типичные ошибки

- Отправить неверный токен и тестировать по 401 ошибку JSON.
- Ожидать default для явного null.
- Засчитать отрицательный тест без проверки состояния.

## Авторизация, объектный доступ и состояние

Нужно проверить необычное разделение прав и повторный переход.

### Входные данные

API-CORE-1 — независимая спецификация локального учебного стенда QA Orbit. Все данные синтетические. Она не описывает production-портал и не заменяет OD-1/OD-API-1 из курсов ИИ.
A1. Bearer-токены стенда: demo-anna → u-a/north/user; demo-boris → u-b/north/user; demo-vera → u-v/north/editor; demo-oleg → u-o/south/editor. Отсутствующий/неверный токен → 401, error.code=UNAUTHENTICATED, WWW-Authenticate: Bearer realm="qa-orbit-lab". Это публичные учебные значения, не способ аутентификации реального продукта.
A2. POST /tasks: Content-Type application/json, JSON object только с title, description, priority. Title обязателен: строка 1–80 кодовых точек, без trim при сохранении. Description необязателен, по умолчанию "", строка 0–500 кодовых точек Unicode. Priority необязателен, по умолчанию normal, enum low | normal | high. null, неверный тип/длина, неизвестное поле → 422 INVALID_TASK. Невалидный JSON → 400 MALFORMED_JSON. Другой/отсутствующий Content-Type → 415 JSON_REQUIRED. Проверяется сначала A1, затем media type, JSON syntax, поля.
A3. Успешное создание → 201, Location: /tasks/{id}; body={id,team,authorId,title,description,priority,state}. ID выдаёт сервер, team/authorId берутся из токена, state=draft. Все поля сохраняются точно, кроме явно указанных defaults. ID уникален внутри процесса. Пример начальных ID не является обещанием последующих ID.
A4. GET /tasks/{id}: автор читает свой draft; published читают все участники той же команды. Чужой draft, другая команда и неизвестный ID → 404 NOT_FOUND. Успех → 200 и полное тело задачи. Чужой editor не получает права читать draft.
A5. POST /tasks/{id}/publish без тела: порядок A1 → существование/команда (404 NOT_FOUND) → автор ИЛИ editor (403 FORBIDDEN) → state=draft (иначе 409 ALREADY_PUBLISHED) → title.trim() непуст (иначе 422 BLANK_TITLE). Успех → 200, тот же ID, меняется только state на published. Результат предыдущей проверки определяет ошибку при нескольких нарушениях.
A6. Все отказы сохраняют набор задач и поля без изменений. GET не изменяет задачи. Все ответы JSON, Content-Type application/json; charset=utf-8, Cache-Control:no-store. Ошибка имеет форму {"error":{"code":"..."}}.
A7. У POST /tasks нет ключа идемпотентности и автоматического устранения дублей: два одинаковых успешных запроса создают две записи. Потеря ответа не доказывает, что создание не состоялось. SLA/нагрузка/пагинация/удаление не входят в контракт.
A8. Управление только учебной моделью: GET /__lab/snapshot и POST /__lab/reset требуют X-Lab-Key:local-only. Снимок содержит все синтетические задачи; reset восстанавливает 4 начальные и счётчик. Это средства проверки лаборатории, не публичные методы продуктового API. Стенд слушает только 127.0.0.1; данные исчезают при остановке.
Начальные данные после reset: t1 north/u-a/title="План"/draft/normal; t2 north/u-a/title="Релиз"/published/high; t3 north/u-a/title="   "/draft/normal; t4 south/u-o/title="Юг"/draft/low. Description у всех "".

Выданные трассы независимы, каждая после reset: S1 Вера GET t1→200 с draft; S2 Борис POST t2/publish→409; S3 Вера POST t1/publish→200 published, но snapshot показывает дополнительно новую задачу t5; S4 Анна POST t3/publish→422 BLANK_TITLE, данные не изменились.

### Задание

1. Найдите нарушения S1–S3 и объясните, почему S4 согласуется с контрактом.
2. Составьте последовательность GET Веры → publish Веры → GET Бориса → повтор Анны.
3. Укажите ожидаемые статусы, поля и точки сравнения snapshot.
4. Объясните, где reset разрешён, а где ломает проверку перехода.

### Критерии

- S1 ожидает 404, несмотря на editor.
- S2 ожидает 403 из-за порядка A5.
- S3 выявляет лишнюю копию через snapshot.
- Повтор проверяется без reset внутри последовательности.

### Разбор после своей попытки

S1 нарушает A4: чужой draft Вере не читается, ожидается 404 NOT_FOUND. S2 нарушает порядок A5: Борис не автор/editor, поэтому 403 FORBIDDEN до проверки published. S3 нарушает A5/A6: меняется только state t1, новый t5 недопустим. S4 корректна: Анна имеет право, t3=draft, но trim(title) пуст,422 без мутации. Последовательность после одного reset: snapshot B; Вера GET t1→404; Вера POST /tasks/t1/publish→200, t1.state=published; snapshot равен B с единственным изменением state t1; Борис GET t1→200 published; Анна повторная публикация→409; snapshot не меняется. Reset нужен до независимого сценария, но не между публикацией и повтором.

### Типичные ошибки

- Проверять 409 под пользователем без разрешения, ожидая обойти 403.
- Разрешить editor всё чтение по привычке.
- Проверять отсутствие копии только по прежнему ID.

## Коллекция, окружение и осмысленные assertions

Коллекция иногда зелёная даже после ошибки создания. Найдите причину и исправьте логику.

### Входные данные

API-CORE-1 — независимая спецификация локального учебного стенда QA Orbit. Все данные синтетические. Она не описывает production-портал и не заменяет OD-1/OD-API-1 из курсов ИИ.
A1. Bearer-токены стенда: demo-anna → u-a/north/user; demo-boris → u-b/north/user; demo-vera → u-v/north/editor; demo-oleg → u-o/south/editor. Отсутствующий/неверный токен → 401, error.code=UNAUTHENTICATED, WWW-Authenticate: Bearer realm="qa-orbit-lab". Это публичные учебные значения, не способ аутентификации реального продукта.
A2. POST /tasks: Content-Type application/json, JSON object только с title, description, priority. Title обязателен: строка 1–80 кодовых точек, без trim при сохранении. Description необязателен, по умолчанию "", строка 0–500 кодовых точек Unicode. Priority необязателен, по умолчанию normal, enum low | normal | high. null, неверный тип/длина, неизвестное поле → 422 INVALID_TASK. Невалидный JSON → 400 MALFORMED_JSON. Другой/отсутствующий Content-Type → 415 JSON_REQUIRED. Проверяется сначала A1, затем media type, JSON syntax, поля.
A3. Успешное создание → 201, Location: /tasks/{id}; body={id,team,authorId,title,description,priority,state}. ID выдаёт сервер, team/authorId берутся из токена, state=draft. Все поля сохраняются точно, кроме явно указанных defaults. ID уникален внутри процесса. Пример начальных ID не является обещанием последующих ID.
A4. GET /tasks/{id}: автор читает свой draft; published читают все участники той же команды. Чужой draft, другая команда и неизвестный ID → 404 NOT_FOUND. Успех → 200 и полное тело задачи. Чужой editor не получает права читать draft.
A5. POST /tasks/{id}/publish без тела: порядок A1 → существование/команда (404 NOT_FOUND) → автор ИЛИ editor (403 FORBIDDEN) → state=draft (иначе 409 ALREADY_PUBLISHED) → title.trim() непуст (иначе 422 BLANK_TITLE). Успех → 200, тот же ID, меняется только state на published. Результат предыдущей проверки определяет ошибку при нескольких нарушениях.
A6. Все отказы сохраняют набор задач и поля без изменений. GET не изменяет задачи. Все ответы JSON, Content-Type application/json; charset=utf-8, Cache-Control:no-store. Ошибка имеет форму {"error":{"code":"..."}}.
A7. У POST /tasks нет ключа идемпотентности и автоматического устранения дублей: два одинаковых успешных запроса создают две записи. Потеря ответа не доказывает, что создание не состоялось. SLA/нагрузка/пагинация/удаление не входят в контракт.
A8. Управление только учебной моделью: GET /__lab/snapshot и POST /__lab/reset требуют X-Lab-Key:local-only. Снимок содержит все синтетические задачи; reset восстанавливает 4 начальные и счётчик. Это средства проверки лаборатории, не публичные методы продуктового API. Стенд слушает только 127.0.0.1; данные исчезают при остановке.
Начальные данные после reset: t1 north/u-a/title="План"/draft/normal; t2 north/u-a/title="Релиз"/published/high; t3 north/u-a/title="   "/draft/normal; t4 south/u-o/title="Юг"/draft/low. Description у всех "".

Старая логика: переменная taskId=t2 осталась после прошлого запуска. Create отправил пустой title и получил 422. Скрипт проверяет только pm.expect(pm.response.code).to.be.below(500), taskId не очищает. Read использует /tasks/{{taskId}} под Анной и получает 200. Время каждого ответа <200 ms, но SLA не задан.

### Задание

1. Объясните ложный успех и перечислите три обязательных исправления.
2. Напишите два именованных assertions для Create и один для Read.
3. Сформулируйте статус набора при недоступном локальном сервере.
4. Скачайте коллекцию и при желании выполните её по порядку на своём стенде.

### Критерии

- 422 не проходит позитивный Create, ожидающий 201.
- taskId очищается до нового создания.
- Read использует только ID текущего успешного Create.
- Неподтверждённый SLA не превращён в требование.

### Разбор после своей попытки

Ложный успех: below500 принимает 422, а Read читает старую published t2 и не связан с текущим созданием. Исправления: pre-request pm.collectionVariables.unset("taskId"); точный статус 201 плюс обязательные поля/defaults; новый ID записывается только из текущего успешного ответа. Read проверяет 200 и data.id===pm.collectionVariables.get("taskId"), а при отсутствии ID сценарий не считается исполненным успешно. Assertions: pm.test("201 on create",()=>pm.response.to.have.status(201)); pm.test("draft",()=>pm.expect(pm.response.json().state).to.eql("draft")); для Read сравнить id. Connection refused означает, что HTTP-результат не получен: Blocked с указанием незапущенной лаборатории. Время 200 ms можно записать наблюдением; порог не является оракулом контракта.

### Типичные ошибки

- Принять 4xx за успешный позитивный тест через below500.
- Сохранить старую переменную после неуспешного Create.
- Отправить учебную коллекцию в реальный production-адрес.

## Диагностика, повторы и честный результат

В отчёт попали три красных события. Нужно решить, где проблема контракта, где теста, а где окружения.

### Входные данные

API-CORE-1 — независимая спецификация локального учебного стенда QA Orbit. Все данные синтетические. Она не описывает production-портал и не заменяет OD-1/OD-API-1 из курсов ИИ.
A1. Bearer-токены стенда: demo-anna → u-a/north/user; demo-boris → u-b/north/user; demo-vera → u-v/north/editor; demo-oleg → u-o/south/editor. Отсутствующий/неверный токен → 401, error.code=UNAUTHENTICATED, WWW-Authenticate: Bearer realm="qa-orbit-lab". Это публичные учебные значения, не способ аутентификации реального продукта.
A2. POST /tasks: Content-Type application/json, JSON object только с title, description, priority. Title обязателен: строка 1–80 кодовых точек, без trim при сохранении. Description необязателен, по умолчанию "", строка 0–500 кодовых точек Unicode. Priority необязателен, по умолчанию normal, enum low | normal | high. null, неверный тип/длина, неизвестное поле → 422 INVALID_TASK. Невалидный JSON → 400 MALFORMED_JSON. Другой/отсутствующий Content-Type → 415 JSON_REQUIRED. Проверяется сначала A1, затем media type, JSON syntax, поля.
A3. Успешное создание → 201, Location: /tasks/{id}; body={id,team,authorId,title,description,priority,state}. ID выдаёт сервер, team/authorId берутся из токена, state=draft. Все поля сохраняются точно, кроме явно указанных defaults. ID уникален внутри процесса. Пример начальных ID не является обещанием последующих ID.
A4. GET /tasks/{id}: автор читает свой draft; published читают все участники той же команды. Чужой draft, другая команда и неизвестный ID → 404 NOT_FOUND. Успех → 200 и полное тело задачи. Чужой editor не получает права читать draft.
A5. POST /tasks/{id}/publish без тела: порядок A1 → существование/команда (404 NOT_FOUND) → автор ИЛИ editor (403 FORBIDDEN) → state=draft (иначе 409 ALREADY_PUBLISHED) → title.trim() непуст (иначе 422 BLANK_TITLE). Успех → 200, тот же ID, меняется только state на published. Результат предыдущей проверки определяет ошибку при нескольких нарушениях.
A6. Все отказы сохраняют набор задач и поля без изменений. GET не изменяет задачи. Все ответы JSON, Content-Type application/json; charset=utf-8, Cache-Control:no-store. Ошибка имеет форму {"error":{"code":"..."}}.
A7. У POST /tasks нет ключа идемпотентности и автоматического устранения дублей: два одинаковых успешных запроса создают две записи. Потеря ответа не доказывает, что создание не состоялось. SLA/нагрузка/пагинация/удаление не входят в контракт.
A8. Управление только учебной моделью: GET /__lab/snapshot и POST /__lab/reset требуют X-Lab-Key:local-only. Снимок содержит все синтетические задачи; reset восстанавливает 4 начальные и счётчик. Это средства проверки лаборатории, не публичные методы продуктового API. Стенд слушает только 127.0.0.1; данные исчезают при остановке.
Начальные данные после reset: t1 north/u-a/title="План"/draft/normal; t2 north/u-a/title="Релиз"/published/high; t3 north/u-a/title="   "/draft/normal; t4 south/u-o/title="Юг"/draft/low. Description у всех "".

D1: GET /tasks/t1 → connection refused, HTTP-status отсутствует. D2: Борис POST /tasks/t2/publish →403 FORBIDDEN, тест ожидал 409. D3: Анна POST /tasks {"title":null} →422 INVALID_TASK, snapshot содержит новую t5. D4: Анна дважды послала корректный одинаковый Create и получила 201 с t5, затем 201 с t6. Все примеры независимы после reset.

### Задание

1. Классифицируйте D1–D4 и выберите следующее действие.
2. Оформите дефект D3 с ожиданием неизменности и без вымышленной причины.
3. Объясните, почему автоматический повтор Create после таймаута опасен.
4. Укажите ограничения доказательства, если анализировали только выданные данные.

### Критерии

- D1 не назван HTTP 500.
- D2 распознан как неверный oracle теста.
- D3 содержит сравнение полного набора задач.
- D4 не объявлен дефектом без требования идемпотентности.

### Разбор после своей попытки

D1: транспортное препятствие, проверить запуск/127.0.0.1/порт, статус исполнения Blocked. D2: тест ошибочен, по A5 право раньше состояния, ожидается 403; скорректировать oracle. D3: дефект A6, отказ создал лишнюю запись; предусловия reset/demo-anna, запрос title:null, ожидается 422+точно тот же snapshot, факт 422+новая t5, причина неизвестна. D4: допустимо по A7, два успешных Create дают два разных ID. При потере ответа слепой повтор может дублировать уже выполненное создание; механизм идемпотентности должен быть явно реализован и проверен, здесь его нет. Если это чтение данных упражнения, вывод относится к предоставленным трассам, а не собственному динамическому прогону.

### Типичные ошибки

- Называть любую ошибку инструмента серверным 500.
- Автоматически считать повтор POST безопасным.
- Принять два ID за дефект вопреки A7.

## Итоговый проект: проверка API-контракта

Вы готовите воспроизводимый учебный набор для нового QA в команде.

### Входные данные

API-CORE-1 — независимая спецификация локального учебного стенда QA Orbit. Все данные синтетические. Она не описывает production-портал и не заменяет OD-1/OD-API-1 из курсов ИИ.
A1. Bearer-токены стенда: demo-anna → u-a/north/user; demo-boris → u-b/north/user; demo-vera → u-v/north/editor; demo-oleg → u-o/south/editor. Отсутствующий/неверный токен → 401, error.code=UNAUTHENTICATED, WWW-Authenticate: Bearer realm="qa-orbit-lab". Это публичные учебные значения, не способ аутентификации реального продукта.
A2. POST /tasks: Content-Type application/json, JSON object только с title, description, priority. Title обязателен: строка 1–80 кодовых точек, без trim при сохранении. Description необязателен, по умолчанию "", строка 0–500 кодовых точек Unicode. Priority необязателен, по умолчанию normal, enum low | normal | high. null, неверный тип/длина, неизвестное поле → 422 INVALID_TASK. Невалидный JSON → 400 MALFORMED_JSON. Другой/отсутствующий Content-Type → 415 JSON_REQUIRED. Проверяется сначала A1, затем media type, JSON syntax, поля.
A3. Успешное создание → 201, Location: /tasks/{id}; body={id,team,authorId,title,description,priority,state}. ID выдаёт сервер, team/authorId берутся из токена, state=draft. Все поля сохраняются точно, кроме явно указанных defaults. ID уникален внутри процесса. Пример начальных ID не является обещанием последующих ID.
A4. GET /tasks/{id}: автор читает свой draft; published читают все участники той же команды. Чужой draft, другая команда и неизвестный ID → 404 NOT_FOUND. Успех → 200 и полное тело задачи. Чужой editor не получает права читать draft.
A5. POST /tasks/{id}/publish без тела: порядок A1 → существование/команда (404 NOT_FOUND) → автор ИЛИ editor (403 FORBIDDEN) → state=draft (иначе 409 ALREADY_PUBLISHED) → title.trim() непуст (иначе 422 BLANK_TITLE). Успех → 200, тот же ID, меняется только state на published. Результат предыдущей проверки определяет ошибку при нескольких нарушениях.
A6. Все отказы сохраняют набор задач и поля без изменений. GET не изменяет задачи. Все ответы JSON, Content-Type application/json; charset=utf-8, Cache-Control:no-store. Ошибка имеет форму {"error":{"code":"..."}}.
A7. У POST /tasks нет ключа идемпотентности и автоматического устранения дублей: два одинаковых успешных запроса создают две записи. Потеря ответа не доказывает, что создание не состоялось. SLA/нагрузка/пагинация/удаление не входят в контракт.
A8. Управление только учебной моделью: GET /__lab/snapshot и POST /__lab/reset требуют X-Lab-Key:local-only. Снимок содержит все синтетические задачи; reset восстанавливает 4 начальные и счётчик. Это средства проверки лаборатории, не публичные методы продуктового API. Стенд слушает только 127.0.0.1; данные исчезают при остановке.
Начальные данные после reset: t1 north/u-a/title="План"/draft/normal; t2 north/u-a/title="Релиз"/published/high; t3 north/u-a/title="   "/draft/normal; t4 south/u-o/title="Юг"/draft/low. Description у всех "".

Офлайн-свидетельства для разбора: S1 Вера GET t1→200 с draft; S2 Борис publish t2→409; S3 Вера publish t1→200 и лишняя t5; S4 Анна publish t3→422 без мутации. D1 connection refused без HTTP-status; D2 Борис publish t2→403 при ожидании 409; D3 title:null→422+лишняя t5; D4 два корректных Create→201/t5 и 201/t6. Это независимые учебные трассы, не фактический вывод скачиваемой лаборатории. Для динамического режима используйте lab и наблюдайте свой реальный результат.

### Задание

1. Соберите 12–18 сценариев с позитивами, тремя уровнями валидации, ролями/командами и переходами.
2. Включите проверку 401+challenge, defaults, точного title/author/Location, инвариантов отказа и публикации.
3. Реализуйте либо опишите цепочку Create→Read→Publish→Repeat с ID текущего запуска.
4. Добавьте собственный класс данных и объясните его риск.
5. Сдайте отчёт: режим, окружение, фактические статусы или Not run, дефекты/ошибки тестов и ограничения.

### Критерии

- Есть прослеживаемость A-правило→сценарий→assertion→свидетельство.
- Оракулы учитывают порядок ошибок и разделение чтения/публикации.
- Нет зависимости от случайного ID и прошлого запуска.
- Отказы и успешная публикация проверяют поля/число записей.
- Результат локальной модели не выдан за проверку production.

### Разбор после своей попытки

Каркас: 1) без токена GET→401+challenge; 2) Create с title=" План "→201/defaults/u-a/точный title/Location; 3) Read полученного ID→200; 4) text/plain→415; 5) обрезанный JSON→400; 6) title:null→422; 7) title × 81→422; 8) unknown authorId→422; 9) чужой draft Борис→404; 10) published Борис→200; 11) другая команда→404; 12) Борис publish→403; 13) Анна публикация пробельного title→422; 14) Вера publish t1→200; 15) повторпод Анной→409; 16) собственное расширение description:null→422 либо😀×80→201. Снимки подтверждают отсутствие мутаций у отказов и единственное изменение state при успехе. Dynamic report содержит реальный вывод; offline report оставляет новые запросы — Not run и правильно классифицирует S1/S2/S3/D3 как нарушения трасс, D2 как неверный тест, D1 как препятствие, D4 как допустимый повтор. Нельзя перепутать выданные ошибочные трассы с результатом корректного локального стенда.

### Типичные ошибки

- Объявить фикстуру плохого ответа фактическим выводом локального сервера.
- Заявить запуск checks без реального исполнения.
- Оставить учебный процесс работающим после завершения.
