mirror of
https://gitlab.com/megazordpobeda/DataRush.git
synced 2026-05-23 02:47:10 +00:00
Merge branch 'master' of gitlab.prodcontest.ru:team-15/project
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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"]
|
||||||
@@ -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',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -19,7 +19,10 @@ class BearerAuth(HttpBearer):
|
|||||||
except Exception:
|
except Exception:
|
||||||
raise AuthenticationError
|
raise AuthenticationError
|
||||||
|
|
||||||
|
try:
|
||||||
user = User.objects.get(id=data["id"])
|
user = User.objects.get(id=data["id"])
|
||||||
|
except User.DoesNotExist:
|
||||||
|
raise AuthenticationError
|
||||||
return user
|
return user
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user