Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
1cc58a7
Create README.md
reSSSno May 23, 2026
a9cd5fb
Create docker-compose.yml
reSSSno May 23, 2026
0a19ad3
Create worker.py
reSSSno May 23, 2026
89da594
Create Dockerfile
reSSSno May 23, 2026
dc0fd82
Create requirements.txt
reSSSno May 23, 2026
4f47f17
Create nginx.conf
reSSSno May 23, 2026
4476115
Create index.html
reSSSno May 23, 2026
ffc595e
Create Dockerfile
reSSSno May 23, 2026
2020f08
Create app.py
reSSSno May 23, 2026
edcdb63
Create requirements.txt
reSSSno May 23, 2026
0636448
Create ci.yml
reSSSno May 23, 2026
72da061
Create test_backend.py
reSSSno May 23, 2026
dadb8b9
Create test_worker.py
reSSSno May 23, 2026
032fb29
Create requirements-dev.txt
reSSSno May 23, 2026
dfe0e8a
Update README.md
reSSSno May 23, 2026
4925e67
Create docker-compose.yml
reSSSno May 23, 2026
6b07ab3
Create requirements-dev.txt
reSSSno May 23, 2026
3428104
Create worker.py
reSSSno May 23, 2026
5af2c2c
Create requirements.txt
reSSSno May 23, 2026
651c36f
Create Dockerfile
reSSSno May 23, 2026
0fb6a51
Create test_backend.py
reSSSno May 23, 2026
62d3770
Create test_worker.py
reSSSno May 23, 2026
a84b658
Create nginx.conf
reSSSno May 23, 2026
12b5306
Create index.html
reSSSno May 23, 2026
53caba2
Create Dockerfile
reSSSno May 23, 2026
b727848
Create app.py
reSSSno May 23, 2026
25eedc5
Create requirements.txt
reSSSno May 23, 2026
e10e86c
Create ci.yml
reSSSno May 23, 2026
04e37d0
Delete SerovAA/password-generator directory
reSSSno May 23, 2026
6c73ba9
Update README.md
reSSSno May 23, 2026
d30b8b0
Update README.md
reSSSno May 23, 2026
68fa672
Update README.md
reSSSno May 23, 2026
c09e3ae
Update README.md
reSSSno May 23, 2026
addfc4b
Update README.md
reSSSno May 23, 2026
9e808a5
Update README.md
reSSSno May 23, 2026
8e8b859
Update README.md
reSSSno May 23, 2026
952c572
Update README.md
reSSSno May 23, 2026
c10e6f3
Update README.md
reSSSno May 23, 2026
19bbb65
Update README.md
reSSSno May 23, 2026
a45350d
Update ci.yml
reSSSno May 23, 2026
2716fc0
Update ci.yml
reSSSno May 23, 2026
8e4e519
Create ci.yml
reSSSno May 23, 2026
f1d664c
Delete SerovAA/.github/workflows directory
reSSSno May 23, 2026
3248d63
Update ci.yml
reSSSno May 23, 2026
cca7fe2
Update worker.py
reSSSno May 23, 2026
0fa9b97
Update README.md
reSSSno May 23, 2026
52ee81a
Update README.md
reSSSno May 23, 2026
39658c5
Update README.md
reSSSno May 23, 2026
4f10ac5
Update README.md
reSSSno May 23, 2026
b5cf90e
Update README.md
reSSSno May 23, 2026
2f2e783
Update README.md
reSSSno May 23, 2026
c34a17c
Update README.md
reSSSno May 23, 2026
f365627
Create success.png
reSSSno May 23, 2026
9a96681
Add files via upload
reSSSno May 23, 2026
ef9b393
Add files via upload
reSSSno May 23, 2026
550ea4a
Update and rename ci.yml to ci-serovaa.yml
reSSSno May 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .github/workflows/ci-serovaa.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: CI - SerovAA

on:
pull_request:
branches: [ main, master ]

jobs:
test-backend:
runs-on: ubuntu-latest
services:
redis:
image: redis:alpine
ports: [6379:6379]
options: --health-cmd "redis-cli ping" --health-interval 10s
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install backend dependencies
working-directory: SerovAA/backend
run: pip install -r requirements.txt
- name: Install pytest
run: pip install pytest
- name: Run backend tests
working-directory: SerovAA
run: PYTHONPATH=. python -m pytest tests/test_backend.py -v
env:
REDIS_HOST: localhost

