server_api + aerver_api_tests

main
Stepan Pilipenko 2 months ago
parent 9702735430
commit b90ae70784

@ -1,3 +1,36 @@
# game_shop
Напиши unittest-ы
game_shop
в случае фошибки вернуть JsonResponse({"error": format(error)}, status=500)
1 -http://localhost:8090/shop/
Запрос GET возвращает ответ JsonResponse c формата JsonResponse({"OK": products}, status=200)
пример products:
[{'id': 2, 'name': 'Серебрянный паровоз', 'count': 23, 'reserved': 0, 'picture_url': 'https://git.softwarrior.ru/stepik104/game_shop/raw/branch/main/images/silver_train.png ', 'description': 'Серебро - благородный метал, такой паровоз не стыдно выкатить на рельсы', 'type': 'skin', 'cost': Decimal('50.00')}, {'id': 4, 'name': 'Золотая башня', 'count': 10, 'reserved': 0, 'picture_url': 'https://git.softwarrior.ru/stepik104/game_shop/raw/branch/main/images/golden_tower.png ', 'description': 'Целая башня из чистого золота, кто-то скажет - непрактично, но мы ответим - да', 'type': 'skin', 'cost': Decimal('100.00')}, {'id': 6, 'name': 'Бронзовая башня', 'count': 100, 'reserved': 0, 'picture_url': 'https://git.softwarrior.ru/stepik104/game_shop/raw/branch/main/images/bronze_tower.png ', 'description': 'Бронзовая башня - великая классика, как оловянный солдатик', 'type': 'skin', 'cost': Decimal('10.00')}, {'id': 7, 'name': 'Капибара', 'count': 30, 'reserved': 0, 'picture_url': 'https://git.softwarrior.ru/stepik104/game_shop/raw/branch/main/images/capi_avatar.png ', 'description': 'Почему нет', 'type': 'avatar', 'cost': Decimal('25.00')}, {'id': 9, 'name': 'Штурмовик', 'count': 30, 'reserved': 0, 'picture_url': 'https://git.softwarrior.ru/stepik104/game_shop/raw/branch/main/images/starwars_avatar.png ', 'description': 'Не всегда попадает в цель, зато всегда лоялен императору', 'type': 'avatar', 'cost': Decimal('25.00')}, {'id': 3, 'name': 'Бронзовый паровоз', 'count': 98, 'reserved': 2, 'picture_url': 'https://git.softwarrior.ru/stepik104/game_shop/raw/branch/main/images/bronze_train.png ', 'description': 'Развитие цивилизации начиналось с бронзы, этот паровоз символизирует начало прогресса', 'type': 'skin', 'cost': Decimal('10.00')}, {'id': 8, 'name': 'Капитан-черепаха', 'count': 30, 'reserved': 1, 'picture_url': 'https://git.softwarrior.ru/stepik104/game_shop/raw/branch/main/images/tortule_avatar.png ', 'description': 'Настоящий пират никуда не торопиться', 'type': 'avatar', 'cost': Decimal('25.00')}, {'id': 5, 'name': 'Серебрянная башня', 'count': 18, 'reserved': 2, 'picture_url': 'https://git.softwarrior.ru/stepik104/game_shop/raw/branch/main/images/silver_tower.png ', 'description': 'Серебряная башня, такая серая и холодная, так бы и заточил туда принцессу', 'type': 'skin', 'cost': Decimal('50.00')}, {'id': 1, 'name': 'Золотой паровоз', 'count': 9, 'reserved': 0, 'picture_url': 'https://git.softwarrior.ru/stepik104/game_shop/raw/branch/main/images/golden_train.png ', 'description': 'Золотой, блестящий, ну разве не чудо', 'type': 'skin', 'cost': Decimal('100.00')}, {'id': 28, 'name': 'Тестовый товар 1', 'count': 8, 'reserved': 0, 'picture_url': '', 'description': 'Описание 1', 'type': 'test', 'cost': Decimal('50.00')}, {'id': 29, 'name': 'Тестовый товар 2', 'count': 4, 'reserved': 0, 'picture_url': '', 'description': 'Описание 2', 'type': 'test', 'cost': Decimal('30.00')}]
Проверить что не пустой
2 - http://localhost:8090/login/
Запрос POST с body
login: str
password: str
email: str
возвращает JsonResponse({"OK": token}, status=200)
пример token
token формата: uuid.uuid4(), пример: 44decdbc-7120-4838-abf8-7528a11467a9
3 - http://localhost:8090/logout/
Запрос POST с header:
Token: str пример: 44decdbc-7120-4838-abf8-7528a11467a9
возвращает JsonResponse({"OK": token}, status=200)
4 - http://localhost:8090/basket/
Запрос POST с header:
Token: str пример: 44decdbc-7120-4838-abf8-7528a11467a9
возвращает JsonResponse({"OK": basket}, status=200)
ошибка если токен невалидный, напиши тест на невалидный токен
5 - http://localhost:8090/history/
Запрос POST с header:
Token: str пример: 44decdbc-7120-4838-abf8-7528a11467a9
возвращает JsonResponse({"OK": histories}, status=200)
ошибка если токен невалидный, напиши тест на невалидный токен

