Баги в API v0-test-api

Нажмите на эндпоинт, чтобы увидеть баги. Нажмите на баг, чтобы прочитать описание.

GET /v1/api/users 5 багов
Content-Type: text/plain вместо application/json

Что сейчас: Ответ приходит с Content-Type: text/plain

Что должно быть: Content-Type: application/json

// Сейчас
Content-Type: text/plain
[{"id":1,"name":"John","age":15,"status":"minor"}]

// Должно быть
Content-Type: application/json
[{"id":1,"name":"John","age":15,"status":"minor"}]
Нет заголовков безопасности

Что сейчас: Отсутствуют X-Content-Type-Options: nosniff и X-Frame-Options: DENY

Что должно быть: Оба заголовка должны присутствовать

Влияние: Уязвимости к MIME-спуфингу и кликджекингу

Публичное кэширование данных (public, max-age=180)

Что сейчас: Cache-Control: public, max-age=180 — данные пользователей кэшируются на 3 минуты

Что должно быть: Cache-Control: no-cache (без кэширования)

Заголовок X-Cache-Info раскрывает внутреннюю реализацию

Что сейчас: Ответ содержит X-Cache-Info: V1-Legacy-Cache-Enabled

Что должно быть: Без лишних заголовков

api_key возвращается в ответах

Что сейчас: Ответы users и passes содержат поле api_key (SELECT * без маппинга полей)

Что должно быть: api_key — секретное поле, не должен возвращаться клиенту

// Сейчас: {"id":1,"name":"John","age":25,"status":"candidate","api_key":"test-key-123"}
// Должно быть: {"id":1,"name":"John","age":25,"status":"candidate"}
GET /v1/api/users?status= 3 бага
Статус-фильтр зависит от регистра

Что сейчас: GET /v1/api/users?status=MINOR → пустой массив

Что должно быть: Должен находить пользователей независимо от регистра

// Запрос: GET /v1/api/users?status=MINOR
// Сейчас: 200 []
// Должно быть: 200 [{"id":1,"name":"John","age":15,"status":"minor"}]
Принимает невалидные статусы без ошибки

Что сейчас: GET /v1/api/users?status=invalid → 200 с пустым массивом

Что должно быть: 400 Bad Request с сообщением об ошибке

Принимает несколько значений через запятую

Что сейчас: GET /v1/api/users?status=minor,candidate → 200 []

Что должно быть: 400 — параметр status принимает только одно значение

POST /v1/api/users 6 багов
Нет валидации name (цифры и спецсимволы проходят)

Что сейчас: Принимает любые символы в name: "John123!"

Что должно быть: Только буквы, пробелы и тире

// Body: {"name": "John123!", "age": 25}
// Сейчас: 201 успех
// Должно быть: 400 {"message":"Valid name is required"}
Возраст 66+ отклоняется (должен создаваться как retired)

Что сейчас: age=66 → 400 "Age must be between 17 and 65"

Что должно быть: age 66-120 должен создаваться со статусом "retired"

// POST /v1/api/users {"name":"Old","age":66}
// Сейчас: 400 {"message":"Age must be between 17 and 65"}
// Должно быть: 201 {"name":"Old","age":66,"status":"retired"}
Возраст 61-65 получает статус retired (должен быть candidate)

Что сейчас: age 61-65 → статус "retired" (логика age > 60)

Что должно быть: 18-65 → "candidate", только 66+ → "retired"

// POST /v1/api/users {"name":"Mid","age":61}
// Сейчас: {"status":"retired"}
// Должно быть: {"status":"candidate"}
Возраст 0-16 не создаётся, а 17 лет получает неверный статус (должен быть minor)

Что сейчас: age=16 → 400 "Age must be between 17 and 65", а age=17 → 201 Created со статусом "candidate"

Что должно быть: любой возраст 0-17 должен создаваться со статусом "minor"

// POST /v1/api/users {"name":"Kid","age":16}
// Сейчас: 400 {"message":"Age must be between 17 and 65"}
// Должно быть: 201 {"name":"Kid","age":16,"status":"minor"}
age возвращается как строка

Что сейчас: age возвращается как "25" (строка)

Что должно быть: age должен быть числом 25

Создаёт 2 пропуска вместо 1

Что сейчас: При создании пользователя генерируется 2 screening pass

Что должно быть: 1 пропуск (как в v2)

GET /v1/api/users/{id} 4 бага
Ищет по id-1 вместо id

Что сейчас: Запрос id=82 ищет пользователя с id=81

Что должно быть: Должен искать по точному id

// GET /v1/api/users/82
// Сейчас: возвращает пользователя с id=81
// Должно быть: пользователь с id=82
Строковый id вызывает 500 Server error вместо 400

Что сейчас: GET /v1/api/users/abc → 500 {"message":"Server error"}

Что должно быть: 400 Bad Request — id должен быть числом