test-worker:
runs-on: ubuntu-latest
services:
redis:
image: redis:alpine
ports: [6379:6379]
options: --health-cmd "redis-cli ping" --health-interval 10s
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install worker dependencies
working-directory: SerovAA/worker
run: pip install -r requirements.txt
- name: Install pytest
run: pip install pytest
- name: Run worker tests
working-directory: SerovAA
run: PYTHONPATH=. python -m pytest tests/test_worker.py -v
env:
REDIS_HOST: localhost

docker-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Docker images
working-directory: SerovAA
run: docker compose build
70 changes: 70 additions & 0 deletions SerovAA/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Password Generator — микросервисное веб-приложение

## Описание проекта

Генератор паролей с микросервисной архитектурой.
Состоит из пяти компонентов:

- **Nginx** — обратный прокси и точка входа (порт 8080)
- **Frontend** — статический HTML/JS интерфейс
- **Backend (Flask)** — API для создания задач генерации паролей
- **Worker** — фоновый сервис, выполняющий длительную генерацию
- **Redis** — брокер задач и хранилище результатов

## Запуск

bash
- docker compose up -d

Приложение будет доступно по адресу: http://localhost:8080

## Остановить:

bash
- docker compose down

## Взаимодействие сервисов
- Пользователь вводит параметры пароля (длина, цифры, спецсимволы) и нажимает «Generate».
- Frontend отправляет POST-запрос на /api/generate в Backend.
- Backend создаёт задачу в Redis и возвращает task_id.
- Worker забирает задачу из очереди, генерирует пароль (с имитацией задержки 2 сек) и сохраняет результат.
- Frontend каждую секунду опрашивает /api/result/<task_id> и отображает пароль.

## Проверка работоспособности (curl)
Создать задачу на генерацию пароля:

bash
curl -X POST http://localhost:8080/api/generate \
-H "Content-Type: application/json" \
-d '{"length": 16, "use_digits": true, "use_special": true}'
Получить результат по task_id (подставьте полученный идентификатор):

bash
curl http://localhost:8080/api/result/<task_id>

## CI/CD Pipeline
При создании pull request в ветку main автоматически запускается GitHub Actions workflow:

- Установка зависимостей для Backend и Worker
- Запуск тестов (pytest) для обоих сервисов
- Проверка сборки Docker Compose

Статус CI: https://github.com/SoftwareEngineering2026/Practice106/actions/workflows/ci-serovaa.yml/badge.svg

## Локальный запуск тестов
* bash
* pip install pytest
* pytest tests/

## Особенности реализации
✅ Единая точка входа — Nginx (порт 8080)

✅ Все сервисы общаются через внутреннюю сеть Docker

✅ Длительные задачи вынесены в отдельный Worker (не блокируют Backend)

✅ Полностью микросервисная архитектура

✅ Запуск одной командой docker compose up

✅ Автоматическое CI-тестирование при Pull Request
6 changes: 6 additions & 0 deletions SerovAA/backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY app.py .
CMD ["python", "app.py"]
36 changes: 36 additions & 0 deletions SerovAA/backend/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from flask import Flask, request, jsonify
import redis
import uuid
import json
import os

app = Flask(__name__)
redis_client = redis.Redis(host=os.getenv('REDIS_HOST', 'localhost'), port=6379, db=0)

@app.route('/api/generate', methods=['POST'])
def generate():
data = request.json
task_id = str(uuid.uuid4())

task_data = {
'status': 'pending',
'length': data['length'],
'use_digits': data['use_digits'],
'use_special': data['use_special']
}

redis_client.set(f"task:{task_id}", json.dumps(task_data))
redis_client.lpush('password_tasks', task_id)

return jsonify({'task_id': task_id})

@app.route('/api/result/<task_id>', methods=['GET'])
def result(task_id):
task_data = redis_client.get(f"task:{task_id}")
if task_data:
task = json.loads(task_data)
return jsonify(task)
return jsonify({'status': 'not_found'}), 404

if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
2 changes: 2 additions & 0 deletions SerovAA/backend/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Flask==2.3.3
redis==5.0.1
41 changes: 41 additions & 0 deletions SerovAA/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
version: '3.8'

services:
nginx:
image: nginx:alpine
ports:
- "8080:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
- ./frontend:/usr/share/nginx/html
depends_on:
- backend
networks:
- app-network

backend:
build: ./backend
environment:
- REDIS_HOST=redis
depends_on:
- redis
networks:
- app-network

worker:
build: ./worker
environment:
- REDIS_HOST=redis
depends_on:
- redis
networks:
- app-network

redis:
image: redis:alpine
networks:
- app-network

