Merge branch 'master' of gitlab.prodcontest.ru:team-15/project

This commit is contained in:
moolcoov
2025-03-01 23:26:52 +03:00
10 changed files with 398 additions and 30 deletions
+9
View File
@@ -54,6 +54,15 @@ build_backend-staticfiles:
DOCKERFILE_PATH: "Dockerfile.staticfiles" DOCKERFILE_PATH: "Dockerfile.staticfiles"
IMAGE_NAME: "$CI_REGISTRY_IMAGE/backend-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: deploy:
image: kroniak/ssh-client:3.19 image: kroniak/ssh-client:3.19
stage: deploy stage: deploy
+14
View File
@@ -356,6 +356,20 @@ services:
source: prometheus_data source: prometheus_data
target: /prometheus 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: proxy:
image: docker.io/nginx:1.27-alpine3.21 image: docker.io/nginx:1.27-alpine3.21
configs: configs:
+26
View File
@@ -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"]
+6 -6
View File
@@ -7,10 +7,10 @@ import type * as Preset from '@docusaurus/preset-classic';
const config: Config = { const config: Config = {
title: 'DataRush', title: 'DataRush',
tagline: 'Изучите основы анализа данных здесь!', 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', url: 'https://prod-team-15-2pc0i3lc.final.prodcontest.ru',
baseUrl: '/', baseUrl: '/docs/',
organizationName: 'megazord', organizationName: 'megazord',
projectName: 'megazord', projectName: 'megazord',
@@ -36,12 +36,12 @@ const config: Config = {
], ],
themeConfig: { themeConfig: {
image: 'https://prod-team-15-2pc0i3lc.final.prodcontest.ru/logo.svg', image: 'https://prod-team-15-2pc0i3lc.final.prodcontest.ru/dr.svg',
navbar: { navbar: {
title: 'DataRush', title: 'DataRush',
logo: { logo: {
alt: 'My Site Logo', alt: 'DataRush',
src: 'https://prod-team-15-2pc0i3lc.final.prodcontest.ru/logo.svg', src: 'https://prod-team-15-2pc0i3lc.final.prodcontest.ru/dr.svg',
}, },
items: [ items: [
{ {
@@ -60,7 +60,7 @@ const config: Config = {
items: [ items: [
{ {
label: 'Начало', label: 'Начало',
to: '/docs/intro', to: '/docs/docs/intro',
}, },
], ],
}, },
+18
View File
@@ -110,6 +110,24 @@ http {
proxy_read_timeout 600s; 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 { location /static {
rewrite ^/static/(.*)$ /$1 break; rewrite ^/static/(.*)$ /$1 break;
proxy_pass http://backend-staticfiles:80; proxy_pass http://backend-staticfiles:80;
+4 -1
View File
@@ -19,7 +19,10 @@ class BearerAuth(HttpBearer):
except Exception: except Exception:
raise AuthenticationError raise AuthenticationError
user = User.objects.get(id=data["id"]) try:
user = User.objects.get(id=data["id"])
except User.DoesNotExist:
raise AuthenticationError
return user return user
@staticmethod @staticmethod
+108
View File
@@ -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)
+4 -3
View File
@@ -14,10 +14,11 @@ class User(BaseModel):
username = models.SlugField(unique=True, verbose_name="юзернейм") username = models.SlugField(unique=True, verbose_name="юзернейм")
password = models.TextField(verbose_name="пароль") password = models.TextField(verbose_name="пароль")
def make_password(self): @staticmethod
return make_password(self.password) 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) return check_password(self.password, password)
status = models.CharField( status = models.CharField(
+208 -20
View File
@@ -1,29 +1,217 @@
from django.core.exceptions import ValidationError import json
import uuid
from django.test import TestCase from django.test import TestCase
from django.contrib.auth.hashers import make_password
from apps.user.models import User from apps.user.models import User
class TestSignUp(TestCase): class SignUpAPITestCase(TestCase):
def test_correct_signup(self): def test_successful_sign_up(self):
user = User( payload = {
email="123123@timka.su", "email": "user@example.com",
password="1321312", "password": "securepassword123",
username="123123", "username": "123",
}
response = self.client.post(
"/api/v1/sign-up",
data=json.dumps(payload),
content_type="application/json",
) )
user.full_clean() self.assertEqual(response.status_code, 201)
user.save() self.assertIn("token", response.json())
self.assertEqual(User.objects.count(), 1)
def test_incorrect_mail(self): def test_missing_required_fields(self):
user = User( payload = {"password": "testpass123", "username": "sffsdf"}
email="123123", response = self.client.post(
password="1321312", "/api/v1/sign-up",
username="123123123", data=json.dumps(payload),
content_type="application/json",
) )
with self.assertRaises(ValidationError): self.assertEqual(response.status_code, 400)
user.full_clean()
def test_missing_params(self): def test_invalid_email_format(self):
user = User(password="123123", username="132131232131") payload = {
with self.assertRaises(ValidationError): "email": "ervtb uktr bym",
user.full_clean() "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())
class SignInAPITestCase(TestCase):
def setUp(self):
self.user = User.objects.create(
email="valid@example.com",
password=make_password("securepassword123"),
username="testuser"
)
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"
)
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")
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")
+1
View File
@@ -485,6 +485,7 @@ LANGUAGE_COOKIE_AGE = 31449600
PASSWORD_HASHERS = [ PASSWORD_HASHERS = [
"django.contrib.auth.hashers.Argon2PasswordHasher", "django.contrib.auth.hashers.Argon2PasswordHasher",
"django.contrib.auth.hashers.ScryptPasswordHasher",
] ]
LANGUAGE_COOKIE_DOMAIN = None LANGUAGE_COOKIE_DOMAIN = None