admin_api + tests

main
Stepan Pilipenko 2 months ago
parent b41a99fd21
commit eb4465fbf4

@ -0,0 +1,62 @@
import psycopg2
from const import DB_CONFIG
def add_product_to_shop(
name: str,
cost: str, # строка, например "100.00" или "50"
count: int,
reserved: int = 0, # по умолчанию 0
picture_url: str = "",
description: str = "",
type: str = ""
) -> int:
connection = psycopg2.connect(**DB_CONFIG)
cursor = connection.cursor()
try:
# Вставка с автоматической генерацией id и возвратом id
cursor.execute("""
INSERT INTO shop (name, cost, count, reserved, picture_url, description, type)
VALUES (%s, %s::NUMERIC, %s, %s, %s, %s, %s)
RETURNING id
""", (name, cost, count, reserved, picture_url, description, type))
product_id = cursor.fetchone()[0]
connection.commit()
return product_id
except psycopg2.Error as e:
print("Ошибка при работе с PostgreSQL:", e)
connection.rollback()
raise
except Exception as e:
print("Ошибка при добавлении товара:", e)
connection.rollback()
raise
finally:
cursor.close()
connection.close()
def delete_product_from_shop(product_id: int) -> bool:
connection = psycopg2.connect(**DB_CONFIG)
cursor = connection.cursor()
try:
# Удаляем товар по id
cursor.execute("DELETE FROM shop WHERE id = %s", (product_id,))
if cursor.rowcount == 0:
raise ValueError(f"Товар с id={product_id} не найден")
connection.commit()
return True
except psycopg2.Error as e:
print("Ошибка при работе с PostgreSQL:", e)
connection.rollback()
raise
except ValueError:
connection.rollback()
raise
finally:
cursor.close()
connection.close()