networks:
app-network:
driver: bridge
73 changes: 73 additions & 0 deletions SerovAA/frontend/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<!DOCTYPE html>
<html>
<head>
<title>Password Generator</title>
<style>
body { font-family: Arial; max-width: 500px; margin: 50px auto; padding: 20px; }
input, button { margin: 10px 0; padding: 8px; }
.result { margin-top: 20px; padding: 10px; background: #f0f0f0; }
.status { color: gray; }
</style>
</head>
<body>
<h1>🔐 Password Generator</h1>

<label>Length: <input type="number" id="length" value="12" min="6" max="32"></label><br>
<label><input type="checkbox" id="digits" checked> Digits</label><br>
<label><input type="checkbox" id="special" checked> Special chars</label><br>

<button onclick="generatePassword()">Generate</button>
<button onclick="checkStatus()">Check Status</button>

<div class="result">
<strong>Password:</strong> <span id="password">-</span><br>
<strong>Task ID:</strong> <span id="taskId">-</span><br>
<strong>Status:</strong> <span id="status" class="status">Idle</span>
</div>

<script>
async function generatePassword() {
const length = document.getElementById('length').value;
const useDigits = document.getElementById('digits').checked;
const useSpecial = document.getElementById('special').checked;

const response = await fetch('/api/generate', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({length, use_digits: useDigits, use_special: useSpecial})
});
const data = await response.json();
document.getElementById('taskId').innerText = data.task_id;
document.getElementById('status').innerText = 'Processing...';
document.getElementById('password').innerText = '-';

// Poll for result
pollResult(data.task_id);
}

async function pollResult(taskId) {
const interval = setInterval(async () => {
const response = await fetch(`/api/result/${taskId}`);
const data = await response.json();
if (data.status === 'completed') {
document.getElementById('password').innerText = data.password;
document.getElementById('status').innerText = 'Completed!';
clearInterval(interval);
} else if (data.status === 'failed') {
document.getElementById('status').innerText = 'Failed';
clearInterval(interval);
}
}, 1000);
}

async function checkStatus() {
const taskId = document.getElementById('taskId').innerText;
if (taskId !== '-') {
const response = await fetch(`/api/result/${taskId}`);
const data = await response.json();
document.getElementById('status').innerText = data.status;
}
}
</script>
</body>
</html>
26 changes: 26 additions & 0 deletions SerovAA/nginx/nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
events {
worker_connections 1024;
}

http {
upstream backend {
server backend:5000;
}

server {
listen 80;

# Frontend
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}

# Backend API
location /api/ {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
}
2 changes: 2 additions & 0 deletions SerovAA/requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pytest==7.4.3
pytest-cov==4.1.0
Binary file added SerovAA/screenshots/errors.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added SerovAA/screenshots/success.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 26 additions & 0 deletions SerovAA/tests/test_backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import pytest
import json
from backend.app import app

@pytest.fixture
def client():
app.config['TESTING'] = True
with app.test_client() as client:
yield client

def test_generate_endpoint_returns_task_id(client):
response = client.post('/api/generate',
json={'length': 10, 'use_digits': True, 'use_special': False})
assert response.status_code == 200
data = json.loads(response.data)
assert 'task_id' in data
assert len(data['task_id']) > 0

def test_result_endpoint_for_nonexistent_task(client):
response = client.get('/api/result/nonexistent-id-123')
assert response.status_code == 404

def test_generate_with_invalid_json(client):
response = client.post('/api/generate', data='invalid', content_type='application/json')
# Flask вернёт 400 или 500 — в зависимости, но главное не 200
assert response.status_code != 200
24 changes: 24 additions & 0 deletions SerovAA/tests/test_worker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import pytest
from worker.worker import generate_password

def test_generate_password_default():
pwd = generate_password(12, True, True)
assert len(pwd) == 12
# должна содержать хотя бы одну букву и одну цифру (если digits true)
assert any(c.isalpha() for c in pwd)
assert any(c.isdigit() for c in pwd)

def test_generate_password_no_digits():
pwd = generate_password(8, use_digits=False, use_special=False)
assert len(pwd) == 8
assert not any(c.isdigit() for c in pwd)
assert not any(c in '!@#$%^&*()' for c in pwd)

def test_generate_password_only_letters():
pwd = generate_password(10, use_digits=False, use_special=False)
assert pwd.isalpha()

def test_generate_password_with_special():
pwd = generate_password(15, use_digits=False, use_special=True)
specials = set('!@#$%^&*()')
assert any(c in specials for c in pwd)
6 changes: 6 additions & 0 deletions SerovAA/worker/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY worker.py .
CMD ["python", "worker.py"]
1 change: 1 addition & 0 deletions SerovAA/worker/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
redis==5.0.1
Loading