From 1209dfe48da9625429795a1016e4ce2102c30563 Mon Sep 17 00:00:00 2001 From: "github-classroom[bot]" <66690702+github-classroom[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 19:09:59 +0000 Subject: [PATCH] Initial commit --- .github/workflows/test.yaml | 72 +++ README.md | 238 +++++++++ solution/.dockerignore | 5 + solution/.gitignore | 2 + solution/Dockerfile | 12 + solution/app.py | 10 + solution/requirements.txt | 7 + tests/init-database.sh | 265 +++++++++++ tests/openapi.yml | 926 ++++++++++++++++++++++++++++++++++++ tests/public-tests.json | 677 ++++++++++++++++++++++++++ 10 files changed, 2214 insertions(+) create mode 100644 .github/workflows/test.yaml create mode 100644 README.md create mode 100644 solution/.dockerignore create mode 100644 solution/.gitignore create mode 100644 solution/Dockerfile create mode 100644 solution/app.py create mode 100644 solution/requirements.txt create mode 100644 tests/init-database.sh create mode 100644 tests/openapi.yml create mode 100644 tests/public-tests.json diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..84c9065 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,72 @@ +name: Run tests + +on: deployment + +permissions: + contents: read + packages: write + +jobs: + build: + name: Build + runs-on: cu-backend + timeout-minutes: 10 + if: github.actor != 'github-classroom[bot]' + container: + image: gcr.io/kaniko-project/executor:debug + steps: + - name: Login to ghcr.io + run: > + echo "{\"auths\": {\"ghcr.io\": {\"auth\": \"$(echo -n "$AUTH" | base64 -w 0)\"}}}" + > /kaniko/.docker/config.json + env: + AUTH: "${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}" + + - name: Build solution image + run: > + /kaniko/executor + --context="${{ github.repositoryUrl }}#refs/heads/main#${{ github.sha }}" + --context-sub-path=solution + --destination="$(echo -n "$REPO:run-${{ github.run_id }}" | tr '[:upper:]' '[:lower:]')" + --destination="$(echo -n "$REPO:latest" | tr '[:upper:]' '[:lower:]')" + --label org.opencontainers.image.source=https://github.com/${{ github.repository }} + env: + GIT_USERNAME: kaniko + GIT_PASSWORD: "${{ secrets.GITHUB_TOKEN }}" + REPO: "ghcr.io/${{ github.repository }}" + + tests: + name: Tests + runs-on: ubuntu-22.04 + needs: build + timeout-minutes: 10 + if: github.actor != 'github-classroom[bot]' + steps: + - uses: Central-University-IT/setup-test-backend@v1 + + - uses: docker/login-action@v3.0.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - run: > + IMAGE_SOLUTION="$(echo "ghcr.io/${{ github.repository }}:run-${{ github.run_id }}" | tr '[:upper:]' '[:lower:]')" + IMAGE_POSTGRES="$(echo "ghcr.io/${{ github.repository_owner }}/postgres:16.1-alpine3.19" | tr '[:upper:]' '[:lower:]')" + /usr/local/bin/checker + continue-on-error: true + + - uses: actions/upload-artifact@v4.0.0 + with: + name: result + path: ./result.json + if-no-files-found: error + compression-level: 0 + + - uses: bots-house/ghcr-delete-image-action@v1.1.0 + continue-on-error: true + with: + owner: ${{ github.repository_owner }} + name: ${{ github.event.repository.name }} + token: ${{ secrets.GITHUB_TOKEN }} + tag: run-${{ github.run_id }} diff --git a/README.md b/README.md new file mode 100644 index 0000000..66ec4bd --- /dev/null +++ b/README.md @@ -0,0 +1,238 @@ +# PROD v2: Pulse Backend + +Ваши коллеги разрабатывают социальную сеть для инвесторов. Уже совсем скоро им нужно сдавать проект, а вся команда бэкенд-разработчиков ушла в отпуск. + +Кто-то проболтался о том, что вы знакомы с Git, HTTP, Docker, PostgreSQL и e2e-тестами. Это именно то, что нужно ребятам (а если с чем-то не знакомы, они рассчитывают на ваши навыки поиска информации)! Помогите коллегам успеть завершить проект до дедлайна и реализуйте новое HTTP API :) + +Результатом выполнения данного задания является Github репозиторий с исходным кодом приложения (директория `solution`). + +## Про приложение + +Приложение должно представлять из себя HTTP сервер, реализующий необходимое [API](./tests/openapi.yml). В наследие от предыдущей команды вам достался инстанс PostgreSQL, который необходимо использовать для хранения данных. + +Приложение должно конфигурироваться через переменные окружения: + +- `SERVER_ADDRESS` — хост и порт, которые будет _слушать_ запущенный HTTP сервер. Например, `0.0.0.0:8080`. + +- `SERVER_PORT` — содержит порт; запущенный сервер должен слушать IP `0.0.0.0` и указанный порт. Используйте эту переменную, если вам не подошел формат данных в переменной `SERVER_ADDRESS` (переданный параметры равнозначны). + +- `POSTGRES_CONN` — DSN- **или** URL-строка для подключения к PostgreSQL. С форматами можно ознакомиться [здесь](https://pkg.go.dev/github.com/jackc/pgx/v4/pgxpool@v4.11.0#ParseConfig). +Пример: `user=postgres password=postgres host=localhost port=5432 dbname=pulse sslmode=verify-ca pool_max_conns=10`. + +Учитывая современные реалии, приложение будет запускаться через Docker контейнер. В репозитории присутствует Dockerfile, с помощью которого будет собираться образ приложения. + +Список используемых зависимостей не ограничен, однако вы должны убедиться, что необходимые зависимости загружаются и подключаются в Dockerfile. + +Описание API находится ниже, но если вы хотите ознакомиться с точными требованиями, не стесняйтесь использовать Swagger и предоставленную [Open API спецификацию](./tests/openapi.yml). + +Тестирование решения происходит с помощью Github CI. Для отправки решения на тестирование необходимо обновить исходный код вашего репозитория на Github (git commit & git push). + +**Вы можете редактировать файлы в директории `solution`. Если в репозитории содержатся изменения в других файлах, решение не будет принято.** + +## Оценивание + +Для получения баллов за группу тестов решение должно пройти все тесты из данной группы. + +Группы тестов могут зависеть друг от друга. Если группа B зависит от группы B, при тестировании группы B могут использоваться эндпоинты, участвовавшие в тестировании группы A. Это свойство транзитивно! + +| Название группы | Описание | Баллы | От каких групп зависит | +|------------------|------------------------------------|-------|------------------------| +| 01/ping | Успешный ответ на `/api/ping`. | 1 | | +| 02/countries | Получение и фильтрация стран. | 6 | | +| 03/auth/register | Регистрация пользователей. | 6 | - 02/countries | +| 04/auth/sign-in | Аутентификация и получение токена. | 7 | - 03/auth/register | +| 05/me | Получение и редактирование собственного профиля. | 8 | - 04/auth/sign-in | +| 06/profiles | Получение профиля по логину. | 5 | - 04/auth/sign-in | +| 07/password | Изменение пароля. | 7 | - 05/me | +| 08/friends | Друзья! | 12 | - 04/auth/sign-in | +| 09/posts/publish | Публикация поста и получение по ID. | 12 | - 05/me
- 08/friends | +| 10/posts/feed | Получение новостной ленты. | 16 | - 09/posts/publish | +| 11/posts/likes | Лайки и дизлайки. | 20 | - 10/posts/feed | + +В спорных ситуациях будет оцениваться качество кода. + +На данный момент в Github CI тестирование производится на публичном наборе тестов. Данные тесты помогают провалидировать минимальную логику приложения, **но не гарантируют прохождения финальных тестов**. + +## Группы тестов + +### Общие требования + +**У всех эндпоинтов есть префикс `/api`.** + +Обратите внимание, возврат успешного ответа на `GET /api/ping` является **обязательным условием для начала тестирования приложения**. + +Поступающие запросы и возвращаемые ответы должны соответствовать структуре и требованиям, описанным в [Open API](./tests/openapi.yml) спецификации. Обращайте внимание на ожидаемые status code, ограничения по длине и разрешенные символы в строках. + +### 01/ping + +Достаточно реализовать возврат успешного ответа (с кодом `200`) на запрос `GET /api/ping`. Содержимое тела ответа при этом не валидируется, можно возвращать `"ok"`. + +Данная логика является блокирующей для всех остальных групп тестов. + +### 02/countries + +Как и в любом большом проекте у нас есть собственный словарь стран, который используется при регистрации пользователей и может учитываться рекомендательными системами и системой локализации контента. + +Про каждую страну известны следующие данные: +```json +{ + "name": "полное название", + "alpha2": "двухбуквенный код страны", + "alpha3": "трехбуквенный код страны", + "region": "географический регион" +} +``` + +Необходимо реализовать следующие эндпоинты: + +- `GET /countries` — получить список доступных стран, доступна фильтрация по регионам. + +- `GET /countries/{alpha2}` — получить страну по её уникальному двухбуквенному коду. + +Самое интересное: **для получения списка стран необходимо использовать предоставленную СУБД PostgreSQL**. + +Данные находятся в таблице `countries`, которая имеет следующее определение: +```sql +CREATE TABLE countries ( + id SERIAL PRIMARY KEY, + name TEXT, + alpha2 TEXT, + alpha3 TEXT, + region TEXT +); + +INSERT INTO countries (name, alpha2, alpha3, region) VALUES + ('Åland Islands','AX','ALA','Europe'), + ('Albania','AL','ALB','Europe'), + ...; +``` + +При тестировании в Github CI база данных уже будет содержать нужный набор данных. Обратите внимание, данные в публичном и закрытом наборе тестов могут отличаться. + +### 03/auth/register + +Эндпоинт `/auth/register` используется для первичной регистрации пользователей. + +Сервер должен поддерживать базу данных пользователей, валидировать запросы и не допускать наличия пользователей с эквивалентными регистрационными данными. + +Не храните пароль пользователей в [открытом виде](https://security.stackexchange.com/questions/36833/why-should-i-hash-passwords), используется хеширование (например, bcrypt). + +### 04/auth/sign-in + +Эндпоинт `/auth/sign-in` предназначен для аутентификации пользователя по логину и паролю и генерации сессионного токена, +который в дальнейшем будет использоваться для генерации запросов. + +Генерируемый токен должен уникально идентифицировать пользователя и быть сложным для подбора (можно использовать JWT). + +Данный токен в дальнейшем будет передаваться пользователем в заголовке `Authorization: Bearer {token}`, и приложение должно уметь понять, какой пользователь хочет сделать запрос. + +Временно будем считать, что время действия токена ограничено одним часом. + +### 05/me + +Эндпоинт `/me/profile` используется для получения и редактирования параметров собственного профиля пользователя. Действие зависит от указанного метода (`GET` и `PATCH`). + +Сервер должен идентифицировать пользователя по переданному токену. Значение токена будет подставляться в заголовок `Authorization` в формате `Bearer {token}`. Например, `Authorization: Bearer $deddz$@pp...`. + +В запросе на редактирование профиля передаются значения только тех полей, которые необходимо обновить. + +### 06/profiles + +Эндпоинт `/profiles/{login}` позволяет получить профиль другого пользователя по логину. + +Обратите внимание, в некоторых ситуациях профиль пользователя получить нельзя (в зависимости от значения параметра `isPublic`). Для получения дополнительных деталей ознакомьтесь со спецификацией API. + +### 07/password + +С помощью `/me/updatePassword` у пользователя появляется возможность изменить пароль от своего аккаунта. + +После изменения пароля: + +- Аутентификация со старым паролем становится невозможной. + +- Все ранее выпущенные токены должны быть отозваны. + +После успешной смены пароля при попытке получить свой профиль со старым токеном пользователь должен получать ошибку. + +### 08/friends + +В приложении появляется возможность добавлять и удалять других пользователей в друзья. +И конечно же можно посмотреть список своих друзейЙ + +Чтобы не нагружать сервера и клиенты слишком сильно, в запросах на получение списка друзей используется пагинация. +С помощью параметров `offset` и `limit` можно "постранично" получить весь список друзей, запрашивая данные порционно. + +Вам потребуется запоминать дату и время последнего добавления в друзья для корректно сортировки и реализации пагинации. + +### 09/posts/publish + +В данной группе проверяется возможность создавать публикации со стороны пользователей. + +Сервер должен генерировать уникальные идентификаторы и запоминать время создания публикаций. + +У пользователя есть доступ к своим постам, постам пользователей с публичным профилем и постам других пользователей, которые добавили данного пользователя в друзья. + +В данной группе не проверяются поля с лайками и дизлайками. + +### 10/posts/feed + +У пользователей появилась возможность смотреть новостную ленту со своими и чужими постами. Вместе с пагинацией. + +В данной группе не проверяются поля с лайками и дизлайками. + +### 11/posts/likes + +Самое интересное: пользователи могут поставить лайк и дизлайк публикации, к которой у них есть доступ. + +Всегда запоминается последняя реакция пользователя. Если пользователь поставил лайк два раза подряд, эффект лайка остается. + +В полях `likesCount` и `dislikesCount` необходимо отразить уникальное число лайков и дизлайков.` + +## Тестирование + +Для тестирования решения отразите ваши изменения в Github репозитории. Разрешено изменять только директорию `solution`, иначе тесты не будут запущены. + +### Тестирование в CI + +Для тестирования решений используется [Github CI](https://docs.github.com/en/actions/automating-builds-and-tests/about-continuous-integration). При отправке новых изменений в репозиторий на Github активируется тестирующий пайплайн. + +Пайплайн состоит из двух этапов: +- Сборка Docker образа с вашим приложением (на основании исходного кода репозитория и Dockerfile). + +- Запуск тестов. Для каждой группы тестов + - запускаются Docker контейнеры с вашим приложением и PostgreSQL; + + - тестирующая система дожидается успешного (`200`) ответа на `GET /api/ping`, на это дается не более 10 секунд; + + - приложение считается запущенным и начинается запуск HTTP тестов из тестируемой группы. + +Проверьте, что ваше приложение готово запускать HTTP сервер на адресе, переданном в переменной окружения `SERVER_ADDRESS`. **В качестве хоста (IP) передается `0.0.0.0`, а не localhost или 127.0.0.1. Это важно!** + +Также проверьте локально, что Docker образ с вашим приложением собирается (выполните `docker build .` в директории `solution`). + +Существующие ограничения: + +- Решению выделяется 3 vCPU, 6 GB RAM и до 1 GB дискового пространства (не учитывая PostgreSQL). + +- В рамках тестирования ваше приложение не должно завершать работу (помните о защите от Exception, panic и прочих причинах аварийного завершения). + +- Сетевое взаимодействие разрешено только с PostgreSQL и тестирующей системой. Обращаться к сторонним ресурсам по сети нельзя. + +Во вкладке Actions можно найти лог тестирования, в котором будут отражены результаты запуска тестов на публичном наборе тестов. + +Прохождение публичного набора тестов не дает гарантию прохождения финальных тестов. + +### Локальное тестирование + +Для локального тестирования вы можете пользоваться [Postman](https://www.postman.com/). В директории проекта кто-то из коллег оставил [Postman коллекцию](./tests/public-tests.json) с публичными тестами для API. Не забудьте переопределить `base_url` в переменных коллекции. + +Для инициализации СУБД PostgreSQL можно использовать [заранее подготовленный скрипт](./tests/init-database.sh), из которого можно выудить SQL запросы. + +Чтобы локальное тестирование было максимально приближенным к тестированию в CI, мы рекомендуем запускать PostgreSQL и ваше приложение в Docker контейнерах (связанных одной сетью). + +## Changelog + +Как это часто бывает, заказчики проекта вносят правки в требования! +Ваших коллег ждала та же участь... Заказчики просили передать, что они будут стараться делать как можно меньше изменений. + +Но удача на нашей стороне! Коллеги будут фиксировать все правки в данном документе и вести ченджлог изменений. \ No newline at end of file diff --git a/solution/.dockerignore b/solution/.dockerignore new file mode 100644 index 0000000..d3ae326 --- /dev/null +++ b/solution/.dockerignore @@ -0,0 +1,5 @@ +.dockerignore +Dockerfile +README.md +.venv/ +__pycache__/ diff --git a/solution/.gitignore b/solution/.gitignore new file mode 100644 index 0000000..808386f --- /dev/null +++ b/solution/.gitignore @@ -0,0 +1,2 @@ +.venv/ +__pycache__/ \ No newline at end of file diff --git a/solution/Dockerfile b/solution/Dockerfile new file mode 100644 index 0000000..c763543 --- /dev/null +++ b/solution/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.12.1-alpine3.19 + +WORKDIR /app + +COPY requirements.txt requirements.txt +RUN pip3 install -r requirements.txt + +COPY . . + +ENV SERVER_PORT=8080 + +CMD ["sh", "-c", "exec python3 -m flask run --host=0.0.0.0 --port=$SERVER_PORT"] \ No newline at end of file diff --git a/solution/app.py b/solution/app.py new file mode 100644 index 0000000..2ae2cbf --- /dev/null +++ b/solution/app.py @@ -0,0 +1,10 @@ +from flask import Flask, request, jsonify + +app = Flask(__name__) + +@app.route('/api/ping', methods=['GET']) +def send(): + return jsonify({"status": "ok"}), 200 + +if __name__ == "__main__": + app.run() diff --git a/solution/requirements.txt b/solution/requirements.txt new file mode 100644 index 0000000..3bd0d17 --- /dev/null +++ b/solution/requirements.txt @@ -0,0 +1,7 @@ +blinker==1.7.0 +click==8.1.7 +Flask==3.0.1 +itsdangerous==2.1.2 +Jinja2==3.1.3 +MarkupSafe==2.1.4 +Werkzeug==3.0.1 diff --git a/tests/init-database.sh b/tests/init-database.sh new file mode 100644 index 0000000..06c9da1 --- /dev/null +++ b/tests/init-database.sh @@ -0,0 +1,265 @@ +#!/bin/bash +set -e + +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL + CREATE TABLE countries ( + id SERIAL PRIMARY KEY, + name TEXT, + alpha2 TEXT, + alpha3 TEXT, + region TEXT + ); + + -- https://github.com/lukes/ISO-3166-Countries-with-Regional-Codes/blob/master/all/all.json + -- cat tests/countries.json| jq '.[] | [.name, ."alpha-2", ."alpha-3", .region] | @csv' -r | tr "'" " " | tr '"' "'" | awk -F "\n" '{print "("$1"),"}' + INSERT INTO countries (name, alpha2, alpha3, region) VALUES + ('Afghanistan','AF','AFG','Asia'), + ('Åland Islands','AX','ALA','Europe'), + ('Albania','AL','ALB','Europe'), + ('Algeria','DZ','DZA','Africa'), + ('American Samoa','AS','ASM','Oceania'), + ('Andorra','AD','AND','Europe'), + ('Angola','AO','AGO','Africa'), + ('Anguilla','AI','AIA','Americas'), + ('Antarctica','AQ','ATA',''), + ('Antigua and Barbuda','AG','ATG','Americas'), + ('Argentina','AR','ARG','Americas'), + ('Armenia','AM','ARM','Asia'), + ('Aruba','AW','ABW','Americas'), + ('Australia','AU','AUS','Oceania'), + ('Austria','AT','AUT','Europe'), + ('Azerbaijan','AZ','AZE','Asia'), + ('Bahamas','BS','BHS','Americas'), + ('Bahrain','BH','BHR','Asia'), + ('Bangladesh','BD','BGD','Asia'), + ('Barbados','BB','BRB','Americas'), + ('Belarus','BY','BLR','Europe'), + ('Belgium','BE','BEL','Europe'), + ('Belize','BZ','BLZ','Americas'), + ('Benin','BJ','BEN','Africa'), + ('Bermuda','BM','BMU','Americas'), + ('Bhutan','BT','BTN','Asia'), + ('Bolivia (Plurinational State of)','BO','BOL','Americas'), + ('Bonaire, Sint Eustatius and Saba','BQ','BES','Americas'), + ('Bosnia and Herzegovina','BA','BIH','Europe'), + ('Botswana','BW','BWA','Africa'), + ('Bouvet Island','BV','BVT','Americas'), + ('Brazil','BR','BRA','Americas'), + ('British Indian Ocean Territory','IO','IOT','Africa'), + ('Brunei Darussalam','BN','BRN','Asia'), + ('Bulgaria','BG','BGR','Europe'), + ('Burkina Faso','BF','BFA','Africa'), + ('Burundi','BI','BDI','Africa'), + ('Cabo Verde','CV','CPV','Africa'), + ('Cambodia','KH','KHM','Asia'), + ('Cameroon','CM','CMR','Africa'), + ('Canada','CA','CAN','Americas'), + ('Cayman Islands','KY','CYM','Americas'), + ('Central African Republic','CF','CAF','Africa'), + ('Chad','TD','TCD','Africa'), + ('Chile','CL','CHL','Americas'), + ('China','CN','CHN','Asia'), + ('Christmas Island','CX','CXR','Oceania'), + ('Cocos (Keeling) Islands','CC','CCK','Oceania'), + ('Colombia','CO','COL','Americas'), + ('Comoros','KM','COM','Africa'), + ('Congo','CG','COG','Africa'), + ('Congo, Democratic Republic of the','CD','COD','Africa'), + ('Cook Islands','CK','COK','Oceania'), + ('Costa Rica','CR','CRI','Americas'), + ('Côte d Ivoire','CI','CIV','Africa'), + ('Croatia','HR','HRV','Europe'), + ('Cuba','CU','CUB','Americas'), + ('Curaçao','CW','CUW','Americas'), + ('Cyprus','CY','CYP','Asia'), + ('Czechia','CZ','CZE','Europe'), + ('Denmark','DK','DNK','Europe'), + ('Djibouti','DJ','DJI','Africa'), + ('Dominica','DM','DMA','Americas'), + ('Dominican Republic','DO','DOM','Americas'), + ('Ecuador','EC','ECU','Americas'), + ('Egypt','EG','EGY','Africa'), + ('El Salvador','SV','SLV','Americas'), + ('Equatorial Guinea','GQ','GNQ','Africa'), + ('Eritrea','ER','ERI','Africa'), + ('Estonia','EE','EST','Europe'), + ('Eswatini','SZ','SWZ','Africa'), + ('Ethiopia','ET','ETH','Africa'), + ('Falkland Islands (Malvinas)','FK','FLK','Americas'), + ('Faroe Islands','FO','FRO','Europe'), + ('Fiji','FJ','FJI','Oceania'), + ('Finland','FI','FIN','Europe'), + ('France','FR','FRA','Europe'), + ('French Guiana','GF','GUF','Americas'), + ('French Polynesia','PF','PYF','Oceania'), + ('French Southern Territories','TF','ATF','Africa'), + ('Gabon','GA','GAB','Africa'), + ('Gambia','GM','GMB','Africa'), + ('Georgia','GE','GEO','Asia'), + ('Germany','DE','DEU','Europe'), + ('Ghana','GH','GHA','Africa'), + ('Gibraltar','GI','GIB','Europe'), + ('Greece','GR','GRC','Europe'), + ('Greenland','GL','GRL','Americas'), + ('Grenada','GD','GRD','Americas'), + ('Guadeloupe','GP','GLP','Americas'), + ('Guam','GU','GUM','Oceania'), + ('Guatemala','GT','GTM','Americas'), + ('Guernsey','GG','GGY','Europe'), + ('Guinea','GN','GIN','Africa'), + ('Guinea-Bissau','GW','GNB','Africa'), + ('Guyana','GY','GUY','Americas'), + ('Haiti','HT','HTI','Americas'), + ('Heard Island and McDonald Islands','HM','HMD','Oceania'), + ('Holy See','VA','VAT','Europe'), + ('Honduras','HN','HND','Americas'), + ('Hong Kong','HK','HKG','Asia'), + ('Hungary','HU','HUN','Europe'), + ('Iceland','IS','ISL','Europe'), + ('India','IN','IND','Asia'), + ('Indonesia','ID','IDN','Asia'), + ('Iran (Islamic Republic of)','IR','IRN','Asia'), + ('Iraq','IQ','IRQ','Asia'), + ('Ireland','IE','IRL','Europe'), + ('Isle of Man','IM','IMN','Europe'), + ('Israel','IL','ISR','Asia'), + ('Italy','IT','ITA','Europe'), + ('Jamaica','JM','JAM','Americas'), + ('Japan','JP','JPN','Asia'), + ('Jersey','JE','JEY','Europe'), + ('Jordan','JO','JOR','Asia'), + ('Kazakhstan','KZ','KAZ','Asia'), + ('Kenya','KE','KEN','Africa'), + ('Kiribati','KI','KIR','Oceania'), + ('Korea (Democratic People s Republic of)','KP','PRK','Asia'), + ('Korea, Republic of','KR','KOR','Asia'), + ('Kuwait','KW','KWT','Asia'), + ('Kyrgyzstan','KG','KGZ','Asia'), + ('Lao People s Democratic Republic','LA','LAO','Asia'), + ('Latvia','LV','LVA','Europe'), + ('Lebanon','LB','LBN','Asia'), + ('Lesotho','LS','LSO','Africa'), + ('Liberia','LR','LBR','Africa'), + ('Libya','LY','LBY','Africa'), + ('Liechtenstein','LI','LIE','Europe'), + ('Lithuania','LT','LTU','Europe'), + ('Luxembourg','LU','LUX','Europe'), + ('Macao','MO','MAC','Asia'), + ('Madagascar','MG','MDG','Africa'), + ('Malawi','MW','MWI','Africa'), + ('Malaysia','MY','MYS','Asia'), + ('Maldives','MV','MDV','Asia'), + ('Mali','ML','MLI','Africa'), + ('Malta','MT','MLT','Europe'), + ('Marshall Islands','MH','MHL','Oceania'), + ('Martinique','MQ','MTQ','Americas'), + ('Mauritania','MR','MRT','Africa'), + ('Mauritius','MU','MUS','Africa'), + ('Mayotte','YT','MYT','Africa'), + ('Mexico','MX','MEX','Americas'), + ('Micronesia (Federated States of)','FM','FSM','Oceania'), + ('Moldova, Republic of','MD','MDA','Europe'), + ('Monaco','MC','MCO','Europe'), + ('Mongolia','MN','MNG','Asia'), + ('Montenegro','ME','MNE','Europe'), + ('Montserrat','MS','MSR','Americas'), + ('Morocco','MA','MAR','Africa'), + ('Mozambique','MZ','MOZ','Africa'), + ('Myanmar','MM','MMR','Asia'), + ('Namibia','NA','NAM','Africa'), + ('Nauru','NR','NRU','Oceania'), + ('Nepal','NP','NPL','Asia'), + ('Netherlands','NL','NLD','Europe'), + ('New Caledonia','NC','NCL','Oceania'), + ('New Zealand','NZ','NZL','Oceania'), + ('Nicaragua','NI','NIC','Americas'), + ('Niger','NE','NER','Africa'), + ('Nigeria','NG','NGA','Africa'), + ('Niue','NU','NIU','Oceania'), + ('Norfolk Island','NF','NFK','Oceania'), + ('North Macedonia','MK','MKD','Europe'), + ('Northern Mariana Islands','MP','MNP','Oceania'), + ('Norway','NO','NOR','Europe'), + ('Oman','OM','OMN','Asia'), + ('Pakistan','PK','PAK','Asia'), + ('Palau','PW','PLW','Oceania'), + ('Palestine, State of','PS','PSE','Asia'), + ('Panama','PA','PAN','Americas'), + ('Papua New Guinea','PG','PNG','Oceania'), + ('Paraguay','PY','PRY','Americas'), + ('Peru','PE','PER','Americas'), + ('Philippines','PH','PHL','Asia'), + ('Pitcairn','PN','PCN','Oceania'), + ('Poland','PL','POL','Europe'), + ('Portugal','PT','PRT','Europe'), + ('Puerto Rico','PR','PRI','Americas'), + ('Qatar','QA','QAT','Asia'), + ('Réunion','RE','REU','Africa'), + ('Romania','RO','ROU','Europe'), + ('Russian Federation','RU','RUS','Europe'), + ('Rwanda','RW','RWA','Africa'), + ('Saint Barthélemy','BL','BLM','Americas'), + ('Saint Helena, Ascension and Tristan da Cunha','SH','SHN','Africa'), + ('Saint Kitts and Nevis','KN','KNA','Americas'), + ('Saint Lucia','LC','LCA','Americas'), + ('Saint Martin (French part)','MF','MAF','Americas'), + ('Saint Pierre and Miquelon','PM','SPM','Americas'), + ('Saint Vincent and the Grenadines','VC','VCT','Americas'), + ('Samoa','WS','WSM','Oceania'), + ('San Marino','SM','SMR','Europe'), + ('Sao Tome and Principe','ST','STP','Africa'), + ('Saudi Arabia','SA','SAU','Asia'), + ('Senegal','SN','SEN','Africa'), + ('Serbia','RS','SRB','Europe'), + ('Seychelles','SC','SYC','Africa'), + ('Sierra Leone','SL','SLE','Africa'), + ('Singapore','SG','SGP','Asia'), + ('Sint Maarten (Dutch part)','SX','SXM','Americas'), + ('Slovakia','SK','SVK','Europe'), + ('Slovenia','SI','SVN','Europe'), + ('Solomon Islands','SB','SLB','Oceania'), + ('Somalia','SO','SOM','Africa'), + ('South Africa','ZA','ZAF','Africa'), + ('South Georgia and the South Sandwich Islands','GS','SGS','Americas'), + ('South Sudan','SS','SSD','Africa'), + ('Spain','ES','ESP','Europe'), + ('Sri Lanka','LK','LKA','Asia'), + ('Sudan','SD','SDN','Africa'), + ('Suriname','SR','SUR','Americas'), + ('Svalbard and Jan Mayen','SJ','SJM','Europe'), + ('Sweden','SE','SWE','Europe'), + ('Switzerland','CH','CHE','Europe'), + ('Syrian Arab Republic','SY','SYR','Asia'), + ('Taiwan, Province of China','TW','TWN','Asia'), + ('Tajikistan','TJ','TJK','Asia'), + ('Tanzania, United Republic of','TZ','TZA','Africa'), + ('Thailand','TH','THA','Asia'), + ('Timor-Leste','TL','TLS','Asia'), + ('Togo','TG','TGO','Africa'), + ('Tokelau','TK','TKL','Oceania'), + ('Tonga','TO','TON','Oceania'), + ('Trinidad and Tobago','TT','TTO','Americas'), + ('Tunisia','TN','TUN','Africa'), + ('Turkey','TR','TUR','Asia'), + ('Turkmenistan','TM','TKM','Asia'), + ('Turks and Caicos Islands','TC','TCA','Americas'), + ('Tuvalu','TV','TUV','Oceania'), + ('Uganda','UG','UGA','Africa'), + ('Ukraine','UA','UKR','Europe'), + ('United Arab Emirates','AE','ARE','Asia'), + ('United Kingdom of Great Britain and Northern Ireland','GB','GBR','Europe'), + ('United States of America','US','USA','Americas'), + ('United States Minor Outlying Islands','UM','UMI','Oceania'), + ('Uruguay','UY','URY','Americas'), + ('Uzbekistan','UZ','UZB','Asia'), + ('Vanuatu','VU','VUT','Oceania'), + ('Venezuela (Bolivarian Republic of)','VE','VEN','Americas'), + ('Viet Nam','VN','VNM','Asia'), + ('Virgin Islands (British)','VG','VGB','Americas'), + ('Virgin Islands (U.S.)','VI','VIR','Americas'), + ('Wallis and Futuna','WF','WLF','Oceania'), + ('Western Sahara','EH','ESH','Africa'), + ('Yemen','YE','YEM','Asia'), + ('Zambia','ZM','ZMB','Africa'), + ('Zimbabwe','ZW','ZWE','Africa'); +EOSQL \ No newline at end of file diff --git a/tests/openapi.yml b/tests/openapi.yml new file mode 100644 index 0000000..a810a68 --- /dev/null +++ b/tests/openapi.yml @@ -0,0 +1,926 @@ +openapi: "3.0.1" +info: + title: Pulse API + version: "1.0" +servers: + - url: http://localhost:8080/v1/api +paths: + /ping: + get: + summary: Проверка сервера на готовность принимать запросы + description: | + Данный эндпоинт позволяет понять, что сервер готов принимать входящие запросы. + + Программа-чекер будет дожидаться первого успешного ответа от сервера на данный эндпоинт, после чего будет запускать проверку тестовый сценариев. + operationId: ping + responses: + "200": + description: | + Если сервер успешно отвечает на данный запрос, считается, что он готов обрабатывать входящие запросы в API. + + Содержимое ответа при этом не валидируется, можно возвращать "ok". + content: + text/plain: + schema: + type: string + example: ok + "500": + description: Если сервер отвечает любым отличным от 200 кодом ответа, считается, что он не готов принимать запросы. + /countries: + get: + summary: Получить список стран + description: | + Получение списка стран с возможной фильтрацией. + + Используется на странице регистрации для предоставления возможности выбора страны, к которой относится пользователь. + + Если никакие из фильтров не переданы, необходимо вернуть все страны. + operationId: listCountries + parameters: + - name: region + description: | + Возвращаемые страны должны относиться только к тем регионам, которые переданы в данном списке. + + Если передан пустой список, считайте, что фильтр по региону отсутствует. + in: query + schema: + type: array + items: + $ref: "#/components/schemas/countryRegion" + responses: + "200": + description: Список стран, соответствующих указанному фильтру. Страны должны быть отсортированы лексикографически по двухбуквенному коду. + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/country" + /countries/{alpha2}: + get: + summary: Получить страну по alpha2 коду + description: | + Получение одной страны по её уникальному двухбуквенному коду. + + Используется для получения информации по определенной стране. + operationId: getCountry + parameters: + - name: alpha2 + description: | + Возвращаемая страна должна иметь указанный alpha2 код. + required: true + in: path + schema: + $ref: "#/components/schemas/countryAlpha2" + responses: + "200": + description: Страна, найденная по указанному коду. + content: + application/json: + schema: + $ref: "#/components/schemas/country" + "404": + description: Страна с указанным кодом не найдена. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + /auth/register: + post: + summary: Регистрация нового пользователя + description: | + Используется для регистрации нового пользователя по логину и паролю. + operationId: authRegister + requestBody: + description: Данные для регистрации пользователя. + required: true + content: + application/json: + schema: + type: object + properties: + login: + $ref: "#/components/schemas/userLogin" + email: + $ref: "#/components/schemas/userEmail" + password: + $ref: "#/components/schemas/userPassword" + countryCode: + $ref: "#/components/schemas/countryAlpha2" + isPublic: + $ref: "#/components/schemas/userIsPublic" + phone: + $ref: "#/components/schemas/userPhone" + image: + $ref: "#/components/schemas/userImage" + required: + - login + - email + - password + - countryCode + - isPublic + responses: + "201": + description: В случае успеха возвращается профиль зарегистрированного пользователя + content: + application/json: + schema: + type: object + properties: + profile: + $ref: "#/components/schemas/userProfile" + required: + - profile + "400": + description: | + Регистрационные данные не соответствуют ожидаемому формату и требованиям. + + Например, данную ошибку необходимо возвращать в следующих ситуациях (это не полный список): + + - Недостаточно "надежный" пароль. + - Страна с указанным кодом не найдена. + - Длина ссылки на аватар пользователя превышает допустимый лимит. + + Для ознакомления с форматом и требованиями к регистрационным данным обратите внимание на описание моделей в Open API спецификации. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "409": + description: | + Нарушено требование на уникальность авторизационных данных пользователей. + + Данный код ответа должен использоваться, если пользователь с таким e-mail, номером телефона или логином уже зарегистрирован. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + /auth/sign-in: + post: + summary: Аутентификация для получения токена + description: | + Процедура аутентификации по логину и паролю позволяет получить токен, который в дальнейшем будет использоваться пользователем для выполнения операций, требующих авторизацию. + + Сервер должен генерировать уникальные токены, имеющие время жизни (в рамках задачи это будет 24 часа). После истечения времени действия токен должен быть недействительным и не может использоваться для аутентификации. + + Токен является уникальным строковым значением с высокой энтропией (злоумышленник не сможет его "подобрать" перебором). При каждой новой аутентификации генерируется новый уникальный токен, который ранее не был использован. Можно использовать JWT. + + В дальнейшем полученный токен будет использоваться для авторизации пользовательских запросов. Значение токена будет подставляться в заголовок `Authorization` в формате `Bearer {token}`. Следовательно, сервер должен уметь идентифицировать пользователя по токену. + operationId: authSignIn + responses: + "200": + description: Успешная аутентификация + content: + application/json: + schema: + type: object + properties: + token: + type: string + description: Сгенерированный токен пользователя + minLength: 20 + example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c + required: + - token + "401": + description: Пользователь с указанным логином и паролем не найден + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + /me/profile: + get: + summary: Получение собственного профиля + description: | + Используется для получения пользователем его собственного профиля. + + Сервер должен идентифицировать пользователя по переданному токену. Значение токена будет подставляться в заголовок `Authorization` в формате `Bearer {token}`. + security: + - bearerAuth: [] + operationId: getMyProfile + responses: + "200": + description: Передан действительный токен, в ответе возвращается профиль пользователя. + content: + application/json: + schema: + $ref: "#/components/schemas/userProfile" + "401": + description: Переданный токен не существует либо некорректен. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + patch: + summary: Редактирование собственного профиля + description: | + Используется для редактирования параметров профиля пользователя. + + Сервер должен идентифицировать пользователя по переданному токену. Значение токена будет подставляться в заголовок `Authorization` в формате `Bearer {token}`. + security: + - bearerAuth: [] + operationId: patchMyProfile + requestBody: + description: | + В теле запроса перечисляются названия параметров, которые необходимо обновить, и новые значения. + + Если значение передано, данное изменение должно быть отражено в профиле пользователя. + Если значение не передано, необходимо оставить прежнее значение параметра. + required: true + content: + application/json: + schema: + type: object + properties: + countryCode: + $ref: "#/components/schemas/countryAlpha2" + isPublic: + $ref: "#/components/schemas/userIsPublic" + phone: + $ref: "#/components/schemas/userPhone" + image: + $ref: "#/components/schemas/userImage" + responses: + "200": + description: Передан действительный токен, в ответе возвращается профиль пользователя с примененными изменениями. + content: + application/json: + schema: + $ref: "#/components/schemas/userProfile" + "401": + description: Переданный токен не существует либо некорректен. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + /profiles/{login}: + get: + summary: Получение профиля пользователя по логину + description: | + Используется для получения профиля другого пользователя по логину. + + Если профиль пользователя публичен (`isPublic: true`), его может получить любой другой пользователь. Иначе профиль пользователя получить нельзя. + + При этом собственный профиль пользователь может получить всегда. Сервер должен идентифицировать пользователя по переданному токену в заголовке `Authorization`. + security: + - bearerAuth: [] + parameters: + - name: login + description: Логин пользователя, чей профиль необходимо получить. + required: true + in: path + schema: + $ref: "#/components/schemas/userLogin" + operationId: getProfile + responses: + "200": + description: Пользователь с указанным логином существует и его профиль может быть получен пользователем, осуществившим запрос. + content: + application/json: + schema: + $ref: "#/components/schemas/userProfile" + "401": + description: Переданный токен не существует либо некорректен. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "403": + description: | + Профиль не может быть получен: либо пользователь с указанным логином не существует, либо у отправителя запроса нет доступа к запрашиваемому профилю. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + /me/updatePassword: + post: + summary: Обновление пароля + description: | + Используется для обновления пароля. + + Сервер должен идентифицировать пользователя по переданному токену. Значение токена будет подставляться в заголовок `Authorization` в формате `Bearer {token}`. + + Важно: после успешного обновления пароля ранее выписанные токены должны быть деактивированы. Как только сервер вернет успешный ответ на данный запрос, пользователь не сможет совершить какие-либо операции с ранее созданными токенами (запросы со старыми токенами должны получать соответствующий ошибочный статус код). + security: + - bearerAuth: [] + operationId: updatePassword + requestBody: + description: | + В теле запроса передается старый и новый пароли. Пароль может быть обновлен только в случае передачи правильного значения старого пароля. + required: true + content: + application/json: + schema: + type: object + properties: + oldPassword: + $ref: "#/components/schemas/userPassword" + newPassword: + $ref: "#/components/schemas/userPassword" + required: + - oldPassword + - newPassword + responses: + "200": + description: Пароль успешно обновлен и ранее выпущенные токены отозваны. + content: + application/json: + schema: + type: object + properties: + status: + type: string + description: Должно принимать значение `ok`. + example: ok + required: + - status + "400": + description: Новый пароль не соответствует требованиям безопасности. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "401": + description: Переданный токен не существует либо некорректен. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "403": + description: Указанный пароль не совпадает с действительным. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + /friends/add: + post: + summary: Добавить пользователя в друзья + description: | + Позволяет добавить другого пользователя к себе в друзья. + + Если указанный пользователь уже добавлен в друзья, верните успешный ответ. + + Сервер должен идентифицировать пользователя по переданному токену. Значение токена будет подставляться в заголовок `Authorization` в формате `Bearer {token}`. + security: + - bearerAuth: [] + operationId: friendsAdd + requestBody: + description: | + В теле запроса передается логин пользователя. + required: true + content: + application/json: + schema: + type: object + properties: + login: + $ref: "#/components/schemas/userLogin" + required: + - login + responses: + "200": + description: Операция завершилась успешно. + content: + application/json: + schema: + type: object + properties: + status: + type: string + description: Должно принимать значение `ok`. + example: ok + required: + - status + "401": + description: Переданный токен не существует либо некорректен. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "404": + description: Пользователь с указанным логином не найден. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + /friends/remove: + post: + summary: Удалить пользователя из друзей + description: | + Позволяет удалить пользователя из друзей. + + Если указанного пользователя нет в друзьях, верните успешный ответ. + + Сервер должен идентифицировать пользователя по переданному токену. Значение токена будет подставляться в заголовок `Authorization` в формате `Bearer {token}`. + security: + - bearerAuth: [] + operationId: friendsRemove + requestBody: + description: | + В теле запроса передается логин пользователя. + required: true + content: + application/json: + schema: + type: object + properties: + login: + $ref: "#/components/schemas/userLogin" + required: + - login + responses: + "200": + description: Операция завершилась успешно. + content: + application/json: + schema: + type: object + properties: + status: + type: string + description: Должно принимать значение `ok`. + example: ok + required: + - status + "401": + description: Переданный токен не существует либо некорректен. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + /friends: + get: + summary: Получение списка друзей + description: | + Используется для получения списка своих друзей. + + Для плавной работы приложения используется пагинация. + + Сервер должен идентифицировать пользователя по переданному токену. Значение токена будет подставляться в заголовок `Authorization` в формате `Bearer {token}`. + security: + - bearerAuth: [] + operationId: friendsList + parameters: + - $ref: "#/components/parameters/paginationLimit" + - $ref: "#/components/parameters/paginationOffset" + responses: + "200": + description: | + Список друзей пользователя, отсортированный по убыванию по дате последнего добавления в друзья. + + В начале идут друзья, которые были добавлены совсем недавно. + content: + application/json: + schema: + type: array + items: + type: object + description: Описание друга. + properties: + login: + $ref: "#/components/schemas/userLogin" + addedAt: + type: string + description: | + Время и дата, когда данный пользователь был добавлен в друзья в последний раз. + + Передается в формате RFC3339. + example: 2006-01-02T15:04:05Z07:00 + required: + - login + - addedAt + "401": + description: Переданный токен не существует либо некорректен. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + /posts/new: + post: + summary: Отправить публикацию + description: | + Используется для отправки публикации в ленту. + + Сервер должен идентифицировать пользователя по переданному токену. Значение токена будет подставляться в заголовок `Authorization` в формате `Bearer {token}`. + security: + - bearerAuth: [] + operationId: submitPost + requestBody: + description: Информация о публикации. + required: true + content: + application/json: + schema: + type: object + properties: + content: + $ref: "#/components/schemas/postContent" + tags: + $ref: "#/components/schemas/postTags" + required: + - content + - tags + responses: + "200": + description: Публикация сохранена. Сервер назначает уникальный идентификатор и время создания публикации. + content: + application/json: + schema: + $ref: "#/components/schemas/post" + "401": + description: Переданный токен не существует либо некорректен. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + /posts/{postId}: + get: + summary: Получить ленту со своими постами + description: | + Используется для получения публикации по её идентификатору. + + Если публикация принадлежит пользователю с публичным профилем, её может получить любой другой аутентифицированный пользователь. + + Если профиль автора закрыт, только автор может получить публикацию по ID. + + Сервер должен идентифицировать пользователя по переданному токену. Значение токена будет подставляться в заголовок `Authorization` в формате `Bearer {token}`. + security: + - bearerAuth: [] + operationId: getPostById + parameters: + - name: postId + description: ID публикации. + required: true + in: path + schema: + $ref: "#/components/schemas/postId" + responses: + "200": + description: Публикация найдена. + content: + application/json: + schema: + $ref: "#/components/schemas/post" + "401": + description: Переданный токен не существует либо некорректен. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "404": + description: Указанный пост не найден либо к нему нет доступа. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + /posts/feed/my: + get: + summary: Получить ленту со своими постами + description: | + Используется для получения списка своих постов. + + Для плавной работы приложения используется пагинация. + + Сервер должен идентифицировать пользователя по переданному токену. Значение токена будет подставляться в заголовок `Authorization` в формате `Bearer {token}`. + security: + - bearerAuth: [] + operationId: getMyFeed + parameters: + - $ref: "#/components/parameters/paginationLimit" + - $ref: "#/components/parameters/paginationOffset" + responses: + "200": + description: | + Список публикаций пользователя, отсортированных по убыванию по дате публикации. + + В начале идут публикации, которые были добавлены совсем недавно. + content: + application/json: + schema: + $ref: "#/components/schemas/post" + "401": + description: Переданный токен не существует либо некорректен. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + /posts/feed/{login}: + get: + summary: Получить ленту с постами другого пользователя + description: | + Используется для получения списка публикаций другого пользователя. + + Если профиль пользователя открыт, его посты доступны всем. + Если профиль пользователя закрыт, его посты доступны самому пользователю и пользователям, на кого он подписан. + + Для плавной работы приложения используется пагинация. + + Сервер должен идентифицировать пользователя по переданному токену. Значение токена будет подставляться в заголовок `Authorization` в формате `Bearer {token}`. + security: + - bearerAuth: [] + operationId: getFeedByOthers + parameters: + - name: login + description: Логин пользователя. + required: true + in: path + schema: + $ref: "#/components/schemas/userLogin" + - $ref: "#/components/parameters/paginationLimit" + - $ref: "#/components/parameters/paginationOffset" + responses: + "200": + description: | + Список публикаций пользователя, отсортированных по убыванию по дате публикации. + + В начале идут публикации, которые были добавлены совсем недавно. + content: + application/json: + schema: + $ref: "#/components/schemas/post" + "401": + description: Переданный токен не существует либо некорректен. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "404": + description: Пользователь не найден либо к нему нет доступа. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + /posts/{postId}/like: + post: + summary: Лайк поста + description: | + Лайк поста. + + Сервер должен идентифицировать пользователя по переданному токену. Значение токена будет подставляться в заголовок `Authorization` в формате `Bearer {token}`. + security: + - bearerAuth: [] + operationId: likstPost + parameters: + - name: postId + description: ID публикации. + required: true + in: path + schema: + $ref: "#/components/schemas/postId" + responses: + "200": + description: Лайк засчитан. + content: + application/json: + schema: + $ref: "#/components/schemas/post" + "401": + description: Переданный токен не существует либо некорректен. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "404": + description: Указанный пост не найден либо к нему нет доступа. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + /posts/{postId}/dislike: + post: + summary: Дизлайк поста + description: | + Дизлайк поста. + + Сервер должен идентифицировать пользователя по переданному токену. Значение токена будет подставляться в заголовок `Authorization` в формате `Bearer {token}`. + security: + - bearerAuth: [] + operationId: dislikePost + parameters: + - name: postId + description: ID публикации. + required: true + in: path + schema: + $ref: "#/components/schemas/postId" + responses: + "200": + description: Дизлайк засчитан. + content: + application/json: + schema: + $ref: "#/components/schemas/post" + "401": + description: Переданный токен не существует либо некорректен. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "404": + description: Указанный пост не найден либо к нему нет доступа. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + +components: + schemas: + countryAlpha2: + type: string + description: Двухбуквенный код, уникально идентифицирующий страну + maxLength: 2 + pattern: "[a-zA-Z]{2}" + example: RU + countryRegion: + type: string + description: Географический регион, к которому относится страна + enum: + - Europe + - Africa + - Americas + - Oceania + - Asia + country: + type: object + description: Информация о стране из стандарта ISO 3166 + properties: + name: + type: string + description: Полное название страны + maxLength: 100 + alpha2: + $ref: "#/components/schemas/countryAlpha2" + alpha3: + type: string + description: Трехбуквенный код страны + maxLength: 3 + pattern: "[a-zA-Z]{3}" + region: + $ref: "#/components/schemas/countryRegion" + required: + - name + - alpha2 + - alpha3 + example: + name: Burkina Faso + alpha2: BF + alpha3: BFA + region: Africa + userLogin: + type: string + description: Логин пользователя + maxLength: 30 + pattern: "[a-zA-Z0-9-]+" + example: yellowMonkey + userEmail: + type: string + description: E-mail пользователя + maxLength: 50 + example: yellowstone1980@you.ru + userPassword: + type: string + description: | + Пароль пользователя, к которому предъявляются следующие требования: + + - Длина пароля не менее 6 символов. + - Присутствуют латинские символы в нижнем и верхнем регистре. + - Присутствует минимум одна цифра. + minLength: 6 + maxLength: 100 + example: $aba4821FWfew01#.fewA$ + userIsPublic: + type: boolean + description: | + Является ли данный профиль публичным. + + Публичные профили доступны другим пользователям: если профиль публичный, любой пользователь платформы сможет получить информацию о пользователе. + example: true + userPhone: + type: string + description: Номер телефона пользователя в формате +123456789 + pattern: \+[\d]+ + example: "+74951239922" + userImage: + type: string + description: Ссылка на фото для аватара пользователя + example: https://http.cat/images/100.jpg + maxLength: 200 + userProfile: + type: object + description: Информация о профиле пользователя + properties: + login: + $ref: "#/components/schemas/userLogin" + email: + $ref: "#/components/schemas/userEmail" + countryCode: + $ref: "#/components/schemas/countryAlpha2" + isPublic: + $ref: "#/components/schemas/userIsPublic" + phone: + $ref: "#/components/schemas/userPhone" + image: + $ref: "#/components/schemas/userImage" + required: + - login + - email + - countryCode + - isPublic + postId: + type: string + description: Уникальный идентификатор публикации, присвоенный сервером. + example: 550e8400-e29b-41d4-a716-446655440000 + postContent: + type: string + description: Текст публикации. + example: Свеча на 400! Покупаем, докупаем и фиксируем прибыль. + maxLength: 1000 + postTags: + type: array + description: Список тегов публикации. + items: + type: string + description: Значение тега. + example: тинькофф + maxLength: 20 + example: + - тинькофф + - спббиржа + - moex + post: + type: object + description: Пользовательская публикация. + properties: + id: + $ref: "#/components/schemas/postId" + content: + $ref: "#/components/schemas/postContent" + author: + $ref: "#/components/schemas/userLogin" + tags: + $ref: "#/components/schemas/postTags" + createdAt: + type: string + description: | + Серверная дата и время в момент, когда пользователь отправил данную публикацию. + Передается в формате RFC3339. + example: 2006-01-02T15:04:05Z07:00 + likesCount: + type: integer + minimum: 0 + default: 0 + description: Число лайков, набранное публикацией. + dislikesCount: + type: integer + minimum: 0 + default: 0 + description: Число дизлайков, набранное публикацией. + required: + - id + - content + - author + - tags + - createdAt + - likesCount + - dislikesCount + errorResponse: + type: object + description: Используется для возвращения ошибки пользователю + properties: + reason: + type: string + description: Описание ошибки в свободной форме + minLength: 5 + required: + - reason + example: + reason: <объяснение, почему запрос пользователя не может быть обработан> + parameters: + paginationLimit: + in: query + name: limit + required: false + description: | + Максимальное число возвращаемых объектов. Используется для запросов с пагинацией. + + Сервер должен возвращать максимальное допустимое число объектов. + schema: + type: integer + format: int32 + minimum: 0 + maximum: 50 + default: 5 + paginationOffset: + in: query + name: offset + required: false + description: | + Какое количество объектов должно быть пропущено с начала. Используется для запросов с пагинацией. + schema: + type: integer + format: int32 + default: 0 + securitySchemes: + bearerAuth: + type: http + scheme: bearer diff --git a/tests/public-tests.json b/tests/public-tests.json new file mode 100644 index 0000000..4ce20fe --- /dev/null +++ b/tests/public-tests.json @@ -0,0 +1,677 @@ +{ + "info": { + "_postman_id": "c30d4269-9aa0-4d39-a5de-37901602b269", + "name": "PROD round 2: public tests [1]", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "01/ping", + "item": [ + { + "name": "Ping", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "", + "pm.test(\"PING server\", function () {", + " pm.sendRequest(pm.variables.get(\"base_url\") + \"/ping\", function (err, response) {", + " pm.expect(response.code).to.be.eq(200, \"Invalid response code status\");", + " });", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/ping", + "host": [ + "{{base_url}}" + ], + "path": [ + "ping" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "02/countries", + "item": [ + { + "name": "List countries", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var schema = {", + " \"type\": \"array\",", + " \"items\": {", + " \"type\": \"object\",", + " \"description\": \"Информация о стране из стандарта ISO 3166\",", + " \"properties\": {", + " \"name\": {", + " \"type\": \"string\",", + " \"description\": \"Полное название страны\",", + " \"maxLength\": 100", + " },", + " \"alpha2\": {", + " \"type\": \"string\",", + " \"description\": \"Двухбуквенный код, уникально идентифицирующий страну\",", + " \"maxLength\": 2,", + " \"pattern\": \"[a-zA-Z]{2}\"", + " },", + " \"alpha3\": {", + " \"type\": \"string\",", + " \"description\": \"Трехбуквенный код страны\",", + " \"maxLength\": 3,", + " \"pattern\": \"[a-zA-Z]{3}\"", + " },", + " \"region\": {", + " \"type\": \"string\",", + " \"description\": \"Географический регион, к которому относится страна\",", + " \"enum\": [", + " \"Europe\",", + " \"Africa\",", + " \"Americas\",", + " \"Oceania\",", + " \"Asia\"", + " ]", + " }", + " },", + " \"required\": [", + " \"name\",", + " \"alpha2\",", + " \"alpha3\"", + " ]", + " },", + " \"$schema\": \"http://json-schema.org/draft-04/schema#\"", + "};", + "", + "var countries = [", + " {", + " \"name\": \"Algeria\",", + " \"alpha2\": \"DZ\",", + " \"alpha3\": \"DZA\",", + " \"region\": \"Africa\"", + " },", + " {", + " \"name\": \"Russian Federation\",", + " \"alpha2\": \"RU\",", + " \"alpha3\": \"RUS\",", + " \"region\": \"Europe\"", + " },", + "];", + "", + "countries.forEach(function (country) {", + " pm.test(`List countries from region \"${country.region}\"`, function () {", + " var url = pm.variables.get(\"base_url\") + `/countries?region=${country.region}`;", + "", + " pm.sendRequest(url, function (err, response) {", + " pm.test(\"Validate response\", () => {", + " var resp = response.json();", + " ", + " pm.expect(response.code).to.be.eq(200, \"Invalid response code status\");", + " pm.expect(tv4.validate(resp, schema), \"Invalid JSON schema\").to.be.true;", + "", + " console.log(\"got\", resp);", + " pm.expect(resp).to.deep.include(country, `Invalid countries list`);", + " });", + " });", + " });", + "});", + "", + "", + "", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/countries", + "host": [ + "{{base_url}}" + ], + "path": [ + "countries" + ] + } + }, + "response": [] + }, + { + "name": "Get country", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var countrySchema = {", + " \"type\": \"object\",", + " \"description\": \"Информация о стране из стандарта ISO 3166\",", + " \"properties\": {", + " \"name\": {", + " \"type\": \"string\",", + " \"description\": \"Полное название страны\",", + " \"maxLength\": 100", + " },", + " \"alpha2\": {", + " \"type\": \"string\",", + " \"description\": \"Двухбуквенный код, уникально идентифицирующий страну\",", + " \"maxLength\": 2,", + " \"pattern\": \"[a-zA-Z]{2}\"", + " },", + " \"alpha3\": {", + " \"type\": \"string\",", + " \"description\": \"Трехбуквенный код страны\",", + " \"maxLength\": 3,", + " \"pattern\": \"[a-zA-Z]{3}\"", + " },", + " \"region\": {", + " \"type\": \"string\",", + " \"description\": \"Географический регион, к которому относится страна\",", + " \"enum\": [", + " \"Europe\",", + " \"Africa\",", + " \"Americas\",", + " \"Oceania\",", + " \"Asia\"", + " ]", + " }", + " },", + " \"required\": [", + " \"name\",", + " \"alpha2\",", + " \"alpha3\"", + " ],", + " \"$schema\": \"http://json-schema.org/draft-04/schema#\"", + "};", + "", + "var countries = [", + " {", + " \"name\": \"Algeria\",", + " \"alpha2\": \"DZ\",", + " \"alpha3\": \"DZA\",", + " \"region\": \"Africa\"", + " },", + " {", + " \"name\": \"Russian Federation\",", + " \"alpha2\": \"RU\",", + " \"alpha3\": \"RUS\",", + " \"region\": \"Europe\"", + " },", + " {", + " \"name\": \"Kazakhstan\",", + " \"alpha2\": \"KZ\",", + " \"alpha3\": \"KAZ\",", + " \"region\": \"Asia\"", + " }", + "];", + "", + "countries.forEach(function (country) {", + " pm.test(`Get country \"${country.name}\" [existing]`, function () {", + " var url = pm.variables.get(\"base_url\") + \"/countries/\" + country.alpha2;", + "", + " pm.sendRequest(url, function (err, response) {", + " pm.test(\"Validate response\", () => {", + " var resp = response.json();", + "", + " pm.expect(response.code).to.be.eq(200, \"Invalid response code status\");", + " pm.expect(tv4.validate(resp, countrySchema), \"Invalid JSON schema\").to.be.true;", + "", + " console.log(\"got\", resp, \"expected\", country);", + " pm.expect(resp).to.deep.eq(country, `Got invalid object`);", + " });", + " });", + " });", + "});", + "", + "", + "", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/countries/RU", + "host": [ + "{{base_url}}" + ], + "path": [ + "countries", + "RU" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "03/auth/registration", + "item": [ + { + "name": "Register a user", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var schema = {", + " \"type\": \"object\",", + " \"properties\": {", + " \"profile\": {", + " \"type\": \"object\",", + " \"description\": \"Информация о профиле пользователя\",", + " \"properties\": {", + " \"login\": {", + " \"type\": \"string\",", + " \"description\": \"Логин пользователя\",", + " \"maxLength\": 30,", + " \"pattern\": \"[a-zA-Z0-9-]+\"", + " },", + " \"email\": {", + " \"type\": \"string\",", + " \"description\": \"E-mail пользователя\",", + " \"maxLength\": 50", + " },", + " \"countryCode\": {", + " \"type\": \"string\",", + " \"description\": \"Двухбуквенный код, уникально идентифицирующий страну\",", + " \"maxLength\": 2,", + " \"pattern\": \"[a-zA-Z]{2}\"", + " },", + " \"isPublic\": {", + " \"type\": \"boolean\",", + " \"description\": \"Является ли данный профиль публичным. \\n\\nПубличные профили доступны другим пользователям: если профиль публичный, любой пользователь платформы сможет получить информацию о пользователе.\\n\"", + " },", + " \"phone\": {", + " \"type\": \"string\",", + " \"description\": \"Номер телефона пользователя в формате +123456789\",", + " \"pattern\": \"\\\\+[\\\\d]+\"", + " },", + " \"image\": {", + " \"type\": \"string\",", + " \"description\": \"Ссылка на фото для аватара пользователя\",", + " \"maxLength\": 200", + " }", + " },", + " \"required\": [", + " \"login\",", + " \"email\",", + " \"countryCode\",", + " \"isPublic\"", + " ]", + " }", + " },", + " \"required\": [", + " \"profile\"", + " ],", + " \"$schema\": \"http://json-schema.org/draft-04/schema#\"", + "};", + "", + "pm.test(\"Register a user\", function () {", + " var url = pm.variables.get(\"base_url\") + \"/auth/register\";", + " const options = {", + " url: url,", + " method: 'POST',", + " header: {", + " 'Content-Type': 'application/json',", + " },", + " body: {", + " mode: 'raw',", + " raw: JSON.stringify({", + " 'login': 'yellowMonkey2',", + " 'email': 'yellowstone1980@you.ru',", + " 'password': '$aba4821FWfew01#.fewA$',", + " 'countryCode': 'RU',", + " 'isPublic': true,", + " 'phone': '+74951239922',", + " })", + " }", + " };", + "", + " const profile = {", + " 'profile': {", + " 'login': 'yellowMonkey2',", + " 'email': 'yellowstone1980@you.ru',", + " 'countryCode': 'RU',", + " 'isPublic': true,", + " 'phone': '+74951239922',", + " }", + " }", + "", + " pm.sendRequest(options, function (err, response) {", + " pm.test(\"Validate response\", () => {", + " var resp = response.json();", + " ", + " pm.expect(response.code).to.be.eq(201, \"Invalid response code status\");", + " pm.expect(tv4.validate(resp, schema), \"Invalid JSON schema\").to.be.true;", + "", + " console.log(\"got\", resp, \"expected\", profile);", + " pm.expect(resp).to.deep.eq(profile, `Got invalid object`);", + " });", + " });", + "});", + "", + "", + "", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "url": { + "raw": "{{base_url}}/auth/register", + "host": [ + "{{base_url}}" + ], + "path": [ + "auth", + "register" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "04/auth/sign-in", + "item": [ + { + "name": "Sign in", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var schema = {", + " \"type\": \"object\",", + " \"properties\": {", + " \"token\": {", + " \"type\": \"string\",", + " \"description\": \"Сгенерированный токен пользователя\",", + " \"minLength\": 20,", + " }", + " },", + " \"required\": [", + " \"token\"", + " ],", + " \"$schema\": \"http://json-schema.org/draft-04/schema#\"", + "};", + "", + "pm.test(\"Register a user\", function () {", + " var url = pm.variables.get(\"base_url\") + \"/auth/register\";", + " const options = {", + " url: url,", + " method: 'POST',", + " header: {", + " 'Content-Type': 'application/json',", + " },", + " body: {", + " mode: 'raw',", + " raw: JSON.stringify({", + " 'login': 'yellowMonkey11000',", + " 'email': 'yellowstone1983@you.ru',", + " 'password': '$aba4821FWfew01#.fewA$',", + " 'countryCode': 'RU',", + " 'isPublic': true,", + " 'phone': '+74951239912',", + " })", + " }", + " };", + "", + " pm.sendRequest(options, function (err, response) {", + " pm.test(\"Validate register response\", () => {", + " var resp = response.json();", + " pm.expect(response.code).to.be.oneOf([201, 409], \"Invalid response code status\");", + " });", + " });", + "});", + "", + "pm.test(\"Sign in\", function () {", + " var url = pm.variables.get(\"base_url\") + \"/auth/sign-in\";", + " const options = {", + " url: url,", + " method: 'POST',", + " header: {", + " 'Content-Type': 'application/json',", + " },", + " body: {", + " mode: 'raw',", + " raw: JSON.stringify({", + " 'login': 'yellowMonkey11000',", + " 'password': '$aba4821FWfew01#.fewA$',", + " })", + " }", + " };", + "", + " pm.sendRequest(options, function (err, response) {", + " pm.test(\"Validate sign-in response\", () => {", + " var resp = response.json();", + "", + " pm.expect(response.code).to.be.eq(200, \"Invalid response code status\");", + " pm.expect(tv4.validate(resp, schema), \"Invalid JSON schema\").to.be.true;", + "", + " console.log('Token', resp.token);", + " });", + " });", + "});", + "", + "", + "", + "", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "url": { + "raw": "{{base_url}}/auth/sign-in", + "host": [ + "{{base_url}}" + ], + "path": [ + "auth", + "sign-in" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "05/me", + "item": [ + { + "name": "Get my profile", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var schema = {", + " \"type\": \"object\",", + " \"properties\": {", + " \"token\": {", + " \"type\": \"string\",", + " \"description\": \"Сгенерированный токен пользователя\",", + " \"minLength\": 20,", + " }", + " },", + " \"required\": [", + " \"token\"", + " ],", + " \"$schema\": \"http://json-schema.org/draft-04/schema#\"", + "};", + "", + "pm.test(\"Register a user\", function () {", + " var url = pm.variables.get(\"base_url\") + \"/auth/register\";", + " const options = {", + " url: url,", + " method: 'POST',", + " header: {", + " 'Content-Type': 'application/json',", + " },", + " body: {", + " mode: 'raw',", + " raw: JSON.stringify({", + " 'login': 'yellowMonkey11000',", + " 'email': 'yellowstone1983@you.ru',", + " 'password': '$aba4821FWfew01#.fewA$',", + " 'countryCode': 'RU',", + " 'isPublic': true,", + " 'phone': '+74951239912',", + " })", + " }", + " };", + "", + " pm.sendRequest(options, function (err, response) {", + " pm.test(\"Validate register response\", () => {", + " var resp = response.json();", + " pm.expect(response.code).to.be.oneOf([201, 409], \"Invalid response code status\");", + " });", + " });", + "});", + "", + "pm.test(\"Sign in\", function () {", + " var url = pm.variables.get(\"base_url\") + \"/auth/sign-in\";", + " const options = {", + " url: url,", + " method: 'POST',", + " header: {", + " 'Content-Type': 'application/json',", + " },", + " body: {", + " mode: 'raw',", + " raw: JSON.stringify({", + " 'login': 'yellowMonkey11000',", + " 'password': '$aba4821FWfew01#.fewA$',", + " })", + " }", + " };", + "", + " pm.sendRequest(options, function (err, response) {", + " pm.test(\"Validate sign-in response\", () => {", + " var resp = response.json();", + "", + " pm.expect(response.code).to.be.eq(200, \"Invalid response code status\");", + " pm.expect(tv4.validate(resp, schema), \"Invalid JSON schema\").to.be.true;", + "", + " pm.environment.set(\"05_profile_token\", resp.token);", + " console.log(\"Token has been saved\")", + "", + " pm.test(\"Get profile\", function () {", + " const url = pm.variables.get(\"base_url\") + \"/me/profile\";", + " const token = pm.environment.get(\"05_profile_token\");", + " const options = {", + " url: url,", + " method: 'GET',", + " header: {", + " 'Content-Type': 'application/json',", + " 'Authorization': `Bearer ${token}`,", + " },", + " };", + "", + " pm.sendRequest(options, function (err, response) {", + " pm.test(\"Validate profile\", () => {", + " var resp = response.json();", + "", + " pm.expect(response.code).to.be.eq(200, \"Invalid response code status\");", + "", + " console.log(\"Got profile\", resp);", + " pm.expect(resp.login).to.be.eq(\"yellowMonkey11000\", \"Invalid login\");", + " });", + " });", + " });", + " });", + " });", + "});", + "", + "", + "", + "", + "", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/me/profile", + "host": [ + "{{base_url}}" + ], + "path": [ + "me", + "profile" + ] + } + }, + "response": [] + } + ] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "base_url", + "value": "http://localhost:57424/api", + "type": "default" + } + ] +} \ No newline at end of file