@ -5,6 +5,7 @@ import json
from datetime import timedelta from datetime import timedelta
from psycopg2 import IntegrityError from psycopg2 import IntegrityError
from collections import Counter from collections import Counter
from decimal import Decimal, InvalidOperation
from utils import * from utils import *
from type import * from type import *
@ -93,7 +94,7 @@ def registration(nickname: str, password: str, email: str) -> str:
token_expiry_date = date.today() + token_live_time token_expiry_date = date.today() + token_live_time
token_expiry_date_str = token_expiry_date.strftime("%Y.%m.%d") token_expiry_date_str = token_expiry_date.strftime("%Y.%m.%d")
money = "100" money = "0"
histories_id = "{}" histories_id = "{}"
connection = psycopg2.connect(**DB_CONFIG) connection = psycopg2.connect(**DB_CONFIG)
@ -118,6 +119,51 @@ def registration(nickname: str, password: str, email: str) -> str:
finally: finally:
cursor.close() cursor.close()
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()
if row is None:
raise ValueError("Пользователь не найден")
histories_id = row[0] # Может быть None или список
# Удаляем все связанные истории, если они есть
if histories_id and len(histories_id) > 0:
# Используем ANY для удаления по массиву ID
cursor.execute("""
DELETE FROM history
WHERE id = ANY(%s::BIGINT[])
""", (histories_id,))
# Удаляем пользователя (корзина удалится автоматически через CASCADE)
cursor.execute("DELETE FROM users WHERE id = %s", (user_id,))
if cursor.rowcount == 0:
raise ValueError("Не удалось удалить пользователя")
connection.commit()
print("Пользователь успешно удалён")
except psycopg2.Error as e:
print("Ошибка при работе с PostgreSQL:", e)
connection.rollback()
raise
except ValueError as e:
connection.rollback()
raise
finally:
cursor.close()
connection.close()
def get_products() -> list[dict]: def get_products() -> list[dict]:
connection = psycopg2.connect(**DB_CONFIG) connection = psycopg2.connect(**DB_CONFIG)
cursor = connection.cursor() cursor = connection.cursor()
@ -170,6 +216,9 @@ def get_basket(token: str):
products_id = res[0] # Это список, например: [1, 1, 3] products_id = res[0] # Это список, например: [1, 1, 3]
if len(products_id) == 0:
return json.dumps([])
# Распаковываем массив и соединяем каждый элемент с таблицей shop # Распаковываем массив и соединяем каждый элемент с таблицей shop
cursor.execute(""" cursor.execute("""
SELECT json_agg(row_to_json(s)) AS result SELECT json_agg(row_to_json(s)) AS result
@ -419,17 +468,34 @@ def buy_products(token: str) -> int:
# 2. Получаем стоимость товаров и проверяем их наличие # 2. Получаем стоимость товаров и проверяем их наличие
product_counts = Counter(products_id) product_counts = Counter(products_id)
total_cost = 0 total_cost = Decimal('0.00')
for product_id in product_counts: for product_id in product_counts:
cursor.execute("SELECT cost::NUMERIC FROM shop WHERE id = %s", (product_id,)) cursor.execute("SELECT cost::NUMERIC FROM shop WHERE id = %s", (product_id,))
shop_row = cursor.fetchone() shop_row = cursor.fetchone()
if shop_row is None: if shop_row is None:
raise ValueError(f"Товар с id={product_id} не найден") raise ValueError(f"Товар с id={product_id} не найден")
cost = shop_row[0] cost = shop_row[0] # Decimal
total_cost += cost * product_counts[product_id] total_cost += cost * product_counts[product_id]
# 3. Вставляем запись в history и получаем её id # 3. Проверяем баланс пользователя
cursor.execute("SELECT money::NUMERIC FROM users WHERE id = %s", (user_id,))
user_row = cursor.fetchone()
if user_row is None:
raise ValueError("Пользователь не найден")
user_money = user_row[0] # Decimal
if user_money < total_cost:
raise ValueError("Недостаточно средств на счету")
# 4. Списываем деньги
new_balance = user_money - total_cost
cursor.execute(
"UPDATE users SET money = %s WHERE id = %s",
(new_balance, user_id)
)
# 5. Вставляем запись в history и получаем её id
cursor.execute(""" cursor.execute("""
INSERT INTO history (user_id, products_id, products_cost, date) INSERT INTO history (user_id, products_id, products_cost, date)
VALUES (%s, %s, %s, CURRENT_DATE) VALUES (%s, %s, %s, CURRENT_DATE)
@ -438,7 +504,7 @@ def buy_products(token: str) -> int:
history_id = cursor.fetchone()[0] # Получаем сгенерированный id history_id = cursor.fetchone()[0] # Получаем сгенерированный id
# 4. Добавляем history_id в users.histories_id # 6. Добавляем history_id в users.histories_id
cursor.execute(""" cursor.execute("""
UPDATE users UPDATE users
SET histories_id = array_append(COALESCE(histories_id, '{}'), %s) SET histories_id = array_append(COALESCE(histories_id, '{}'), %s)
@ -448,7 +514,7 @@ def buy_products(token: str) -> int:
if cursor.rowcount == 0: if cursor.rowcount == 0:
raise ValueError("Не удалось обновить историю пользователя") raise ValueError("Не удалось обновить историю пользователя")
# 5. Уменьшаем reserved в shop для каждого купленного товара # 7. Уменьшаем reserved в shop для каждого купленного товара
for product_id, quantity in product_counts.items(): for product_id, quantity in product_counts.items():
cursor.execute(""" cursor.execute("""
UPDATE shop UPDATE shop
@ -457,7 +523,6 @@ def buy_products(token: str) -> int:
""", (quantity, product_id, quantity)) """, (quantity, product_id, quantity))
if cursor.rowcount == 0: if cursor.rowcount == 0:
# Проверяем, существует ли товар
cursor.execute("SELECT id FROM shop WHERE id = %s", (product_id,)) cursor.execute("SELECT id FROM shop WHERE id = %s", (product_id,))
if cursor.fetchone() is None: if cursor.fetchone() is None:
raise ValueError(f"Товар с id={product_id} не найден") raise ValueError(f"Товар с id={product_id} не найден")
@ -467,11 +532,54 @@ def buy_products(token: str) -> int:
f"попытка списать {quantity}, но reserved < {quantity}" f"попытка списать {quantity}, но reserved < {quantity}"
) )
# 6. Очищаем корзину (products_id = пустой массив) # 8. Очищаем корзину (products_id = пустой массив)
cursor.execute("UPDATE basket SET products_id = '{}' WHERE user_id = %s", (user_id,)) cursor.execute("UPDATE basket SET products_id = '{}' WHERE user_id = %s", (user_id,))
connection.commit() connection.commit()
return history_id # Можно вернуть id заказа, если нужно return history_id
except psycopg2.Error as e:
print("Ошибка при работе с PostgreSQL:", e)
connection.rollback()
raise
except ValueError:
connection.rollback()
raise
finally:
cursor.close()
connection.close()
def add_money(token: str, money: str) -> None:
# Валидация и преобразование входной суммы
try:
# Удаляем возможные пробелы и заменяем запятую на точку (для удобства)
money_clean = money.strip().replace(',', '.')
amount = Decimal(money_clean)
if amount < 0:
raise ValueError("Сумма должна быть неотрицательной")
except (InvalidOperation, ValueError) as e:
raise ValueError("Некорректный формат суммы") from e
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)
# Обновляем баланс: money = money + amount
cursor.execute("""
UPDATE users
SET money = money + %s
WHERE id = %s
""", (amount, user_id))
if cursor.rowcount == 0:
raise ValueError("Пользователь не найден")
connection.commit()
except psycopg2.Error as e: except psycopg2.Error as e:
print("Ошибка при работе с PostgreSQL:", e) print("Ошибка при работе с PostgreSQL:", e)