@ -0,0 +1,4 @@
from .api import *
from .type import *
from .consts import *
from .utils import *

@ -1,6 +1,6 @@
import psycopg2
from consts import DB_CONFIG
from ..consts import DB_CONFIG
def add_product_to_shop(
name: str,

@ -7,9 +7,9 @@ from psycopg2 import IntegrityError
from collections import Counter
from decimal import Decimal, InvalidOperation
from utils import *
from type import *
from consts import *
from ..utils import *
from ..type import *
from ..consts import *
def update_token_expiry_date(token: str, expiry_date: date) -> None:
connection = psycopg2.connect(**DB_CONFIG)
@ -123,11 +123,7 @@ def unregister(token: str) -> None:
connection = psycopg2.connect(**DB_CONFIG)
cursor = connection.cursor()
try:
if not check_token(cursor, token):
raise AuthError("Пользователь не авторизован")
user_id: int = get_user_id(cursor, token)
# Получаем массив histories_id пользователя
cursor.execute("SELECT histories_id FROM users WHERE id = %s", (user_id,))
row = cursor.fetchone()

@ -1 +1 @@
from .const import *
from .consts import *

@ -1,11 +1,14 @@
#from django.contrib import admin
from django.urls import path
from server.views import (get_shop, get_user, get_basket, get_history)
from .views import (get_shop, login, logout, register, unregister, get_basket, get_history)
urlpatterns = [
path("shop/", get_shop, name="get_shop"),
path("user/", get_user, name="get_user"),
path("login/", login, name="login"),
path("logout/", logout, name="logout"),
path("register/", register, name="register"),
path("unregister/", unregister, name="unregister"),
path("basket/", get_basket, name="get_basket"),
path("history/", get_history, name="get_history"),
]

@ -2,15 +2,15 @@ import json
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from api import get_products
from utils import decimal_to_float
from app.api import api
from app.utils import decimal_to_float
@csrf_exempt
async def get_shop(request):
try:
products = None
if request.method == 'GET':
products = get_products()
products = api.get_products()
user_agent = request.headers.get('User-Agent')
products = decimal_to_float(products)
print("get_shop", user_agent)
@ -19,35 +19,68 @@ async def get_shop(request):
return JsonResponse({"ERROR": format(error)}, status=500)
@csrf_exempt
async def get_user(request):
async def login(request):
try:
token = None
if request.method == 'POST':
body: dict = json.loads(request.body)
# TODO: вызвать API метод и вернуть JSON
print("get_user", body)
return JsonResponse({"OK": "JSON cleared"}, status=200)
token = api.login(body["email"], body["password"])
return JsonResponse({"OK": token}, status=200)
except Exception as error:
return JsonResponse({"error": format(error)}, status=500)
@csrf_exempt
async def get_basket(request):
async def logout(request):
try:
token = None
if request.method == 'POST':
token = request.headers.get("Token")
api.logout(token)
return JsonResponse({"OK": token}, status=200)
except Exception as error:
return JsonResponse({"error": format(error)}, status=500)
@csrf_exempt
async def register(request):
try:
token = None
if request.method == 'POST':
body: dict = json.loads(request.body)
# TODO: вызвать API метод и вернуть JSON
print("get_basket", body)
return JsonResponse({"OK": "JSON cleared"}, status=200)
token = api.registration(body["nickname"], body["password"], body["email"])
return JsonResponse({"OK": token}, status=200)
except Exception as error:
return JsonResponse({"error": format(error)}, status=500)
@csrf_exempt
async def unregister(request):
try:
token = None
if request.method == 'POST':
token = request.headers.get("Token")
api.unregister(token)
return JsonResponse({"OK": token}, status=200)
except Exception as error:
return JsonResponse({"error": format(error)}, status=500)
@csrf_exempt
async def get_basket(request):
try:
basket = None
if request.method == 'POST':
token = request.headers.get("Token")
basket = api.get_basket(token)
return JsonResponse({"OK": basket}, status=200)
except Exception as error:
return JsonResponse({"error": format(error)}, status=500)
@csrf_exempt
async def get_history(request):
try:
histories = None
if request.method == 'POST':
body: dict = json.loads(request.body)
# TODO: вызвать API метод и вернуть JSON
print("get_history", body)
return JsonResponse({"OK": "JSON cleared"}, status=200)
token = request.headers.get("Token")
histories = api.get_histories(token)
return JsonResponse({"OK": histories}, status=200)
except Exception as error:
return JsonResponse({"error": format(error)}, status=500)

@ -1,16 +1,17 @@
import unittest
import psycopg2
from api import (
from app.api import (
registration,
add_product_to_basket,
buy_products,
get_basket,
get_histories,
unregister,
get_products,
add_money # Импортируем напрямую
)
from api import add_product_to_shop, delete_product_from_shop
from consts import DB_CONFIG
from app.api import add_product_to_shop, delete_product_from_shop
from app.consts import *
class TestFullUserFlow(unittest.TestCase):
@ -182,6 +183,19 @@ class TestFullUserFlow(unittest.TestCase):
cur.close()
conn.close()
def test_15_check_products(self):
"""15. Проверка, что товары есть"""
conn = psycopg2.connect(**DB_CONFIG)
cur = conn.cursor()
try:
products = get_products()
print(products)
self.assertIsNotNone(products)
print("✅ Товары действительно есть")
finally:
cur.close()
conn.close()
# Вспомогательные методы
def _get_user_balance(self) -> float:
conn = psycopg2.connect(**DB_CONFIG)

@ -0,0 +1,182 @@
import unittest
import requests
import uuid
BASE_URL = "http://localhost:8090"
def get_error_message(response: requests.Response) -> str:
"""Извлекает сообщение об ошибке из ответа сервера или возвращает понятную заглушку."""
try:
if response.headers.get('Content-Type', '').startswith('application/json'):
data = response.json()
return data.get("error", "No 'error' field in JSON response")
else:
return f"Non-JSON response: {response.text[:200]}"
except Exception as e:
return f"Failed to parse error: {e} | Raw: {response.text[:200]}"
class TestGameShopAPI(unittest.TestCase):
valid_token = None
test_nickname = "autotest_user"
test_password = "secure_password_123"
test_email = "autotest@example.com"
@classmethod
def setUpClass(cls):
"""Регистрируем пользователя один раз перед всеми тестами."""
super().setUpClass()
register_payload = {
"nickname": cls.test_nickname,
"password": cls.test_password,
"email": cls.test_email
}
try:
response = requests.post(f"{BASE_URL}/register/", json=register_payload, timeout=10)
if response.status_code != 200:
error_msg = get_error_message(response)
raise RuntimeError(f"Registration failed ({response.status_code}): {error_msg}")
data = response.json()
cls.valid_token = data.get("OK")
if not cls.valid_token:
raise ValueError("Response missing 'OK' field with token")
uuid.UUID(cls.valid_token)
except Exception as e:
raise unittest.SkipTest(f"Не удалось зарегистрировать пользователя: {e}")
@classmethod
def tearDownClass(cls):
"""Удаляем пользователя после всех тестов."""
if cls.valid_token:
try:
headers = {"Token": cls.valid_token}
response = requests.post(f"{BASE_URL}/unregister/", headers=headers, timeout=10)
if response.status_code != 200:
error_msg = get_error_message(response)
print(f"⚠️ Внимание: unregister завершился с кодом {response.status_code}: {error_msg}")
except Exception as e:
print(f"⚠️ Ошибка при unregister: {e}")
super().tearDownClass()
def test_1_get_shop_products(self):
"""GET /shop/ — должен вернуть непустой список товаров"""
response = requests.get(f"{BASE_URL}/shop/")
if response.status_code != 200:
error_msg = get_error_message(response)
self.fail(f"GET /shop/ failed ({response.status_code}): {error_msg}")
data = response.json()
self.assertIn("OK", data, f"Ответ не содержит 'OK'. Получено: {data}")
products = data["OK"]
self.assertIsInstance(products, list, f"'OK' должен быть списком, получено: {type(products)}")
self.assertGreater(len(products), 0, "Список товаров пуст")
product = products[0]
required = {"id", "name", "count", "reserved", "picture_url", "description", "type", "cost"}
missing = required - set(product.keys())
self.assertFalse(missing, f"В товаре отсутствуют поля: {missing}. Товар: {product}")
self.assertIsInstance(product["cost"], (int, float), f"'cost' должен быть числом, получено: {type(product['cost'])}")
def test_2_registration_returns_valid_token(self):
"""Проверка, что токен — валидный UUID"""
self.assertIsNotNone(self.valid_token, "Токен не был получен при регистрации")
try:
uuid.UUID(self.valid_token)
except ValueError:
self.fail(f"Токен не является валидным UUID: {self.valid_token}")
def test_3_basket_success(self):
"""POST /basket/ с валидным токеном → возвращает '[]'"""
headers = {"Token": self.valid_token}
response = requests.post(f"{BASE_URL}/basket/", headers=headers)
if response.status_code != 200:
error_msg = get_error_message(response)
self.fail(f"POST /basket/ failed ({response.status_code}): {error_msg}")
data = response.json()
self.assertIn("OK", data, f"Ответ не содержит 'OK'. Получено: {data}")
self.assertEqual(data["OK"], '[]', f"Ожидалась пустая корзина ('[]'), получено: {data['OK']}")
def test_4_history_success(self):
"""POST /history/ с валидным токеном → возвращает '[]'"""
headers = {"Token": self.valid_token}
response = requests.post(f"{BASE_URL}/history/", headers=headers)
if response.status_code != 200:
error_msg = get_error_message(response)
self.fail(f"POST /history/ failed ({response.status_code}): {error_msg}")
data = response.json()
self.assertIn("OK", data, f"Ответ не содержит 'OK'. Получено: {data}")
self.assertEqual(data["OK"], '[]', f"Ожидалась пустая история ('[]'), получено: {data['OK']}")
def test_5_basket_invalid_token(self):
"""POST /basket/ с невалидным токеном → ошибка 500"""
headers = {"Token": "invalid-token-12345"}
response = requests.post(f"{BASE_URL}/basket/", headers=headers)
if response.status_code != 500:
error_msg = get_error_message(response)
self.fail(f"Ожидался статус 500 для невалидного токена, получен {response.status_code}. Ошибка: {error_msg}")
data = response.json()
self.assertIn("error", data, f"Ответ должен содержать 'error', получено: {data}")
self.assertIsInstance(data["error"], str, f"'error' должен быть строкой, получено: {type(data['error'])}")
self.assertGreater(len(data["error"]), 0, "Сообщение об ошибке пустое")
def test_6_history_invalid_token(self):
"""POST /history/ с невалидным токеном → ошибка 500"""
headers = {"Token": str(uuid.uuid4())}
response = requests.post(f"{BASE_URL}/history/", headers=headers)
if response.status_code != 500:
error_msg = get_error_message(response)
self.fail(f"Ожидался статус 500 для невалидного токена, получен {response.status_code}. Ошибка: {error_msg}")
data = response.json()
self.assertIn("error", data, f"Ответ должен содержать 'error', получено: {data}")
def test_7_login_success(self):
"""POST /login/ — успешная аутентификация зарегистрированного пользователя"""
login_payload = {
"email": self.test_email,
"password": self.test_password
}
response = requests.post(f"{BASE_URL}/login/", json=login_payload)
if response.status_code != 200:
error_msg = get_error_message(response)
self.fail(f"POST /login/ failed ({response.status_code}): {error_msg}")
data = response.json()
self.assertIn("OK", data, f"Ответ не содержит 'OK'. Получено: {data}")
login_token = data["OK"]
try:
uuid.UUID(login_token)
except ValueError:
self.fail(f"Токен от /login/ не является валидным UUID: {login_token}")
# Проверка, что токен работает
basket_resp = requests.post(f"{BASE_URL}/basket/", headers={"Token": login_token})
if basket_resp.status_code != 200:
error_msg = get_error_message(basket_resp)
self.fail(f"Токен от /login/ не работает: {error_msg}")
def test_8_logout_success(self):
"""POST /logout/ с валидным токеном — успешный выход"""
headers = {"Token": self.valid_token}
response = requests.post(f"{BASE_URL}/logout/", headers=headers)
if response.status_code != 200:
error_msg = get_error_message(response)
self.fail(f"POST /logout/ failed ({response.status_code}): {error_msg}")
data = response.json()
self.assertIn("OK", data, f"Ответ не содержит 'OK'. Получено: {data}")
self.assertEqual(data["OK"], self.valid_token, "В ответе должен быть тот же токен")
def test_9_all_scenarios_completed(self):
"""Финальная проверка: все тесты пройдены"""
pass
if __name__ == "__main__":
unittest.main(verbosity=2)
Loading…
Cancel
Save