Showcase update - f20be312
Quote Bot CI/CD Pipeline / check (push) Waiting to run
Quote Bot CI/CD Pipeline / Security Scan (push) Waiting to run
Quote Bot CI/CD Pipeline / Build and push to Docker Hub (push) Blocked by required conditions
Quote Bot CI/CD Pipeline / Deploy to VPS via Ansible (push) Blocked by required conditions
Quote Bot CI/CD Pipeline / check (push) Waiting to run
Quote Bot CI/CD Pipeline / Security Scan (push) Waiting to run
Quote Bot CI/CD Pipeline / Build and push to Docker Hub (push) Blocked by required conditions
Quote Bot CI/CD Pipeline / Deploy to VPS via Ansible (push) Blocked by required conditions
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
.github/
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
TOKEN=your_token_here
|
||||
|
||||
ADMIN_IDS=123456789,987654321
|
||||
@@ -0,0 +1,96 @@
|
||||
name: Quote Bot CI/CD Pipeline
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
check-syntax:
|
||||
name: check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Syntax Check
|
||||
run: |
|
||||
pip install pyflakes
|
||||
pyflakes src/
|
||||
|
||||
security-scan:
|
||||
name: Security Scan
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run OWASP Noir
|
||||
id: noir
|
||||
uses: owasp-noir/noir@v1.1.0
|
||||
with:
|
||||
base_path: 'src'
|
||||
- name: Display results
|
||||
run: echo '${{ steps.noir.outputs.endpoints }}' | jq .
|
||||
- name: Run Bandit (Python SAST)
|
||||
run: |
|
||||
pip install bandit
|
||||
bandit -r src/
|
||||
|
||||
build-and-push:
|
||||
name: Build and push to Docker Hub
|
||||
needs: [security-scan, check-syntax]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Скачиваем код бота
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Логинимся в Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_LOGIN }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
- name: Сборка и пуш Docker образа
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: leor156/quote-bot:latest
|
||||
|
||||
#terraform infrastructure-check and infrastructure-deployment:
|
||||
#name: check infrastructure
|
||||
#needs: [build-and-push]
|
||||
#runs-on: ubuntu-latest
|
||||
#steps:
|
||||
#- uses: actions/checkout@v4
|
||||
#- name: Terraform init
|
||||
#run: terraform init
|
||||
#- name: Terraform plan
|
||||
#run: terraform plan
|
||||
#- name: Terraform apply
|
||||
#run: terraform apply -auto-approve
|
||||
|
||||
deploy:
|
||||
name: Deploy to VPS via Ansible
|
||||
needs: build-and-push
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Скачиваем код репозитория (чтобы получить файл deploy.yml)
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Запускаем Ansible плейбук
|
||||
uses: dawidd6/action-ansible-playbook@v2
|
||||
with:
|
||||
playbook: auto-deployment/A_deploy.yml
|
||||
key: ${{ secrets.SERVER_SSH_KEY }}
|
||||
options: |
|
||||
-i ${{ secrets.SERVER_HOST }},
|
||||
-u ${{ secrets.SERVER_USER }}
|
||||
--extra-vars "TOKEN=${{ secrets.TG_TOKEN_QUOTE }} ADMIN_IDS=${{ secrets.ADMIN_ID_TG }}"
|
||||
|
||||
- name: Notifier
|
||||
uses: ethyaan/tgate-action@v1.0.0
|
||||
if: always()
|
||||
with:
|
||||
token: ${{ secrets.TG_TOKEN_ALERT }}
|
||||
to: ${{ secrets.ADMIN_ID_TG }}
|
||||
thread_id: ${{ secrets.threadid }}
|
||||
disable_web_page_preview: false
|
||||
disable_notification: false
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
.env
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
|
||||
# Terraform
|
||||
authorized_key.json
|
||||
.terraform/
|
||||
.terraform.lock.hcl
|
||||
*.tfstate
|
||||
*.tfstate.backup
|
||||
*.tfvars
|
||||
*.tfvars.json
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
|
||||
# Python venv
|
||||
venv/
|
||||
.venv/
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
default:
|
||||
tags:
|
||||
- universal
|
||||
|
||||
stages:
|
||||
- tests
|
||||
- build-and-push
|
||||
- deploy
|
||||
|
||||
tests_job1:
|
||||
stage: tests
|
||||
image: python:3.11-slim
|
||||
variables:
|
||||
PYTHONPATH: "."
|
||||
before_script:
|
||||
- pip install --no-cache-dir -r requirements.txt
|
||||
- pip install pytest
|
||||
script:
|
||||
- pytest tests/
|
||||
|
||||
push_job:
|
||||
resource_group: production
|
||||
stage: build-and-push
|
||||
image:
|
||||
name: gcr.io/kaniko-project/executor:v1.23.0-debug
|
||||
entrypoint: [""]
|
||||
before_script:
|
||||
- mkdir -p /kaniko/.docker
|
||||
- cp "$DOCKER_AUTH_CONFIG" /kaniko/.docker/config.json
|
||||
script:
|
||||
- >
|
||||
/kaniko/executor
|
||||
--context "${CI_PROJECT_DIR}"
|
||||
--dockerfile "${CI_PROJECT_DIR}/Dockerfile"
|
||||
--destination "${DOCKERHUB_USER}/quote-bot:${CI_COMMIT_SHORT_SHA}"
|
||||
--destination "${DOCKERHUB_USER}/quote-bot:latest"
|
||||
|
||||
deploy_to_server:
|
||||
resource_group: production
|
||||
stage: deploy
|
||||
image: ghcr.io/ansible/creator-ee:v0.22.0
|
||||
variables:
|
||||
ANSIBLE_HOST_KEY_CHECKING: "False"
|
||||
before_script:
|
||||
- mkdir -p ~/.ssh
|
||||
- chmod 700 ~/.ssh
|
||||
- echo "$ANSIBLE_PRIVATE_KEY" > ~/.ssh/id_ed25519
|
||||
- chmod 600 ~/.ssh/id_ed25519
|
||||
- ansible-galaxy collection install community.docker
|
||||
script:
|
||||
- cd auto-deployment/
|
||||
- ansible-playbook -i inventory.ini A_deploy.yml
|
||||
--extra-vars "image_tag=$CI_COMMIT_SHORT_SHA"
|
||||
|
||||
environment:
|
||||
name: production
|
||||
url: http://$BOT_SERVER_IP
|
||||
on_stop: stop_production
|
||||
|
||||
stop_production:
|
||||
stage: deploy
|
||||
image: ghcr.io/ansible/creator-ee:v0.22.0
|
||||
variables:
|
||||
ANSIBLE_HOST_KEY_CHECKING: "False"
|
||||
before_script:
|
||||
- ansible-galaxy collection install community.docker
|
||||
- mkdir -p ~/.ssh
|
||||
- chmod 700 ~/.ssh
|
||||
- echo "$ANSIBLE_PRIVATE_KEY" > ~/.ssh/id_ed25519
|
||||
- chmod 600 ~/.ssh/id_ed25519
|
||||
script:
|
||||
- cd auto-deployment/
|
||||
- ansible-playbook -i inventory.ini A_deploy.yml
|
||||
--extra-vars "container_state=absent"
|
||||
when: manual
|
||||
environment:
|
||||
name: production
|
||||
action: stop
|
||||
|
||||
|
||||
variables:
|
||||
MSG_SUCCESS: "✅ <b>Успех!</b> Пайплайн проекта <b>$CI_PROJECT_NAME</b> завершен.\nВетка: $CI_COMMIT_REF_NAME\nКоммит: $CI_COMMIT_MESSAGE"
|
||||
MSG_FAILURE: "❌ <b>Ошибка!</b> Пайплайн проекта <b>$CI_PROJECT_NAME</b> упал.\nВетка: $CI_COMMIT_REF_NAME\n<a href='$CI_PIPELINE_URL'>Смотреть логи пайплайна</a>"
|
||||
|
||||
.notify_telegram:
|
||||
stage: .post
|
||||
tags:
|
||||
- tg
|
||||
image: curlimages/curl:latest
|
||||
script:
|
||||
- |
|
||||
curl -s -X POST "https://api.telegram.org/bot$ALERT_TG_BOT/sendMessage" \
|
||||
-d chat_id="$ADMIN_ID_TG" \
|
||||
-d text="$MESSAGE" \
|
||||
-d parse_mode="HTML" \
|
||||
-d disable_web_page_preview="true"
|
||||
|
||||
notify_success:
|
||||
extends: .notify_telegram
|
||||
variables:
|
||||
MESSAGE: $MSG_SUCCESS
|
||||
when: on_success
|
||||
|
||||
notify_failure:
|
||||
extends: .notify_telegram
|
||||
variables:
|
||||
MESSAGE: $MSG_FAILURE
|
||||
when: on_failure
|
||||
|
||||
mirror_to_gitea:
|
||||
image:
|
||||
name: alpine/git:latest
|
||||
entrypoint: [""]
|
||||
stage: .post
|
||||
variables:
|
||||
GIT_DEPTH: 1
|
||||
script:
|
||||
- git config user.name "GitLab CI/CD"
|
||||
- git config user.email "ci@leor156.online"
|
||||
- git checkout --orphan showcase
|
||||
- git add -A
|
||||
- git commit -m "Showcase update - ${CI_COMMIT_SHORT_SHA}"
|
||||
- git remote add gitea https://leo:$GITEA_TOKEN@gitea.leor156.online/leo/quote_bot_auto-deployment.git
|
||||
- git push gitea showcase:main --force
|
||||
when: on_success
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
FROM python:3.13-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
|
||||
COPY src/ ./src/
|
||||
|
||||
|
||||
COPY data/ ./data/
|
||||
|
||||
CMD ["python", "-m", "src.bot"]
|
||||
@@ -0,0 +1,39 @@
|
||||
# deploy.yml
|
||||
- name: Автоматический деплой Telegram-бота
|
||||
hosts: all
|
||||
|
||||
vars:
|
||||
image_name: "leor156/quote-bot:{{ image_tag | default('latest') }}"
|
||||
container_name: "quote-bot"
|
||||
container_state: "started"
|
||||
|
||||
tasks:
|
||||
- name: Убеждаемся, что установлена библиотека Docker для Python
|
||||
apt:
|
||||
name: python3-docker
|
||||
state: present
|
||||
update_cache: yes
|
||||
|
||||
- name: Скачиваем образ бота с Docker Hub
|
||||
community.docker.docker_image:
|
||||
name: "{{ image_name }}"
|
||||
source: pull
|
||||
|
||||
- name: Запускаем контейнер
|
||||
community.docker.docker_container:
|
||||
name: "{{ container_name }}"
|
||||
image: "{{ image_name }}"
|
||||
state: "{{ container_state }}"
|
||||
restart_policy: unless-stopped
|
||||
pull: always
|
||||
env:
|
||||
TOKEN: "{{ lookup('env', 'BOT_TOKEN') }}"
|
||||
ADMIN_IDS: "{{ lookup('env', 'ADMIN_ID_TG') }}"
|
||||
|
||||
- name: Чистка
|
||||
community.docker.docker_prune:
|
||||
images: yes
|
||||
images_filters:
|
||||
dangling: true
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
[server-bot]
|
||||
quote-bot-prod ansible_host="{{ lookup('env', 'BOT_SERVER_IP') }}" ansible_user=root ansible_ssh_private_key_file=~/.ssh/id_ed25519
|
||||
@@ -0,0 +1,44 @@
|
||||
resource "yandex_compute_filesystem" "shared_disk" {
|
||||
name = "my-shared-30gb-disk"
|
||||
type = "network-hdd"
|
||||
size = 30
|
||||
zone = "ru-central1-d"
|
||||
}
|
||||
|
||||
data "yandex_compute_image" "ubuntu" {
|
||||
family = "ubuntu-2404-lts"
|
||||
}
|
||||
|
||||
resource "yandex_compute_instance" "servers" {
|
||||
count = 2
|
||||
name = "server-${count.index + 1}"
|
||||
platform_id = "standard-v3"
|
||||
zone = "ru-central1-d"
|
||||
|
||||
resources {
|
||||
cores = 2
|
||||
memory = 2
|
||||
}
|
||||
|
||||
|
||||
|
||||
# Указываем загрузочный диск с Ubuntu
|
||||
boot_disk {
|
||||
initialize_params {
|
||||
image_id = data.yandex_compute_image.ubuntu.id
|
||||
size = 10 # Размер загрузочного диска
|
||||
}
|
||||
}
|
||||
# Подключаем наш общий диск на 30 ГБ
|
||||
filesystem {
|
||||
filesystem_id = yandex_compute_filesystem.shared_disk.id
|
||||
device_name = "shared-disk"
|
||||
}
|
||||
# Подключаем сервер к подсети (которая лежит в файле network.tf)
|
||||
network_interface {
|
||||
subnet_id = yandex_vpc_subnet.subnet.id
|
||||
|
||||
# Даем белый IP только первому серверу
|
||||
nat = count.index == 0 ? true : false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
resource "yandex_vpc_network" "network" {
|
||||
name = "my-network"
|
||||
}
|
||||
|
||||
resource "yandex_vpc_subnet" "subnet" {
|
||||
name = "my-internal-subnet"
|
||||
v4_cidr_blocks = ["10.0.0.0/24"]
|
||||
zone = "ru-central1-d"
|
||||
network_id = yandex_vpc_network.network.id
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
output "public_ip_server_1" {
|
||||
value = yandex_compute_instance.servers[0].network_interface.0.nat_ip_address
|
||||
description = "Белый IP первого сервера"
|
||||
}
|
||||
output "internal_ips" {
|
||||
value = yandex_compute_instance.servers[*].network_interface.0.ip_address
|
||||
description = "Внутренние IP адреса обоих серверов"
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
yandex = {
|
||||
source = "yandex-cloud/yandex"
|
||||
}
|
||||
}
|
||||
required_version = ">= 0.13"
|
||||
}
|
||||
provider "yandex" {
|
||||
service_account_key_file = "authorized_key.json"
|
||||
cloud_id = "b1gl9hdpdr35i83uh5t5"
|
||||
folder_id = "b1gbb5kej5tlskp8i5oq"
|
||||
zone = "ru-central1-a"
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
variable "zone" {
|
||||
type = string
|
||||
default = "ru-central1-d"
|
||||
}
|
||||
|
||||
variable "network" {
|
||||
type = string
|
||||
default = "ya-network"
|
||||
}
|
||||
|
||||
variable "subnet" {
|
||||
type = string
|
||||
default = "ya-network"
|
||||
}
|
||||
|
||||
variable "subnet_v4_cidr_blocks" {
|
||||
type = list(string)
|
||||
default = ["192.168.10.0/24"]
|
||||
}
|
||||
|
||||
variable "nat" {
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "image_family" {
|
||||
type = string
|
||||
default = "ubuntu-2404-lts"
|
||||
}
|
||||
|
||||
variable "name" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "cores" {
|
||||
type = number
|
||||
default = 2
|
||||
}
|
||||
|
||||
variable "memory" {
|
||||
type = number
|
||||
default = 4
|
||||
}
|
||||
|
||||
variable "disk_size" {
|
||||
type = number
|
||||
default = 50
|
||||
}
|
||||
|
||||
variable "disk_type" {
|
||||
type = string
|
||||
default = "network-nvme"
|
||||
}
|
||||
|
||||
variable "user_name" {
|
||||
default = ""
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "user_pass" {
|
||||
default = ""
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "admin_pass" {
|
||||
default = ""
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "timeout_create" {
|
||||
default = "10m"
|
||||
}
|
||||
|
||||
variable "timeout_delete" {
|
||||
default = "10m"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
Учиться — значит открывать то, что ты уже знал. Делать — значит демонстрировать, что ты это знаешь. Обучать — значит напоминать другим, что они знают это так же хорошо, как и ты.
|
||||
Жизнь — это то, что случается, пока ты строишь другие планы.
|
||||
Единственный способ делать великую работу — любить то, что делаешь.
|
||||
Код, написанный наспех, работает ровно столько, сколько тебе нужно для демонстрации, — и ни секунды больше.
|
||||
Лучший способ предсказать будущее — изобрести его.
|
||||
@@ -0,0 +1 @@
|
||||
pyTelegramBotAPI==4.23.0
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
import logging
|
||||
|
||||
from telebot import TeleBot
|
||||
from telebot.types import Message
|
||||
|
||||
from .config import TOKEN, ADMIN_IDS
|
||||
from .quotes import get_random_quote, add_quote, count_quotes
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(message)s",
|
||||
)
|
||||
|
||||
bot = TeleBot(TOKEN)
|
||||
|
||||
# ─── Хэндлеры ────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@bot.message_handler(commands=["start"])
|
||||
def cmd_start(message: Message):
|
||||
name = message.from_user.first_name
|
||||
bot.reply_to(
|
||||
message,
|
||||
f"Привет, {name}! 👋\n\n"
|
||||
"Я бот с цитатами. Вот что я умею:\n"
|
||||
"/quote — случайная цитата\n"
|
||||
"/stats — сколько цитат в базе\n"
|
||||
"/add <текст> — добавить цитату (только для администратора)",
|
||||
)
|
||||
logging.info(
|
||||
f"[/start] user_id={message.from_user.id} ({message.from_user.first_name})"
|
||||
)
|
||||
|
||||
|
||||
@bot.message_handler(commands=["quote"])
|
||||
def cmd_quote(message: Message):
|
||||
quote = get_random_quote()
|
||||
if quote is None:
|
||||
bot.reply_to(
|
||||
message,
|
||||
"😔 Цитат пока нет. Попросите администратора добавить хотя бы одну!",
|
||||
)
|
||||
else:
|
||||
bot.reply_to(message, f"💬 {quote}")
|
||||
logging.info(f"[/quote] user_id={message.from_user.id}")
|
||||
|
||||
|
||||
@bot.message_handler(commands=["stats"])
|
||||
def cmd_stats(message: Message):
|
||||
total = count_quotes()
|
||||
bot.reply_to(message, f"📚 В базе {total} цитат.")
|
||||
logging.info(f"[/stats] user_id={message.from_user.id}, total={total}")
|
||||
|
||||
|
||||
@bot.message_handler(commands=["add"])
|
||||
def cmd_add(message: Message):
|
||||
# Проверяем права
|
||||
if message.from_user.id not in ADMIN_IDS:
|
||||
bot.reply_to(message, "🚫 У вас нет прав для добавления цитат.")
|
||||
logging.warning(
|
||||
f"[/add] Unauthorized attempt by user_id={message.from_user.id}")
|
||||
return
|
||||
|
||||
# Извлекаем текст после команды /add
|
||||
parts = message.text.split(maxsplit=1)
|
||||
if len(parts) < 2 or not parts[1].strip():
|
||||
bot.reply_to(
|
||||
message, "✏️ Укажите текст цитаты. Пример:\n/add Жизнь прекрасна!")
|
||||
return
|
||||
|
||||
quote_text = parts[1].strip()
|
||||
added = add_quote(quote_text)
|
||||
|
||||
if added:
|
||||
bot.reply_to(message, f"✅ Цитата добавлена:\n\n💬 {quote_text}")
|
||||
logging.info(
|
||||
f"[/add] Added new quote by user_id={message.from_user.id}")
|
||||
else:
|
||||
bot.reply_to(message, "⚠️ Такая цитата уже есть в базе.")
|
||||
|
||||
|
||||
# ─── Точка входа ─────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def main():
|
||||
logging.info("Бот запущен в режиме polling...")
|
||||
bot.infinity_polling()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,8 @@
|
||||
import os
|
||||
|
||||
TOKEN = os.environ.get("TOKEN")
|
||||
if not TOKEN:
|
||||
raise ValueError("Переменная окружения TOKEN не задана!")
|
||||
|
||||
admin_ids_raw = os.environ.get("ADMIN_IDS", "")
|
||||
ADMIN_IDS = {int(i) for i in admin_ids_raw.split(",") if i.strip()}
|
||||
@@ -0,0 +1,41 @@
|
||||
import random
|
||||
from pathlib import Path
|
||||
|
||||
DATA_FILE = Path(__file__).parent.parent / "data" / "quotes.txt"
|
||||
|
||||
|
||||
def _read_all() -> list[str]:
|
||||
"""Читает все цитаты из файла. Возвращает пустой список, если файл пуст или не существует."""
|
||||
if not DATA_FILE.exists():
|
||||
return []
|
||||
lines = DATA_FILE.read_text(encoding="utf-8").splitlines()
|
||||
return [line for line in lines if line.strip()]
|
||||
|
||||
|
||||
def get_random_quote() -> str | None:
|
||||
"""Возвращает случайную цитату или None, если их нет."""
|
||||
quotes = _read_all()
|
||||
if not quotes:
|
||||
return None
|
||||
return random.choice(quotes) # nosec
|
||||
|
||||
|
||||
def add_quote(text: str) -> bool:
|
||||
"""
|
||||
Добавляет новую цитату в файл.
|
||||
Возвращает False, если такая цитата уже существует, иначе True.
|
||||
"""
|
||||
quotes = _read_all()
|
||||
if text in quotes:
|
||||
return False
|
||||
DATA_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
with DATA_FILE.open("a", encoding="utf-8") as f:
|
||||
f.write(text + "\n")
|
||||
return True
|
||||
|
||||
|
||||
def count_quotes() -> int:
|
||||
"""Возвращает количество цитат в базе."""
|
||||
return len(_read_all())
|
||||
|
||||
# Секретная строка.
|
||||
@@ -0,0 +1,67 @@
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
|
||||
# Подменяем DATA_FILE на временный файл перед импортом модуля
|
||||
class TestQuotes(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
"""Создаем временный файл для каждого теста."""
|
||||
self.tmp = tempfile.NamedTemporaryFile(mode="w",
|
||||
suffix=".txt",
|
||||
delete=False,
|
||||
encoding="utf-8")
|
||||
self.tmp.close()
|
||||
# Патчим путь к файлу в модуле quotes
|
||||
self.patcher = patch("src.quotes.DATA_FILE", Path(self.tmp.name))
|
||||
self.patcher.start()
|
||||
|
||||
def tearDown(self):
|
||||
"""Удаляем временный файл после каждого теста."""
|
||||
self.patcher.stop()
|
||||
os.unlink(self.tmp.name)
|
||||
|
||||
def test_empty_file_returns_none(self):
|
||||
from src.quotes import get_random_quote
|
||||
|
||||
self.assertIsNone(get_random_quote())
|
||||
|
||||
def test_empty_file_count_is_zero(self):
|
||||
from src.quotes import count_quotes
|
||||
|
||||
self.assertEqual(count_quotes(), 0)
|
||||
|
||||
def test_add_and_count(self):
|
||||
from src.quotes import add_quote, count_quotes
|
||||
|
||||
add_quote("Первая цитата")
|
||||
add_quote("Вторая цитата")
|
||||
self.assertEqual(count_quotes(), 2)
|
||||
|
||||
def test_add_returns_true_for_new_quote(self):
|
||||
from src.quotes import add_quote
|
||||
|
||||
result = add_quote("Новая цитата")
|
||||
self.assertTrue(result)
|
||||
|
||||
def test_add_returns_false_for_duplicate(self):
|
||||
from src.quotes import add_quote
|
||||
|
||||
add_quote("Дубликат")
|
||||
result = add_quote("Дубликат")
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_get_random_returns_string(self):
|
||||
from src.quotes import add_quote, get_random_quote
|
||||
|
||||
add_quote("Тестовая цитата")
|
||||
quote = get_random_quote()
|
||||
self.assertIsInstance(quote, str)
|
||||
self.assertEqual(quote, "Тестовая цитата")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user