@ -21,8 +21,11 @@ if __name__ == "__main__":
# print(get_history(token)) # print(get_history(token))
token = login("vasili_pupkin@gmail.com", "Vasya2005") token = login("vasili_pupkin@gmail.com", "Vasya2005")
# add_product_to_basket(token, 1) # add_product_to_basket(token, 3)
# add_product_to_basket(token, 1) # add_product_to_basket(token, 3)
# add_product_to_basket(token, 1) # add_product_to_basket(token, 5)
# add_product_to_basket(token, 2) # add_product_to_basket(token, 5)
print(get_histories(token)) add_money()
# buy_products(token)
# print(get_histories(token))
# unregister(token)

@ -0,0 +1,222 @@
import unittest
import psycopg2
import json # Используем json.loads вместо eval
from api import (
registration,
add_product_to_basket,
buy_products,
get_basket,
get_histories,
unregister,
add_money # Импортируем напрямую
)
from admin_api import add_product_to_shop, delete_product_from_shop
from const import DB_CONFIG
class TestFullUserFlow(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.test_email = "test_user@example.com"
cls.test_password = "secure_password"
cls.test_nickname = "testuser"
cls.token = None
cls.product_ids = []
def test_01_register_user(self):
"""1. Регистрация тестового пользователя"""
self.__class__.token = registration(
nickname=self.test_nickname,
password=self.test_password,
email=self.test_email
)
self.assertIsNotNone(self.token)
print(f"✅ Пользователь зарегистрирован, токен: {self.token}")
def test_02_add_test_products(self):
"""2. Добавление тестовых товаров в shop"""
pid1 = add_product_to_shop(
name="Тестовый товар 1",
cost="50.00",
count=10,
reserved=0,
description="Описание 1",
type="test"
)
pid2 = add_product_to_shop(
name="Тестовый товар 2",
cost="30.00",
count=5,
reserved=0,
description="Описание 2",
type="test"
)
self.__class__.product_ids = [pid1, pid2]
print(f"✅ Товары добавлены: {self.product_ids}")
def test_03_add_to_basket(self):
"""3. Добавление товаров в корзину"""
add_product_to_basket(self.token, self.product_ids[0])
add_product_to_basket(self.token, self.product_ids[0]) # ещё раз
add_product_to_basket(self.token, self.product_ids[1])
print("✅ Товары добавлены в корзину")
def test_04_check_shop_state_after_add_to_basket(self):
"""4. Проверка, что count и reserved изменились корректно"""
conn = psycopg2.connect(**DB_CONFIG)
cur = conn.cursor()
try:
cur.execute("SELECT count, reserved FROM shop WHERE id = %s", (self.product_ids[0],))
count1, reserved1 = cur.fetchone()
self.assertEqual(count1, 8)
self.assertEqual(reserved1, 2)
cur.execute("SELECT count, reserved FROM shop WHERE id = %s", (self.product_ids[1],))
count2, reserved2 = cur.fetchone()
self.assertEqual(count2, 4)
self.assertEqual(reserved2, 1)
print("✅ Состояние магазина корректно после добавления в корзину")
finally:
cur.close()
conn.close()
def test_05_add_money_and_verify(self):
"""5. Добавляем деньги и проверяем баланс"""
# Начальный баланс = 0 (из регистрации)
initial_balance = self._get_user_balance()
self.assertEqual(initial_balance, 0.0)
# Добавляем 50.00
add_money(self.token, "150.00")
# Проверяем новый баланс
new_balance = self._get_user_balance()
self.assertEqual(new_balance, 150.0)
print("✅ Деньги добавлены, баланс = 150.00")
def test_06_buy_products(self):
"""6. Покупка товаров (130.00)"""
total_cost = 50.00 * 2 + 30.00 # 130.00
history_id = buy_products(self.token)
self.assertIsInstance(history_id, int)
print(f"✅ Покупка завершена, history_id: {history_id}")
def test_07_check_user_money_deducted(self):
"""7. Проверка списания денег: 150 - 130 = 20"""
balance = self._get_user_balance()
self.assertAlmostEqual(balance, 20.0, places=2)
print("✅ Деньги списаны корректно, остаток = 20.00")
def test_08_check_shop_after_buy(self):
"""8. Проверка, что reserved = 0 после покупки"""
conn = psycopg2.connect(**DB_CONFIG)
cur = conn.cursor()
try:
for pid in self.product_ids:
cur.execute("SELECT reserved FROM shop WHERE id = %s", (pid,))
reserved = cur.fetchone()[0]
self.assertEqual(reserved, 0)
print("✅ reserved корректно сброшен после покупки")
finally:
cur.close()
conn.close()
def test_09_check_history_created(self):
"""9. Проверка создания истории покупок"""
histories = get_histories(self.token)
self.assertEqual(len(histories), 1)
history = histories[0]
self.assertEqual(history['user_id'], self._get_user_id())
self.assertEqual(history['products_cost'], 130.0)
print("✅ История покупки создана")
def test_10_check_basket_is_empty(self):
"""10. Проверка, что корзина пуста"""
basket = get_basket(self.token)
self.assertEqual(basket, '[]')
print("✅ Корзина пуста")
def test_11_unregister_user(self):
"""11. Удаление пользователя"""
unregister(self.token)
print("✅ Пользователь удалён")
def test_12_check_user_and_related_data_deleted(self):
"""12. Проверка, что пользователь, корзина и история удалены"""
conn = psycopg2.connect(**DB_CONFIG)
cur = conn.cursor()
try:
# Пользователь
cur.execute("SELECT id FROM users WHERE token = %s", (self.token,))
self.assertIsNone(cur.fetchone())
user_id = self._get_user_id_before_delete()
if user_id:
# Корзина (CASCADE)
cur.execute("SELECT user_id FROM basket WHERE user_id = %s", (user_id,))
self.assertIsNone(cur.fetchone())
# История (удалена вручную)
cur.execute("SELECT id FROM history WHERE user_id = %s", (user_id,))
self.assertIsNone(cur.fetchone())
print("✅ Пользователь, корзина и история успешно удалены")
finally:
cur.close()
conn.close()
def test_13_delete_test_products(self):
"""13. Удаление тестовых товаров"""
for pid in self.product_ids:
delete_product_from_shop(pid)
print("✅ Тестовые товары удалены")
def test_14_check_products_deleted(self):
"""14. Проверка, что товары удалены"""
conn = psycopg2.connect(**DB_CONFIG)
cur = conn.cursor()
try:
for pid in self.product_ids:
cur.execute("SELECT id FROM shop WHERE id = %s", (pid,))
self.assertIsNone(cur.fetchone())
print("✅ Товары действительно удалены")
finally:
cur.close()
conn.close()
# Вспомогательные методы
def _get_user_balance(self) -> float:
conn = psycopg2.connect(**DB_CONFIG)
cur = conn.cursor()
try:
cur.execute("SELECT money::NUMERIC FROM users WHERE token = %s", (self.token,))
return float(cur.fetchone()[0])
finally:
cur.close()
conn.close()
def _get_user_id(self) -> int:
conn = psycopg2.connect(**DB_CONFIG)
cur = conn.cursor()
try:
cur.execute("SELECT id FROM users WHERE token = %s", (self.token,))
row = cur.fetchone()
return row[0] if row else None
finally:
cur.close()
conn.close()
def _get_user_id_before_delete(self) -> int:
# После unregister токен недействителен, но мы можем попробовать найти по email
conn = psycopg2.connect(**DB_CONFIG)
cur = conn.cursor()
try:
cur.execute("SELECT id FROM users WHERE email = %s", (self.test_email,))
row = cur.fetchone()
return row[0] if row else None
finally:
cur.close()
conn.close()
if __name__ == '__main__':
unittest.main()
Loading…
Cancel
Save