// GET /v1/api/users/abc
// Сейчас: 500 Server error
// Должно быть: 400 {"message":"id must be a number"}
Несуществующий id возвращает 200 с данными другого пользователя

Что сейчас: id=999 ищет пользователя с id=998. Если такой есть — 200 с его данными. Если нет — 404

Что должно быть: 404 User not found для любого несуществующего id

// GET /v1/api/users/999
// Сейчас: может вернуть 200 с данными другого пользователя (id=998)
// Должно быть: 404 {"message":"User not found"}
Несуществующий id возвращает 200 с пустым телом вместо 404

Что сейчас: GET /v1/api/users/999999 → 200 {}

Что должно быть: 404 User not found

// GET /v1/api/users/999999
// Сейчас: 200 {}
// Должно быть: 404 {"message":"User not found"}
PATCH /v1/api/users/{id} 6 багов
Отрицательный возраст → 500 Server error

Что сейчас: {"age": -1} → 500 {"message":"Server error"} (вместо внятной ошибки валидации)

Что должно быть: 400 "Age must be greater than or equal to 0"

age=0 отклоняется (легитимное значение)

Что сейчас: PATCH {"age": 0} → 400 "At least one field (name or age) is required" — 0 воспринимается как отсутствие поля

Что должно быть: 200, age=0 принят, статус пересчитан в minor

В ответе нет id

Что сейчас: Ответ не содержит поле id

Что должно быть: id должен быть в ответе

// Сейчас: {"name":"John","age":30,"status":"candidate"} — нет id
// Должно быть: {"id":1,"name":"John","age":30,"status":"candidate"}
age возвращается строкой

Что сейчас: age возвращается как "30"

Что должно быть: age как число 30

Нет валидации name

Что сейчас: Принимает любые символы в name при обновлении

Что должно быть: Только буквы, пробелы и тире

Нет status в ответе при возрасте 66+ (retired)

Что сейчас: PATCH age=70 → {"name":"John","age":"70"} — без status

Что должно быть: {"name":"John","age":70,"status":"retired"} — status должен быть

DELETE /v1/api/users/{id} 4 бага
Работает без API-ключа

Что сейчас: DELETE /v1/api/users/{id} выполняется вообще без заголовка X-Fix-Bug — даже без ключа приходит 200 {"message": "User deleted successfully"}

Что должно быть: 401 Unauthorized без валидного API-ключа

// Запрос без заголовка X-Fix-Bug
// Сейчас: 200 {"message":"User deleted successfully"}
// Должно быть: 401 {"message":"API key is required in X-Fix-Bug header"}
SELECT вместо DELETE — не удаляет

Что сейчас: Не удаляет пользователя — только читает и возвращает сообщение

Что должно быть: Должен выполнять DELETE и удалять запись

1. DELETE /v1/api/users/1 → 200 {"message":"User deleted successfully"}
2. GET /v1/api/users/2 → пользователь всё ещё существует (id-1: вернёт id=1)
200 вместо 404 для несуществующего id

Что сейчас: DELETE /v1/api/users/999999 → 200 {"message": "User deleted successfully"}

Что должно быть: 404 User not found

В ответе нет данных удалённого пользователя

Что сейчас: Возвращает только message, без id и данных пользователя

Что должно быть: Полные данные пользователя в ответе

GET /v1/api/passes 3 бага
Нет заголовков безопасности, Cache-Control: public

Что сейчас: Ответ без X-Content-Type-Options: nosniff и X-Frame-Options: DENY, вместо no-cache стоит Cache-Control: public

Что должно быть: Заголовки безопасности на каждом ответе + Cache-Control: no-cache

Игнорирование параметров limit и offset (пагинация)

Что сейчас: Параметры ?limit=X&offset=Y игнорируются — всегда возвращается полный список пропусков

Что должно быть: Поддерживать SQL пагинацию через LIMIT и OFFSET

// GET /v1/api/passes?limit=2&offset=3
// Сейчас: все пропуски (пагинация игнорируется)
// Должно быть: только 2 пропуска, начиная с 4-го
Неконсистентный формат даты (без Z и миллисекунд)

Что сейчас: Даты возвращаются в формате YYYY-MM-DDTHH:mm:ss (без .000 и Z)

Что должно быть: Полный ISO-таймстамп с миллисекундами и суффиксом Z

// Сейчас: "2026-06-16T15:49:19"
// Должно быть: "2026-06-16T15:49:19.000Z"
POST /v1/api/passes 5 багов
Пропуск на 60 часов вместо 1 часа

Что сейчас: expires_at = NOW() + 60 hours

Что должно быть: expires_at = NOW() + 1 hour

// Сейчас: пропуск действует 60 часов
// Должно быть: пропуск действует 1 час
Нет проверки существования user_id

Что сейчас: Создаёт пропуск для любого user_id, даже несуществующего

