diff --git a/README.md b/README.md index 2293496..a354600 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,8 @@ - `POSTGRES_DATABASE` — имя базы данных PostgreSQL, с которой должно работать приложение. +- `RANDOM_SECRET` — псевдо-случайная последовательность из 128 символов (a-z, A-Z, 0-9), сгенерированная тестирующей системой. Можете использовать её, если вашему приложению необходим секретный ключ (например, для JWT). Если вам не требуется данное значение, можете его не использовать. + Автор приложения сам выбирает, с какими из переменных окружения ему комфортно работать. Учитывая современные реалии, приложение будет запускаться через Docker контейнер. В репозитории присутствует Dockerfile, с помощью которого будет собираться образ приложения. @@ -80,6 +82,8 @@ Если структура запроса не соответствует требованиям и описанному формату, по умолчанию возвращается код ответа 400. Если указан более специфичный код ответа, используйте его. +Если запрос некорректен хотя бы в одном параметре, весь запрос отвергается и признается некорректным. + ### 01/ping Достаточно реализовать возврат успешного ответа (с кодом `200`) на запрос `GET /api/ping`. Содержимое тела ответа при этом не валидируется, можно возвращать `"ok"`. @@ -217,7 +221,7 @@ INSERT INTO countries (name, alpha2, alpha3, region) VALUES Всегда запоминается последняя реакция пользователя. Если пользователь поставил лайк два раза подряд, эффект лайка остается. Если пользователь поставил лайк, а потом дизлайк, остается реакция дизлайка. -В полях `likesCount` и `dislikesCount` необходимо отразить уникальное число лайков и дизлайков. +В полях `likesCount` и `dislikesCount` необходимо отразить число лайков и дизлайков публикации, при этом от каждого пользователя учитывается только его самая последняя реакция. ## Тестирование @@ -272,6 +276,28 @@ INSERT INTO countries (name, alpha2, alpha3, region) VALUES Не забывайте делать `git pull --rebase`, чтобы загрузить актуальные требования в локальную версию репозитория. +### 02.03.2024 + +Коллеги, привет! Ваш Project Manager передал все опасения касательно сроков, поэтому мы договорились, +что финальное тестирование будет проходить, опираясь на версию спецификации, опубликованную 3 марта 15:00 (МСК). + +Напоминаем! В тестах будет проверяться только то поведение, которое было описано в README либо спецификации. + +Обращаем внимание: при работе с публичным набором тестов в Postman обращайте внимание на содержимое вкладки Tests, именно там заключена логика тестирования. +Request-path в Postman изменены на `GET /api/ping`, чтобы нерелевантная информация в логах не смущала вас. + +И еще немного полезных замечаний: + +- Если запрос некорректен хотя бы в одном параметре, весь запрос отвергается и признается некорректным. + +- Если вам нужен секретный ключ, можете (необязательно!) использовать `RANDOM_SECRET`. + +- Timezone при передаче времени не так важна. Важно, чтобы счетчик времени монотонно рос и был одного формата во всех ответах backend'а. + +- Чтобы отобразить число лайков и дизлайков поста, учитывайте только последнюю реакцию от каждого пользователя. + +- Если структура ответа предполагает опциональность поля, сервер не должен возвращать данное поле при его отсутствии. + ### 01.03.2024 Коллеги, с первым днем весны! @@ -279,7 +305,7 @@ INSERT INTO countries (name, alpha2, alpha3, region) VALUES Напоминаем вам, что корректные логин, номер телефона, e-mail и другая подобная информация должны состоять минимум из одного символа! А длина уникального идентификатора публикации не превышает разумных значений... -Также добавим, что в эндпоинте `/countries` если хотя бы один переданный регион является некорректным, весь запрос считается некорректным. +Также добавим, что в эндпоинте `/countries` если хотя бы один переданный регион является некорректным, весь запрос считается некорректным. Это общее правило: если запрос некорректен хотя бы в одном параметре, весь запрос отвергается и признается некорректным. ### 28.02.2024 @@ -287,7 +313,7 @@ INSERT INTO countries (name, alpha2, alpha3, region) VALUES Если профиль пользователя закрыт, доступ к его профилю и его публикациям появляется у пользователей, кого данный пользователь добавил в друзья. -При если это Маша добавила Петю в друзья, не значит, что Петя добавил Машу в друзья. Можно расценивать добавление в друзья как подписку. +При этом если Маша добавила Петю в друзья, не значит, что Петя добавил Машу в друзья. Можно расценивать добавление в друзья как подписку. Группа `08/friends` зависит от группы `06/profiles`. diff --git a/tests/openapi.yml b/tests/openapi.yml index 39fccb8..2bd384b 100644 --- a/tests/openapi.yml +++ b/tests/openapi.yml @@ -251,6 +251,7 @@ paths: Если значение передано, данное изменение должно быть отражено в профиле пользователя. Если значение не передано, необходимо оставить прежнее значение параметра. + Если передана пустая структура, ничего изменять не требуется, возвращается успешный ответ. required: true content: application/json: @@ -630,6 +631,8 @@ paths: Для плавной работы приложения используется пагинация. + Можете считать, что пользователей с логином `my` не будет. + Сервер должен идентифицировать пользователя по переданному токену. Значение токена будет подставляться в заголовок `Authorization` в формате `Bearer {token}`. security: - bearerAuth: [] @@ -722,7 +725,7 @@ paths: $ref: "#/components/schemas/postId" responses: "200": - description: Лайк засчитан. + description: Реакция засчитана, возвращайте пубикацию с актуальным числом лайков и дизлайков. content: application/json: schema: @@ -758,7 +761,7 @@ paths: $ref: "#/components/schemas/postId" responses: "200": - description: Дизлайк засчитан. + description: Реакция засчитана, возвращайте пубикацию с актуальным числом лайков и дизлайков. content: application/json: schema: diff --git a/tests/public-tests.json b/tests/public-tests.json index ffde4a2..3870506 100644 --- a/tests/public-tests.json +++ b/tests/public-tests.json @@ -118,7 +118,7 @@ " var url = pm.variables.get(\"base_url\") + `/countries?region=${country.region}`;", "", " pm.sendRequest(url, function (err, response) {", - " pm.test(\"Validate response\", () => {", + " pm.test(`Validate response for region ${country.region}`, () => {", " var resp = response.json();", " ", " pm.expect(response.code).to.be.eq(200, \"Invalid response code status\");", @@ -383,16 +383,15 @@ } ], "request": { - "method": "POST", + "method": "GET", "header": [], "url": { - "raw": "{{base_url}}/auth/register", + "raw": "{{base_url}}/ping", "host": [ "{{base_url}}" ], "path": [ - "auth", - "register" + "ping" ] } }, @@ -403,6 +402,133 @@ { "name": "04/auth/sign-in", "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.oneOf([201, 409], \"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": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/ping", + "host": [ + "{{base_url}}" + ], + "path": [ + "ping" + ] + } + }, + "response": [] + }, { "name": "Sign in", "event": [ @@ -425,6 +551,122 @@ " \"$schema\": \"http://json-schema.org/draft-04/schema#\"", "};", "", + "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': 'yellowMonkey2',", + " '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": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/ping", + "host": [ + "{{base_url}}" + ], + "path": [ + "ping" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "05/me", + "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 = {", @@ -436,50 +678,32 @@ " body: {", " mode: 'raw',", " raw: JSON.stringify({", - " 'login': 'yellowMonkey11000',", - " 'email': 'yellowstone1983@you.ru',", + " 'login': 'yellowMonkey10000',", + " 'email': 'yellowstone1980@you.ru',", " 'password': '$aba4821FWfew01#.fewA$',", " 'countryCode': 'RU',", " 'isPublic': true,", - " 'phone': '+74951239912',", + " 'phone': '+74951239922',", " })", " }", " };", "", + " const profile = {", + " 'profile': {", + " 'login': 'yellowMonkey10000',", + " 'email': 'yellowstone1980@you.ru',", + " 'countryCode': 'RU',", + " 'isPublic': true,", + " 'phone': '+74951239922',", + " }", + " }", + "", " pm.sendRequest(options, function (err, response) {", - " pm.test(\"Validate register response\", () => {", + " pm.test(\"Validate 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);", - " });", - " });", - " });", " });", "});", "", @@ -492,28 +716,22 @@ } ], "request": { - "method": "POST", + "method": "GET", "header": [], "url": { - "raw": "{{base_url}}/auth/sign-in", + "raw": "{{base_url}}/ping", "host": [ "{{base_url}}" ], "path": [ - "auth", - "sign-in" + "ping" ] } }, "response": [] - } - ] - }, - { - "name": "05/me", - "item": [ + }, { - "name": "Get my profile", + "name": "Sign in", "event": [ { "listen": "test", @@ -534,9 +752,9 @@ " \"$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 = {", + "pm.test(\"Sign in\", function () {", + " var url = pm.variables.get(\"base_url\") + \"/auth/sign-in\";", + " var options = {", " url: url,", " method: 'POST',", " header: {", @@ -545,84 +763,26 @@ " body: {", " mode: 'raw',", " raw: JSON.stringify({", - " 'login': 'yellowMonkey11000',", - " 'email': 'yellowstone1983@you.ru',", + " 'login': 'yellowMonkey10000',", " 'password': '$aba4821FWfew01#.fewA$',", - " 'countryCode': 'RU',", - " 'isPublic': true,", - " 'phone': '+74951239912',", " })", " }", " };", - "", + " ", " pm.sendRequest(options, function (err, response) {", - " pm.test(\"Validate register response\", () => {", + " pm.test(\"Validate sign-in 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.expect(response.code).to.be.eq(200, \"Invalid response code status\");", + " pm.expect(tv4.validate(resp, schema), \"Invalid JSON schema\").to.be.true;", "", - " 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\");", - " });", - " });", - " });", - " });", - " });", + " pm.environment.set(\"05_profile_token\", resp.token);", + " console.log(\"Token has been saved\")", " });", " });", "});", "", "", - "", - "", - "", - "", - "", "" ], "type": "text/javascript" @@ -633,13 +793,64 @@ "method": "GET", "header": [], "url": { - "raw": "{{base_url}}/me/profile", + "raw": "{{base_url}}/ping", "host": [ "{{base_url}}" ], "path": [ - "me", - "profile" + "ping" + ] + } + }, + "response": [] + }, + { + "name": "Get my profile", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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(\"yellowMonkey10000\", \"Invalid login\");", + " });", + " });", + "});", + "", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/ping", + "host": [ + "{{base_url}}" + ], + "path": [ + "ping" ] } },