From fc40273f92fe24a3361daede550bc2411310bc9d Mon Sep 17 00:00:00 2001 From: Timur Date: Sat, 1 Mar 2025 21:33:59 +0300 Subject: [PATCH 01/10] rewrite tests on signup --- services/backend/apps/user/test.py | 71 +++++++++++++++++++++--------- 1 file changed, 51 insertions(+), 20 deletions(-) diff --git a/services/backend/apps/user/test.py b/services/backend/apps/user/test.py index cad7709..40d3431 100644 --- a/services/backend/apps/user/test.py +++ b/services/backend/apps/user/test.py @@ -1,29 +1,60 @@ -from django.core.exceptions import ValidationError +import json from django.test import TestCase from apps.user.models import User -class TestSignUp(TestCase): - def test_correct_signup(self): - user = User( - email="123123@timka.su", - password="1321312", - username="123123", +class SignUpAPITestCase(TestCase): + def test_successful_sign_up(self): + payload = { + "email": "user@example.com", + "password": "securepassword123", + "username": "123", + } + response = self.client.post( + "/api/v1/sign-up", + data=json.dumps(payload), + content_type="application/json", ) - user.full_clean() - user.save() + self.assertEqual(response.status_code, 201) + self.assertIn("token", response.json()) + self.assertEqual(User.objects.count(), 1) - def test_incorrect_mail(self): - user = User( - email="123123", - password="1321312", - username="123123123", + def test_missing_required_fields(self): + payload = {"password": "testpass123", "username": "sffsdf"} + response = self.client.post( + "/api/v1/sign-up", + data=json.dumps(payload), + content_type="application/json", ) - with self.assertRaises(ValidationError): - user.full_clean() + self.assertEqual(response.status_code, 400) - def test_missing_params(self): - user = User(password="123123", username="132131232131") - with self.assertRaises(ValidationError): - user.full_clean() + def test_invalid_email_format(self): + payload = { + "email": "ervtb uktr bym", + "password": "securepassword123", + "username": "123", + } + response = self.client.post( + "/api/v1/sign-up", + data=json.dumps(payload), + content_type="application/json", + ) + self.assertEqual(response.status_code, 400) + + def test_existing_user_conflict(self): + User.objects.create( + email="existing@example.com", password="existingpass123", username="testing" + ) + payload = { + "email": "existing@example.com", + "password": "sfsad", + "username": "testing", + } + response = self.client.post( + "/api/v1/sign-up", + data=json.dumps(payload), + content_type="application/json", + ) + self.assertEqual(response.status_code, 409) + self.assertIn("detail", response.json()) From 321206d2950e51f4294ff4110b63fa680168ecf1 Mon Sep 17 00:00:00 2001 From: Timur Date: Sat, 1 Mar 2025 21:58:42 +0300 Subject: [PATCH 02/10] add tests on sign-in --- services/backend/apps/user/models.py | 7 +-- services/backend/apps/user/test.py | 79 ++++++++++++++++++++++++++++ services/backend/config/settings.py | 1 + 3 files changed, 84 insertions(+), 3 deletions(-) diff --git a/services/backend/apps/user/models.py b/services/backend/apps/user/models.py index 5a246db..f525c29 100644 --- a/services/backend/apps/user/models.py +++ b/services/backend/apps/user/models.py @@ -14,10 +14,11 @@ class User(BaseModel): username = models.SlugField(unique=True, verbose_name="юзернейм") password = models.TextField(verbose_name="пароль") - def make_password(self): - return make_password(self.password) + @staticmethod + def make_password(password: str): + return make_password(password) - def check_password(self, password): + def check_password(self, password: str): return check_password(self.password, password) status = models.CharField( diff --git a/services/backend/apps/user/test.py b/services/backend/apps/user/test.py index 40d3431..82fc2c6 100644 --- a/services/backend/apps/user/test.py +++ b/services/backend/apps/user/test.py @@ -1,5 +1,6 @@ import json from django.test import TestCase +from django.contrib.auth.hashers import make_password from apps.user.models import User @@ -58,3 +59,81 @@ class SignUpAPITestCase(TestCase): ) self.assertEqual(response.status_code, 409) self.assertIn("detail", response.json()) + +class SignInAPITestCase(TestCase): + def setUp(self): + self.user = User.objects.create( + email="valid@example.com", + password=make_password("securepassword123"), + username="testuser" + ) + print(self.user.password) + self.valid_payload = { + "email": "valid@example.com", + "password": "securepassword123" + } + + def test_successful_sign_in(self): + response = self.client.post( + "/api/v1/sign-in", + data=json.dumps(self.valid_payload), + content_type="application/json" + ) + print(make_password(self.valid_payload["password"])) + self.assertEqual(response.status_code, 200) + self.assertIn("token", response.json()) + + def test_missing_credentials(self): + # Test missing email + response = self.client.post( + "/api/v1/sign-in", + data=json.dumps({"password": "pass"}), + content_type="application/json" + ) + self.assertEqual(response.status_code, 400) + + # Test missing password + response = self.client.post( + "/api/v1/sign-in", + data=json.dumps({"email": "test@example.com"}), + content_type="application/json" + ) + self.assertEqual(response.status_code, 400) + + def test_invalid_email_format(self): + payload = { + "email": "invalid-email", + "password": "password123" + } + response = self.client.post( + "/api/v1/sign-in", + data=json.dumps(payload), + content_type="application/json" + ) + self.assertEqual(response.status_code, 401) + + def test_incorrect_password(self): + payload = { + "email": "valid@example.com", + "password": "wrongpassword" + } + response = self.client.post( + "/api/v1/sign-in", + data=json.dumps(payload), + content_type="application/json" + ) + self.assertEqual(response.status_code, 401) + self.assertEqual(response.json()["detail"], "Unauthorized") + + def test_nonexistent_user(self): + payload = { + "email": "notexist@example.com", + "password": "password123" + } + response = self.client.post( + "/api/v1/sign-in", + data=json.dumps(payload), + content_type="application/json" + ) + self.assertEqual(response.status_code, 401) + self.assertEqual(response.json()["detail"], "Unauthorized") diff --git a/services/backend/config/settings.py b/services/backend/config/settings.py index 66332c8..45b28ed 100644 --- a/services/backend/config/settings.py +++ b/services/backend/config/settings.py @@ -485,6 +485,7 @@ LANGUAGE_COOKIE_AGE = 31449600 PASSWORD_HASHERS = [ "django.contrib.auth.hashers.Argon2PasswordHasher", + "django.contrib.auth.hashers.ScryptPasswordHasher", ] LANGUAGE_COOKIE_DOMAIN = None From e9f1efde5ab29119ab8983c73ef5bc343dd40926 Mon Sep 17 00:00:00 2001 From: Timur Date: Sat, 1 Mar 2025 22:45:20 +0300 Subject: [PATCH 03/10] add tests on `me` endpoint --- services/backend/apps/user/test.py | 80 ++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/services/backend/apps/user/test.py b/services/backend/apps/user/test.py index 82fc2c6..bce54cd 100644 --- a/services/backend/apps/user/test.py +++ b/services/backend/apps/user/test.py @@ -1,4 +1,6 @@ import json +import uuid + from django.test import TestCase from django.contrib.auth.hashers import make_password @@ -137,3 +139,81 @@ class SignInAPITestCase(TestCase): ) self.assertEqual(response.status_code, 401) self.assertEqual(response.json()["detail"], "Unauthorized") + + +class UserMeEndpointTestCase(TestCase): + def setUp(self): + # Create test user and token + self.user = User.objects.create( + email="johndoe@example.com", + username="johndoe", + password=make_password("securepassword123") + ) + resp = self.client.post( + "/api/v1/sign-in", + data=json.dumps({"email": "johndoe@example.com", "password": "securepassword123"}), + content_type="application/json" + ).json() + self.token = resp['token'] + self.url = "/api/v1/me" + + def test_get_authenticated_user_data(self): + """Test authenticated user can retrieve their profile (200 OK)""" + response = self.client.get( + self.url, + HTTP_AUTHORIZATION=f"Bearer {self.token}" + ) + + self.assertEqual(response.status_code, 200) + data = response.json() + + # Validate UserSchema structure + self.assertIn("id", data) + self.assertIn("email", data) + self.assertIn("username", data) + + # Validate UUID format if ID is present + if data["id"] is not None: + try: + uuid.UUID(data["id"]) + except ValueError: + self.fail("ID is not a valid UUID") + + # Validate response content + self.assertEqual(data["email"], "johndoe@example.com") + self.assertEqual(data["username"], "johndoe") + + def test_unauthenticated_access(self): + """Test unauthorized access returns 401 Unauthorized""" + response = self.client.get(self.url) + + self.assertEqual(response.status_code, 401) + self.assertEqual(response.json()["detail"], "Unauthorized") + + def test_invalid_auth_scheme(self): + """Test invalid authentication scheme returns 401""" + response = self.client.get( + self.url, + HTTP_AUTHORIZATION=f"InvalidScheme {self.token}" + ) + + self.assertEqual(response.status_code, 401) + self.assertEqual(response.json()["detail"], "Unauthorized") + + def test_malformed_token(self): + """Test malformed token returns 401""" + test_cases = [ + "invalid.token.123", + "Bearer", + "", + "123456" + ] + + for token in test_cases: + with self.subTest(token=token): + response = self.client.get( + self.url, + HTTP_AUTHORIZATION=f"Bearer {token}" + ) + self.assertEqual(response.status_code, 401) + self.assertEqual(response.json()["detail"], "Unauthorized") From f2803e1d10cdd9ca7a88c918fb4de59006a4f1cc Mon Sep 17 00:00:00 2001 From: ITQ Date: Sat, 1 Mar 2025 22:48:36 +0300 Subject: [PATCH 04/10] (scope): [body] [footer(s)] --- .gitlab-ci.yml | 9 +++++++++ compose.yaml | 14 ++++++++++++++ docs/Dockerfile | 26 ++++++++++++++++++++++++++ infrastructure/nginx/nginx.conf | 18 ++++++++++++++++++ 4 files changed, 67 insertions(+) create mode 100644 docs/Dockerfile diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c9a3f05..a734261 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -54,6 +54,15 @@ build_backend-staticfiles: DOCKERFILE_PATH: "Dockerfile.staticfiles" IMAGE_NAME: "$CI_REGISTRY_IMAGE/backend-staticfiles" +build_docs: + <<: *build-template + rules: + - if: '$CI_COMMIT_REF_NAME == "master"' + variables: + CONTEXT: "${CI_PROJECT_DIR}/docs" + DOCKERFILE_PATH: "Dockerfile" + IMAGE_NAME: "$CI_REGISTRY_IMAGE/docs" + deploy: image: kroniak/ssh-client:3.19 stage: deploy diff --git a/compose.yaml b/compose.yaml index 21501ef..9aac6d8 100644 --- a/compose.yaml +++ b/compose.yaml @@ -356,6 +356,20 @@ services: source: prometheus_data target: /prometheus + docs: + image: gitlab.prodcontest.ru:5050/team-15/project/docs:latest + build: + context: ./docs + dockerfile: Dockerfile + ports: + - name: web + target: 3000 + published: 8008 + host_ip: 0.0.0.0 + protocol: tcp + restart: unless-stopped + shm_size: 4mb + proxy: image: docker.io/nginx:1.27-alpine3.21 configs: diff --git a/docs/Dockerfile b/docs/Dockerfile new file mode 100644 index 0000000..ee2e828 --- /dev/null +++ b/docs/Dockerfile @@ -0,0 +1,26 @@ +# Stage 1: Base image +FROM node:lts AS base + +ENV FORCE_COLOR=0 + +RUN corepack enable + +WORKDIR /opt/docusaurus + +# Stage 2: Production build mode +FROM base AS prod + +WORKDIR /opt/docusaurus + +COPY . /opt/docusaurus/ + +RUN npm ci + +RUN npm run build + +# Stage 3: Serve with docusaurus serve +FROM prod AS serve + +EXPOSE 3000 + +CMD ["npm", "run", "serve", "--", "--host", "0.0.0.0", "--no-open"] diff --git a/infrastructure/nginx/nginx.conf b/infrastructure/nginx/nginx.conf index 6e338eb..dff0f61 100644 --- a/infrastructure/nginx/nginx.conf +++ b/infrastructure/nginx/nginx.conf @@ -110,6 +110,24 @@ http { proxy_read_timeout 600s; } + location /docs { + proxy_pass http://docs:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_cache_bypass $http_upgrade; + proxy_hide_header X-Powered-By; + + proxy_connect_timeout 75s; + proxy_send_timeout 600s; + proxy_read_timeout 600s; + } + location /static { rewrite ^/static/(.*)$ /$1 break; proxy_pass http://backend-staticfiles:80; From 5c813273cf7d6e511a1adaa866c305a4e28a553e Mon Sep 17 00:00:00 2001 From: Timur Date: Sat, 1 Mar 2025 23:09:08 +0300 Subject: [PATCH 05/10] remove prints from auth tests --- services/backend/apps/user/test.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/services/backend/apps/user/test.py b/services/backend/apps/user/test.py index bce54cd..33bbc41 100644 --- a/services/backend/apps/user/test.py +++ b/services/backend/apps/user/test.py @@ -69,7 +69,6 @@ class SignInAPITestCase(TestCase): password=make_password("securepassword123"), username="testuser" ) - print(self.user.password) self.valid_payload = { "email": "valid@example.com", "password": "securepassword123" @@ -81,7 +80,6 @@ class SignInAPITestCase(TestCase): data=json.dumps(self.valid_payload), content_type="application/json" ) - print(make_password(self.valid_payload["password"])) self.assertEqual(response.status_code, 200) self.assertIn("token", response.json()) From 7a787182f37b327ace1e7eae3503e378e595dc82 Mon Sep 17 00:00:00 2001 From: Timur Date: Sat, 1 Mar 2025 23:09:24 +0300 Subject: [PATCH 06/10] add tests on getting competetion by it id --- services/backend/apps/competition/tests.py | 108 +++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 services/backend/apps/competition/tests.py diff --git a/services/backend/apps/competition/tests.py b/services/backend/apps/competition/tests.py new file mode 100644 index 0000000..f9508a2 --- /dev/null +++ b/services/backend/apps/competition/tests.py @@ -0,0 +1,108 @@ +import uuid + +from django.contrib.auth.hashers import make_password +from django.test import TestCase + +from apps.user.models import User +from apps.competition.models import Competition + + +class CompetitionEndpointTests(TestCase): + def setUp(self): + self.user = User.objects.create( + email="user@example.com", + password=make_password("password123"), + username="t1wk4" + ) + + self.competition = Competition.objects.create( + title="AI Challenge", + description="Machine Learning Competition", + type="solo", + participation_type="edu" + ) + + resp = self.client.post( + "/api/v1/sign-in", + data={"email": self.user.email, "password": "password123"}, + content_type="application/json", + ).json() + token = resp["token"] + + self.valid_headers = { + "HTTP_AUTHORIZATION": f"Bearer {token}" + } + + # --- Helper methods --- + def get_url(self, competition_id): + return f"/api/v1/competition/{competition_id}" + + # --- Test Cases --- + def test_get_competition_success(self): + """Authenticated user gets competition details (200 OK)""" + response = self.client.get( + self.get_url(self.competition.id), + **self.valid_headers + ) + + self.assertEqual(response.status_code, 200) + data = response.json() + + # Validate required fields + self.assertEqual(data["id"], str(self.competition.id)) + self.assertEqual(data["title"], "AI Challenge") + self.assertEqual(data["type"], "solo") + + # Validate optional null fields + self.assertIsNone(data["image_url"]) + self.assertIsNone(data["start_date"]) + self.assertIsNone(data["end_date"]) + + def test_invalid_uuid_format(self): + """Invalid UUID format returns 400 Bad Request""" + response = self.client.get( + self.get_url("invalid-id"), + **self.valid_headers + ) + self.assertEqual(response.status_code, 400) + + def test_unauthenticated_access(self): + """Missing auth token returns 401 Unauthorized""" + response = self.client.get(self.get_url(self.competition.id)) + self.assertEqual(response.status_code, 401) + self.assertEqual(response.json()["detail"], "Unauthorized") + + def test_nonexistent_competition(self): + """Valid UUID but missing competition returns 404""" + new_uuid = uuid.uuid4() + response = self.client.get( + self.get_url(new_uuid), + **self.valid_headers + ) + self.assertEqual(response.status_code, 404) + self.assertEqual(response.json()["detail"], "Not Found") + + def test_invalid_auth_token(self): + """Invalid token returns 401 Unauthorized""" + response = self.client.get( + self.get_url(self.competition.id), + HTTP_AUTHORIZATION="Bearer invalid_token" + ) + self.assertEqual(response.status_code, 401) + self.assertEqual(response.json()["detail"], "Unauthorized") + + def test_malformed_auth_header(self): + """Malformed Authorization header returns 401""" + cases = [ + ("InvalidScheme valid_token_123", 401), + ("Bearer", 401), # Missing token + ("", 401), # No header + ] + + for header, expected_status in cases: + with self.subTest(header=header): + response = self.client.get( + self.get_url(self.competition.id), + HTTP_AUTHORIZATION=header + ) + self.assertEqual(response.status_code, expected_status) From a467f24a5de34e981e307d4c3110e0b5b86d1072 Mon Sep 17 00:00:00 2001 From: ITQ Date: Sat, 1 Mar 2025 23:10:30 +0300 Subject: [PATCH 07/10] (scope): [body] [footer(s)] --- docs/docusaurus.config.ts | 8 ++++---- infrastructure/nginx/nginx.conf | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts index bb23c46..a8c42e5 100644 --- a/docs/docusaurus.config.ts +++ b/docs/docusaurus.config.ts @@ -7,7 +7,7 @@ import type * as Preset from '@docusaurus/preset-classic'; const config: Config = { title: 'DataRush', tagline: 'Изучите основы анализа данных здесь!', - favicon: 'https://prod-team-15-2pc0i3lc.final.prodcontest.ru/logo.svg', + favicon: 'https://prod-team-15-2pc0i3lc.final.prodcontest.ru/dr.svg', url: 'https://prod-team-15-2pc0i3lc.final.prodcontest.ru', baseUrl: '/', @@ -36,12 +36,12 @@ const config: Config = { ], themeConfig: { - image: 'https://prod-team-15-2pc0i3lc.final.prodcontest.ru/logo.svg', + image: 'https://prod-team-15-2pc0i3lc.final.prodcontest.ru/dr.svg', navbar: { title: 'DataRush', logo: { - alt: 'My Site Logo', - src: 'https://prod-team-15-2pc0i3lc.final.prodcontest.ru/logo.svg', + alt: 'DataRush', + src: 'https://prod-team-15-2pc0i3lc.final.prodcontest.ru/dr.svg', }, items: [ { diff --git a/infrastructure/nginx/nginx.conf b/infrastructure/nginx/nginx.conf index dff0f61..93a93dc 100644 --- a/infrastructure/nginx/nginx.conf +++ b/infrastructure/nginx/nginx.conf @@ -111,6 +111,7 @@ http { } location /docs { + rewrite ^/docs/(.*)$ /$1 break; proxy_pass http://docs:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; From 9093c0bb90fdf2cc8efb5a1d4e39f4081c2dbe5f Mon Sep 17 00:00:00 2001 From: ITQ Date: Sat, 1 Mar 2025 23:22:20 +0300 Subject: [PATCH 08/10] (scope): [body] [footer(s)] --- docs/docusaurus.config.ts | 2 +- infrastructure/nginx/nginx.conf | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts index a8c42e5..6665170 100644 --- a/docs/docusaurus.config.ts +++ b/docs/docusaurus.config.ts @@ -10,7 +10,7 @@ const config: Config = { favicon: 'https://prod-team-15-2pc0i3lc.final.prodcontest.ru/dr.svg', url: 'https://prod-team-15-2pc0i3lc.final.prodcontest.ru', - baseUrl: '/', + baseUrl: '/docs/', organizationName: 'megazord', projectName: 'megazord', diff --git a/infrastructure/nginx/nginx.conf b/infrastructure/nginx/nginx.conf index 93a93dc..dff0f61 100644 --- a/infrastructure/nginx/nginx.conf +++ b/infrastructure/nginx/nginx.conf @@ -111,7 +111,6 @@ http { } location /docs { - rewrite ^/docs/(.*)$ /$1 break; proxy_pass http://docs:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; From 7e4012bf09c53089a0fbc88f0852df1399cf4f90 Mon Sep 17 00:00:00 2001 From: Timur Date: Sat, 1 Mar 2025 23:24:48 +0300 Subject: [PATCH 09/10] add handler on `user not found` exception --- services/backend/api/v1/auth.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/services/backend/api/v1/auth.py b/services/backend/api/v1/auth.py index 1d36ce9..4284e49 100644 --- a/services/backend/api/v1/auth.py +++ b/services/backend/api/v1/auth.py @@ -19,7 +19,10 @@ class BearerAuth(HttpBearer): except Exception: raise AuthenticationError - user = User.objects.get(id=data["id"]) + try: + user = User.objects.get(id=data["id"]) + except User.DoesNotExist: + raise AuthenticationError return user @staticmethod From 03210c70e470a465d95f39cf79e0a4a9cd276e4c Mon Sep 17 00:00:00 2001 From: ITQ Date: Sat, 1 Mar 2025 23:26:12 +0300 Subject: [PATCH 10/10] (scope): [body] [footer(s)] --- docs/docusaurus.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts index 6665170..758ca67 100644 --- a/docs/docusaurus.config.ts +++ b/docs/docusaurus.config.ts @@ -60,7 +60,7 @@ const config: Config = { items: [ { label: 'Начало', - to: '/docs/intro', + to: '/docs/docs/intro', }, ], },