Что должно быть: 404 если пользователь не найден

Разрешает дублирование активных пропусков

Что сейчас: Можно создать сколько угодно активных пропусков для одного user_id

Что должно быть: У кандидата может быть только 1 активный пропуск (400 если уже есть)

Выдача пропуска несовершеннолетнему

Что сейчас: Создаёт пропуск для кандидатов со статусом minor (возраст < 18)

Что должно быть: Блокировать с 400 — скрининг для несовершеннолетних запрещён

Неконсистентный passcode (lowercase/underscore)

Что сейчас: Passcode генерируется как scr_7b93fcaa (нижний регистр, подчёркивание)

Что должно быть: SCR-7B93FCAA (верхний регистр, дефис)

// Сейчас: "scr_7b93fcaa"
// Должно быть: "SCR-7B93FCAA"
GET /v1/api/passes/user/{user_id} 2 бага
Нет заголовков безопасности, Cache-Control: public

Что сейчас: Ответ без X-Content-Type-Options: nosniff и X-Frame-Options: DENY, вместо no-cache стоит Cache-Control: public

Что должно быть: Заголовки безопасности на каждом ответе + Cache-Control: no-cache

Игнорирует параметр user_id

Что сейчас: Игнорирует user_id и возвращает только ПЕРВЫЙ пропуск из всех пропусков ключа (не отфильтрованный и не полный список)

Что должно быть: Фильтровать и по user_id, и по api_key

// GET /v1/api/passes/user/99999
// Сейчас: первый пропуск владельца ключа (user_id игнорируется)
// Должно быть: пропуск только для user_id=99999 или 404
PATCH /v1/api/passes/{id} 6 багов
Нет заголовков безопасности, Cache-Control: public

Что сейчас: Ответ без X-Content-Type-Options: nosniff и X-Frame-Options: DENY, вместо no-cache стоит Cache-Control: public

Что должно быть: Заголовки безопасности на каждом ответе + Cache-Control: no-cache

Принимает любой статус

Что сейчас: Принимает "cancelled", "pending", "banana" и любые другие значения

Что должно быть: Только "active" или "expired", иначе 400

updated_at не обновляется

Что сейчас: updated_at остаётся равным issued_at

Что должно быть: updated_at должен обновляться на NOW() при каждом изменении

Обновление неизменяемых полей (user_id, pass_code)

Что сейчас: PATCH позволяет менять user_id и pass_code

Что должно быть: Эти поля иммутабельны — попытка их изменить → 400

// PATCH /v1/api/passes/1 {"pass_code": "hacked"}
// Сейчас: 200, pass_code изменён
// Должно быть: 400 "Cannot update immutable fields"
Воскрешение просроченного пропуска

Что сейчас: Просроченный пропуск (expires_at < NOW()) можно перевести обратно в active

Что должно быть: Смена статуса просроченного пропуска блокируется с 400

Обход лимита активных пропусков

Что сейчас: Можно через PATCH активировать второй пропуск, если у кандидата уже есть active

Что должно быть: Блокировать 400 при попытке двойной активации

DELETE /v1/api/passes/{id} 4 бага
Нет заголовков безопасности, Cache-Control: public

Что сейчас: Ответ без X-Content-Type-Options: nosniff и X-Frame-Options: DENY, вместо no-cache стоит Cache-Control: public

Что должно быть: Заголовки безопасности на каждом ответе + Cache-Control: no-cache

200 даже если пропуск не найден

Что сейчас: DELETE /v1/api/passes/99999 → 200 {"message": "Deleted"}

Что должно быть: 404 если пропуск не найден

Не фильтрует по api_key

Что сейчас: Любой авторизованный пользователь может удалить чужой пропуск

Что должно быть: Фильтр по api_key

Разрешает удаление активного пропуска

Что сейчас: Можно удалить пропуск в статусе active

Что должно быть: Сначала нужно перевести в expired, потом удалять

Maintenance Service 3 бага
setTimeout вместо setInterval — 1 запуск

Файл: src/services/maintenanceService.js

Что сейчас: setTimeout(cron, interval) — очистка срабатывает 1 раз и больше не повторяется

Что должно быть: setInterval(cron, interval) — периодический запуск

Опечатка CLEANP вместо CLEANUP

Файл: src/services/maintenanceService.js

Что сейчас: Читает process.env.CLEANP_INTERVAL (пропущена U)

Что должно быть: process.env.CLEANUP_INTERVAL

Условие очистки: +30 минут к сроку

Файл: src/services/maintenanceService.js

Что сейчас: Удаляет пропуска, срок которых истёк 30+ минут назад

Что должно быть: Удалять все просроченные (expires_at < NOW())

// Сейчас: WHERE expires_at < NOW() - INTERVAL '30 minutes'
// Должно: WHERE expires_at < NOW()
46 багов. Все в v1. v2 — эталонная версия без багов.