From 1cc58a797bd7a19b412c53af59c1f8ed9eaa89e9 Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 18:28:58 +0300
Subject: [PATCH 01/56] Create README.md
---
SerovAA/README.md | 50 +++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 50 insertions(+)
create mode 100644 SerovAA/README.md
diff --git a/SerovAA/README.md b/SerovAA/README.md
new file mode 100644
index 0000000..9192853
--- /dev/null
+++ b/SerovAA/README.md
@@ -0,0 +1,50 @@
+# Password Generator - Microservices Demo
+
+## Architecture
+- **Nginx**: Reverse proxy and static frontend server
+- **Frontend**: HTML/JS UI served by Nginx
+- **Backend (Flask)**: API endpoint for generating password tasks
+- **Worker (Python)**: Long-running password generation service
+- **Redis**: Task queue and result storage
+
+## Run
+
+docker compose up -d
+
+## How it works
+- Frontend submits generation request to backend API
+- Backend creates task in Redis and returns task_id
+- Worker picks up task from Redis queue
+- Worker generates password (simulated 2s delay)
+- Frontend polls for result and displays password
+
+----
+## Проверка работоспособности
+
+### Клонируем репозиторий
+1. git clone https://github.com/SoftwareEngineering2026/Practice105.git
+2. cd Practice105/SerovAA/
+
+### Создаём структуру и файлы (скопируй вышеуказанные файлы)
+
+### Запускаем
+docker compose up -d
+
+### Проверяем
+curl http://localhost:8080/api/generate -X POST -H "Content-Type: application/json" -d '{"length": 16, "use_digits": true, "use_special": true}'
+
+### Останавливаем
+docker compose down
+
+----
+## Особенности решения:
+
+✅ Одна точка входа — Nginx на порту 8080
+
+✅ Все сервисы в одной сети Docker
+
+✅ Worker выполняет "длительные задачи" (generation с delay)
+
+✅ Готово к запуску одной командой docker compose up
+
+✅ Полностью микросервисный (frontend, backend, worker, redis)
From a9cd5fb77d462129126294cb423d3fdf3d1b423d Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 18:29:43 +0300
Subject: [PATCH 02/56] Create docker-compose.yml
---
SerovAA/password-generator/docker-compose.yml | 41 +++++++++++++++++++
1 file changed, 41 insertions(+)
create mode 100644 SerovAA/password-generator/docker-compose.yml
diff --git a/SerovAA/password-generator/docker-compose.yml b/SerovAA/password-generator/docker-compose.yml
new file mode 100644
index 0000000..960b21f
--- /dev/null
+++ b/SerovAA/password-generator/docker-compose.yml
@@ -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
From 0a19ad3f95783f9ec6273930f71095cde124a082 Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 18:30:26 +0300
Subject: [PATCH 03/56] Create worker.py
---
SerovAA/password-generator/worker/worker.py | 37 +++++++++++++++++++++
1 file changed, 37 insertions(+)
create mode 100644 SerovAA/password-generator/worker/worker.py
diff --git a/SerovAA/password-generator/worker/worker.py b/SerovAA/password-generator/worker/worker.py
new file mode 100644
index 0000000..0e057c1
--- /dev/null
+++ b/SerovAA/password-generator/worker/worker.py
@@ -0,0 +1,37 @@
+import redis
+import json
+import random
+import string
+import os
+import time
+
+redis_client = redis.Redis(host=os.getenv('REDIS_HOST', 'localhost'), port=6379, db=0)
+
+def generate_password(length, use_digits, use_special):
+ chars = string.ascii_letters
+ if use_digits:
+ chars += string.digits
+ if use_special:
+ chars += '!@#$%^&*()'
+ return ''.join(random.choice(chars) for _ in range(length))
+
+while True:
+ task_id = redis_client.brpop('password_tasks')[1].decode()
+
+ # Simulate long processing
+ time.sleep(2)
+
+ task_data = json.loads(redis_client.get(f"task:{task_id}"))
+ try:
+ password = generate_password(
+ task_data['length'],
+ task_data['use_digits'],
+ task_data['use_special']
+ )
+ task_data['status'] = 'completed'
+ task_data['password'] = password
+ except Exception as e:
+ task_data['status'] = 'failed'
+ task_data['error'] = str(e)
+
+ redis_client.set(f"task:{task_id}", json.dumps(task_data))
From 89da594861c3f44065f86d2b96afce1ff1659ad2 Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 18:30:53 +0300
Subject: [PATCH 04/56] Create Dockerfile
---
SerovAA/password-generator/worker/Dockerfile | 6 ++++++
1 file changed, 6 insertions(+)
create mode 100644 SerovAA/password-generator/worker/Dockerfile
diff --git a/SerovAA/password-generator/worker/Dockerfile b/SerovAA/password-generator/worker/Dockerfile
new file mode 100644
index 0000000..d55b117
--- /dev/null
+++ b/SerovAA/password-generator/worker/Dockerfile
@@ -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"]
From dc0fd82d6027bedc970557ab59c81d43ae2fafe6 Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 18:31:11 +0300
Subject: [PATCH 05/56] Create requirements.txt
---
SerovAA/password-generator/worker/requirements.txt | 1 +
1 file changed, 1 insertion(+)
create mode 100644 SerovAA/password-generator/worker/requirements.txt
diff --git a/SerovAA/password-generator/worker/requirements.txt b/SerovAA/password-generator/worker/requirements.txt
new file mode 100644
index 0000000..785cc92
--- /dev/null
+++ b/SerovAA/password-generator/worker/requirements.txt
@@ -0,0 +1 @@
+redis==5.0.1
From 4f47f17b4948fb3d93b2a1db422d2b7faf08c3fe Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 18:31:43 +0300
Subject: [PATCH 06/56] Create nginx.conf
---
SerovAA/password-generator/nginx/nginx.conf | 26 +++++++++++++++++++++
1 file changed, 26 insertions(+)
create mode 100644 SerovAA/password-generator/nginx/nginx.conf
diff --git a/SerovAA/password-generator/nginx/nginx.conf b/SerovAA/password-generator/nginx/nginx.conf
new file mode 100644
index 0000000..8b8e9a7
--- /dev/null
+++ b/SerovAA/password-generator/nginx/nginx.conf
@@ -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;
+ }
+ }
+}
From 4476115e447cf97a3731e741b2810c21748fb889 Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 18:32:17 +0300
Subject: [PATCH 07/56] Create index.html
---
.../password-generator/frontend/index.html | 73 +++++++++++++++++++
1 file changed, 73 insertions(+)
create mode 100644 SerovAA/password-generator/frontend/index.html
diff --git a/SerovAA/password-generator/frontend/index.html b/SerovAA/password-generator/frontend/index.html
new file mode 100644
index 0000000..c5e87cf
--- /dev/null
+++ b/SerovAA/password-generator/frontend/index.html
@@ -0,0 +1,73 @@
+
+
+
+ Password Generator
+
+
+
+ 🔐 Password Generator
+
+
+
+
+
+
+
+
+
+ Password: -
+ Task ID: -
+ Status: Idle
+
+
+
+
+
From ffc595e42eeef1c3a9d87ae1eccc6a313b7e9db8 Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 18:33:08 +0300
Subject: [PATCH 08/56] Create Dockerfile
---
SerovAA/password-generator/backend/Dockerfile | 6 ++++++
1 file changed, 6 insertions(+)
create mode 100644 SerovAA/password-generator/backend/Dockerfile
diff --git a/SerovAA/password-generator/backend/Dockerfile b/SerovAA/password-generator/backend/Dockerfile
new file mode 100644
index 0000000..6113261
--- /dev/null
+++ b/SerovAA/password-generator/backend/Dockerfile
@@ -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"]
From 2020f087aa30ee995c3f79942f958e34f14cc3a3 Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 18:33:28 +0300
Subject: [PATCH 09/56] Create app.py
---
SerovAA/password-generator/backend/app.py | 36 +++++++++++++++++++++++
1 file changed, 36 insertions(+)
create mode 100644 SerovAA/password-generator/backend/app.py
diff --git a/SerovAA/password-generator/backend/app.py b/SerovAA/password-generator/backend/app.py
new file mode 100644
index 0000000..9b1c62c
--- /dev/null
+++ b/SerovAA/password-generator/backend/app.py
@@ -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/', 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)
From edcdb6381019c08b8271c61b10697232dfd7da82 Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 18:33:48 +0300
Subject: [PATCH 10/56] Create requirements.txt
---
SerovAA/password-generator/backend/requirements.txt | 2 ++
1 file changed, 2 insertions(+)
create mode 100644 SerovAA/password-generator/backend/requirements.txt
diff --git a/SerovAA/password-generator/backend/requirements.txt b/SerovAA/password-generator/backend/requirements.txt
new file mode 100644
index 0000000..bf804e5
--- /dev/null
+++ b/SerovAA/password-generator/backend/requirements.txt
@@ -0,0 +1,2 @@
+Flask==2.3.3
+redis==5.0.1
From 06364488049c991a12dc7ded9e09f4a22d30ef2c Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 18:35:22 +0300
Subject: [PATCH 11/56] Create ci.yml
---
.../.github/workflows/ci.yml | 64 +++++++++++++++++++
1 file changed, 64 insertions(+)
create mode 100644 SerovAA/password-generator/.github/workflows/ci.yml
diff --git a/SerovAA/password-generator/.github/workflows/ci.yml b/SerovAA/password-generator/.github/workflows/ci.yml
new file mode 100644
index 0000000..c45ed54
--- /dev/null
+++ b/SerovAA/password-generator/.github/workflows/ci.yml
@@ -0,0 +1,64 @@
+name: CI Pipeline
+
+on:
+ pull_request:
+ branches: [ main, master ]
+
+jobs:
+ test-backend:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+
+ - name: Install backend dependencies
+ working-directory: СеровАА/backend
+ run: |
+ python -m pip install --upgrade pip
+ pip install -r requirements.txt
+
+ - name: Install dev dependencies
+ run: pip install pytest pytest-cov
+
+ - name: Run backend tests
+ working-directory: СеровАА
+ run: |
+ python -m pytest tests/test_backend.py -v --tb=short
+
+ test-worker:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+
+ - name: Install worker dependencies
+ working-directory: СеровАА/worker
+ run: |
+ python -m pip install --upgrade pip
+ pip install -r requirements.txt
+
+ - name: Install dev dependencies
+ run: pip install pytest pytest-cov
+
+ - name: Run worker tests
+ working-directory: СеровАА
+ run: |
+ python -m pytest tests/test_worker.py -v --tb=short
+
+ # Необязательная проверка: собрать Docker Compose (без запуска)
+ docker-build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Build Docker images
+ working-directory: СеровАА
+ run: docker compose build
From 72da0618507484dd4896accfceebfb9d4e9d08ba Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 18:36:16 +0300
Subject: [PATCH 12/56] Create test_backend.py
---
.../password-generator/tests/test_backend.py | 26 +++++++++++++++++++
1 file changed, 26 insertions(+)
create mode 100644 SerovAA/password-generator/tests/test_backend.py
diff --git a/SerovAA/password-generator/tests/test_backend.py b/SerovAA/password-generator/tests/test_backend.py
new file mode 100644
index 0000000..49e95eb
--- /dev/null
+++ b/SerovAA/password-generator/tests/test_backend.py
@@ -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
From dadb8b9ac8e0fb0f43d5195b67b207272427b2a7 Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 18:36:52 +0300
Subject: [PATCH 13/56] Create test_worker.py
---
.../password-generator/tests/test_worker.py | 24 +++++++++++++++++++
1 file changed, 24 insertions(+)
create mode 100644 SerovAA/password-generator/tests/test_worker.py
diff --git a/SerovAA/password-generator/tests/test_worker.py b/SerovAA/password-generator/tests/test_worker.py
new file mode 100644
index 0000000..3d137e3
--- /dev/null
+++ b/SerovAA/password-generator/tests/test_worker.py
@@ -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)
From 032fb294b1a256e8d49f3f7a3ea57bbc69c7ca07 Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 18:37:35 +0300
Subject: [PATCH 14/56] Create requirements-dev.txt
---
SerovAA/password-generator/requirements-dev.txt | 2 ++
1 file changed, 2 insertions(+)
create mode 100644 SerovAA/password-generator/requirements-dev.txt
diff --git a/SerovAA/password-generator/requirements-dev.txt b/SerovAA/password-generator/requirements-dev.txt
new file mode 100644
index 0000000..acdca76
--- /dev/null
+++ b/SerovAA/password-generator/requirements-dev.txt
@@ -0,0 +1,2 @@
+pytest==7.4.3
+pytest-cov==4.1.0
From dfe0e8ae26439389b3f44a9c05ebf24d1134ae3d Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 18:38:09 +0300
Subject: [PATCH 15/56] Update README.md
---
SerovAA/README.md | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/SerovAA/README.md b/SerovAA/README.md
index 9192853..de8a03f 100644
--- a/SerovAA/README.md
+++ b/SerovAA/README.md
@@ -36,6 +36,20 @@ curl http://localhost:8080/api/generate -X POST -H "Content-Type: application/js
### Останавливаем
docker compose down
+## CI/CD Pipeline
+
+Проект использует **GitHub Actions** для автоматической проверки при pull request в `main` ветку.
+
+### Что проверяется:
+- ✅ Корректность API (backend тесты)
+- ✅ Генерация паролей (worker тесты)
+- ✅ Сборка Docker образов (docker compose build)
+
+### Как запустить тесты локально:
+```bash
+pip install pytest
+pytest tests/
+
----
## Особенности решения:
From 4925e67654590a272b9bd472ba490f90c5321591 Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 18:51:54 +0300
Subject: [PATCH 16/56] Create docker-compose.yml
---
SerovAA/docker-compose.yml | 41 ++++++++++++++++++++++++++++++++++++++
1 file changed, 41 insertions(+)
create mode 100644 SerovAA/docker-compose.yml
diff --git a/SerovAA/docker-compose.yml b/SerovAA/docker-compose.yml
new file mode 100644
index 0000000..960b21f
--- /dev/null
+++ b/SerovAA/docker-compose.yml
@@ -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
From 6b07ab3d86ce2a94630c2ea5dafe7cc0c8b9b815 Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 18:52:12 +0300
Subject: [PATCH 17/56] Create requirements-dev.txt
---
SerovAA/requirements-dev.txt | 2 ++
1 file changed, 2 insertions(+)
create mode 100644 SerovAA/requirements-dev.txt
diff --git a/SerovAA/requirements-dev.txt b/SerovAA/requirements-dev.txt
new file mode 100644
index 0000000..acdca76
--- /dev/null
+++ b/SerovAA/requirements-dev.txt
@@ -0,0 +1,2 @@
+pytest==7.4.3
+pytest-cov==4.1.0
From 3428104bca3c4066a707a18781eb539413da4984 Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 18:52:53 +0300
Subject: [PATCH 18/56] Create worker.py
---
SerovAA/worker/worker.py | 37 +++++++++++++++++++++++++++++++++++++
1 file changed, 37 insertions(+)
create mode 100644 SerovAA/worker/worker.py
diff --git a/SerovAA/worker/worker.py b/SerovAA/worker/worker.py
new file mode 100644
index 0000000..0e057c1
--- /dev/null
+++ b/SerovAA/worker/worker.py
@@ -0,0 +1,37 @@
+import redis
+import json
+import random
+import string
+import os
+import time
+
+redis_client = redis.Redis(host=os.getenv('REDIS_HOST', 'localhost'), port=6379, db=0)
+
+def generate_password(length, use_digits, use_special):
+ chars = string.ascii_letters
+ if use_digits:
+ chars += string.digits
+ if use_special:
+ chars += '!@#$%^&*()'
+ return ''.join(random.choice(chars) for _ in range(length))
+
+while True:
+ task_id = redis_client.brpop('password_tasks')[1].decode()
+
+ # Simulate long processing
+ time.sleep(2)
+
+ task_data = json.loads(redis_client.get(f"task:{task_id}"))
+ try:
+ password = generate_password(
+ task_data['length'],
+ task_data['use_digits'],
+ task_data['use_special']
+ )
+ task_data['status'] = 'completed'
+ task_data['password'] = password
+ except Exception as e:
+ task_data['status'] = 'failed'
+ task_data['error'] = str(e)
+
+ redis_client.set(f"task:{task_id}", json.dumps(task_data))
From 5af2c2c849a2bed9ace5a3b1c998cebe3f1678c0 Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 18:53:11 +0300
Subject: [PATCH 19/56] Create requirements.txt
---
SerovAA/worker/requirements.txt | 1 +
1 file changed, 1 insertion(+)
create mode 100644 SerovAA/worker/requirements.txt
diff --git a/SerovAA/worker/requirements.txt b/SerovAA/worker/requirements.txt
new file mode 100644
index 0000000..785cc92
--- /dev/null
+++ b/SerovAA/worker/requirements.txt
@@ -0,0 +1 @@
+redis==5.0.1
From 651c36fac1bb754a3318680ccdd619d431c4941a Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 18:53:23 +0300
Subject: [PATCH 20/56] Create Dockerfile
---
SerovAA/worker/Dockerfile | 6 ++++++
1 file changed, 6 insertions(+)
create mode 100644 SerovAA/worker/Dockerfile
diff --git a/SerovAA/worker/Dockerfile b/SerovAA/worker/Dockerfile
new file mode 100644
index 0000000..d55b117
--- /dev/null
+++ b/SerovAA/worker/Dockerfile
@@ -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"]
From 0fb6a518e1334b2b0c3498e406e85057528571de Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 18:54:23 +0300
Subject: [PATCH 21/56] Create test_backend.py
---
SerovAA/tests/test_backend.py | 26 ++++++++++++++++++++++++++
1 file changed, 26 insertions(+)
create mode 100644 SerovAA/tests/test_backend.py
diff --git a/SerovAA/tests/test_backend.py b/SerovAA/tests/test_backend.py
new file mode 100644
index 0000000..49e95eb
--- /dev/null
+++ b/SerovAA/tests/test_backend.py
@@ -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
From 62d37705a0dacbdbbf2915f486e9a1372299606f Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 18:54:40 +0300
Subject: [PATCH 22/56] Create test_worker.py
---
SerovAA/tests/test_worker.py | 24 ++++++++++++++++++++++++
1 file changed, 24 insertions(+)
create mode 100644 SerovAA/tests/test_worker.py
diff --git a/SerovAA/tests/test_worker.py b/SerovAA/tests/test_worker.py
new file mode 100644
index 0000000..3d137e3
--- /dev/null
+++ b/SerovAA/tests/test_worker.py
@@ -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)
From a84b658df88e77781fe71cd4c450783c152ff3db Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 18:55:03 +0300
Subject: [PATCH 23/56] Create nginx.conf
---
SerovAA/nginx/nginx.conf | 26 ++++++++++++++++++++++++++
1 file changed, 26 insertions(+)
create mode 100644 SerovAA/nginx/nginx.conf
diff --git a/SerovAA/nginx/nginx.conf b/SerovAA/nginx/nginx.conf
new file mode 100644
index 0000000..8b8e9a7
--- /dev/null
+++ b/SerovAA/nginx/nginx.conf
@@ -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;
+ }
+ }
+}
From 12b5306ee387ccf0b893c923c36a69b723222912 Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 18:55:28 +0300
Subject: [PATCH 24/56] Create index.html
---
SerovAA/frontend/index.html | 73 +++++++++++++++++++++++++++++++++++++
1 file changed, 73 insertions(+)
create mode 100644 SerovAA/frontend/index.html
diff --git a/SerovAA/frontend/index.html b/SerovAA/frontend/index.html
new file mode 100644
index 0000000..c5e87cf
--- /dev/null
+++ b/SerovAA/frontend/index.html
@@ -0,0 +1,73 @@
+
+
+
+ Password Generator
+
+
+
+ 🔐 Password Generator
+
+
+
+
+
+
+
+
+
+ Password: -
+ Task ID: -
+ Status: Idle
+
+
+
+
+
From 53caba2a9b26c2c5d9c85ca21d73626cab8233a1 Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 18:55:59 +0300
Subject: [PATCH 25/56] Create Dockerfile
---
SerovAA/backend/Dockerfile | 6 ++++++
1 file changed, 6 insertions(+)
create mode 100644 SerovAA/backend/Dockerfile
diff --git a/SerovAA/backend/Dockerfile b/SerovAA/backend/Dockerfile
new file mode 100644
index 0000000..6113261
--- /dev/null
+++ b/SerovAA/backend/Dockerfile
@@ -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"]
From b72784843510f3cc0c8dbf38fcaae2cc3595159d Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 18:56:15 +0300
Subject: [PATCH 26/56] Create app.py
---
SerovAA/backend/app.py | 36 ++++++++++++++++++++++++++++++++++++
1 file changed, 36 insertions(+)
create mode 100644 SerovAA/backend/app.py
diff --git a/SerovAA/backend/app.py b/SerovAA/backend/app.py
new file mode 100644
index 0000000..9b1c62c
--- /dev/null
+++ b/SerovAA/backend/app.py
@@ -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/', 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)
From 25eedc59011a3fc74aa1eeca8846cc3bf3fe4838 Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 18:56:30 +0300
Subject: [PATCH 27/56] Create requirements.txt
---
SerovAA/backend/requirements.txt | 2 ++
1 file changed, 2 insertions(+)
create mode 100644 SerovAA/backend/requirements.txt
diff --git a/SerovAA/backend/requirements.txt b/SerovAA/backend/requirements.txt
new file mode 100644
index 0000000..bf804e5
--- /dev/null
+++ b/SerovAA/backend/requirements.txt
@@ -0,0 +1,2 @@
+Flask==2.3.3
+redis==5.0.1
From e10e86c0db06c1c77ff543002d2eadb0d474ef2c Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 18:57:48 +0300
Subject: [PATCH 28/56] Create ci.yml
---
SerovAA/.github/workflows/ci.yml | 64 ++++++++++++++++++++++++++++++++
1 file changed, 64 insertions(+)
create mode 100644 SerovAA/.github/workflows/ci.yml
diff --git a/SerovAA/.github/workflows/ci.yml b/SerovAA/.github/workflows/ci.yml
new file mode 100644
index 0000000..c45ed54
--- /dev/null
+++ b/SerovAA/.github/workflows/ci.yml
@@ -0,0 +1,64 @@
+name: CI Pipeline
+
+on:
+ pull_request:
+ branches: [ main, master ]
+
+jobs:
+ test-backend:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+
+ - name: Install backend dependencies
+ working-directory: СеровАА/backend
+ run: |
+ python -m pip install --upgrade pip
+ pip install -r requirements.txt
+
+ - name: Install dev dependencies
+ run: pip install pytest pytest-cov
+
+ - name: Run backend tests
+ working-directory: СеровАА
+ run: |
+ python -m pytest tests/test_backend.py -v --tb=short
+
+ test-worker:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+
+ - name: Install worker dependencies
+ working-directory: СеровАА/worker
+ run: |
+ python -m pip install --upgrade pip
+ pip install -r requirements.txt
+
+ - name: Install dev dependencies
+ run: pip install pytest pytest-cov
+
+ - name: Run worker tests
+ working-directory: СеровАА
+ run: |
+ python -m pytest tests/test_worker.py -v --tb=short
+
+ # Необязательная проверка: собрать Docker Compose (без запуска)
+ docker-build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Build Docker images
+ working-directory: СеровАА
+ run: docker compose build
From 04e37d0fe3258a29805fa391215266fefee0bd2a Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 18:57:59 +0300
Subject: [PATCH 29/56] Delete SerovAA/password-generator directory
---
.../.github/workflows/ci.yml | 64 ----------------
SerovAA/password-generator/backend/Dockerfile | 6 --
SerovAA/password-generator/backend/app.py | 36 ---------
.../backend/requirements.txt | 2 -
SerovAA/password-generator/docker-compose.yml | 41 -----------
.../password-generator/frontend/index.html | 73 -------------------
SerovAA/password-generator/nginx/nginx.conf | 26 -------
.../password-generator/requirements-dev.txt | 2 -
.../password-generator/tests/test_backend.py | 26 -------
.../password-generator/tests/test_worker.py | 24 ------
SerovAA/password-generator/worker/Dockerfile | 6 --
.../worker/requirements.txt | 1 -
SerovAA/password-generator/worker/worker.py | 37 ----------
13 files changed, 344 deletions(-)
delete mode 100644 SerovAA/password-generator/.github/workflows/ci.yml
delete mode 100644 SerovAA/password-generator/backend/Dockerfile
delete mode 100644 SerovAA/password-generator/backend/app.py
delete mode 100644 SerovAA/password-generator/backend/requirements.txt
delete mode 100644 SerovAA/password-generator/docker-compose.yml
delete mode 100644 SerovAA/password-generator/frontend/index.html
delete mode 100644 SerovAA/password-generator/nginx/nginx.conf
delete mode 100644 SerovAA/password-generator/requirements-dev.txt
delete mode 100644 SerovAA/password-generator/tests/test_backend.py
delete mode 100644 SerovAA/password-generator/tests/test_worker.py
delete mode 100644 SerovAA/password-generator/worker/Dockerfile
delete mode 100644 SerovAA/password-generator/worker/requirements.txt
delete mode 100644 SerovAA/password-generator/worker/worker.py
diff --git a/SerovAA/password-generator/.github/workflows/ci.yml b/SerovAA/password-generator/.github/workflows/ci.yml
deleted file mode 100644
index c45ed54..0000000
--- a/SerovAA/password-generator/.github/workflows/ci.yml
+++ /dev/null
@@ -1,64 +0,0 @@
-name: CI Pipeline
-
-on:
- pull_request:
- branches: [ main, master ]
-
-jobs:
- test-backend:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
-
- - name: Set up Python
- uses: actions/setup-python@v5
- with:
- python-version: '3.11'
-
- - name: Install backend dependencies
- working-directory: СеровАА/backend
- run: |
- python -m pip install --upgrade pip
- pip install -r requirements.txt
-
- - name: Install dev dependencies
- run: pip install pytest pytest-cov
-
- - name: Run backend tests
- working-directory: СеровАА
- run: |
- python -m pytest tests/test_backend.py -v --tb=short
-
- test-worker:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
-
- - name: Set up Python
- uses: actions/setup-python@v5
- with:
- python-version: '3.11'
-
- - name: Install worker dependencies
- working-directory: СеровАА/worker
- run: |
- python -m pip install --upgrade pip
- pip install -r requirements.txt
-
- - name: Install dev dependencies
- run: pip install pytest pytest-cov
-
- - name: Run worker tests
- working-directory: СеровАА
- run: |
- python -m pytest tests/test_worker.py -v --tb=short
-
- # Необязательная проверка: собрать Docker Compose (без запуска)
- docker-build:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
-
- - name: Build Docker images
- working-directory: СеровАА
- run: docker compose build
diff --git a/SerovAA/password-generator/backend/Dockerfile b/SerovAA/password-generator/backend/Dockerfile
deleted file mode 100644
index 6113261..0000000
--- a/SerovAA/password-generator/backend/Dockerfile
+++ /dev/null
@@ -1,6 +0,0 @@
-FROM python:3.11-slim
-WORKDIR /app
-COPY requirements.txt .
-RUN pip install -r requirements.txt
-COPY app.py .
-CMD ["python", "app.py"]
diff --git a/SerovAA/password-generator/backend/app.py b/SerovAA/password-generator/backend/app.py
deleted file mode 100644
index 9b1c62c..0000000
--- a/SerovAA/password-generator/backend/app.py
+++ /dev/null
@@ -1,36 +0,0 @@
-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/', 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)
diff --git a/SerovAA/password-generator/backend/requirements.txt b/SerovAA/password-generator/backend/requirements.txt
deleted file mode 100644
index bf804e5..0000000
--- a/SerovAA/password-generator/backend/requirements.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-Flask==2.3.3
-redis==5.0.1
diff --git a/SerovAA/password-generator/docker-compose.yml b/SerovAA/password-generator/docker-compose.yml
deleted file mode 100644
index 960b21f..0000000
--- a/SerovAA/password-generator/docker-compose.yml
+++ /dev/null
@@ -1,41 +0,0 @@
-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
diff --git a/SerovAA/password-generator/frontend/index.html b/SerovAA/password-generator/frontend/index.html
deleted file mode 100644
index c5e87cf..0000000
--- a/SerovAA/password-generator/frontend/index.html
+++ /dev/null
@@ -1,73 +0,0 @@
-
-
-
- Password Generator
-
-
-
- 🔐 Password Generator
-
-
-
-
-
-
-
-
-
- Password: -
- Task ID: -
- Status: Idle
-
-
-
-
-
diff --git a/SerovAA/password-generator/nginx/nginx.conf b/SerovAA/password-generator/nginx/nginx.conf
deleted file mode 100644
index 8b8e9a7..0000000
--- a/SerovAA/password-generator/nginx/nginx.conf
+++ /dev/null
@@ -1,26 +0,0 @@
-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;
- }
- }
-}
diff --git a/SerovAA/password-generator/requirements-dev.txt b/SerovAA/password-generator/requirements-dev.txt
deleted file mode 100644
index acdca76..0000000
--- a/SerovAA/password-generator/requirements-dev.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-pytest==7.4.3
-pytest-cov==4.1.0
diff --git a/SerovAA/password-generator/tests/test_backend.py b/SerovAA/password-generator/tests/test_backend.py
deleted file mode 100644
index 49e95eb..0000000
--- a/SerovAA/password-generator/tests/test_backend.py
+++ /dev/null
@@ -1,26 +0,0 @@
-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
diff --git a/SerovAA/password-generator/tests/test_worker.py b/SerovAA/password-generator/tests/test_worker.py
deleted file mode 100644
index 3d137e3..0000000
--- a/SerovAA/password-generator/tests/test_worker.py
+++ /dev/null
@@ -1,24 +0,0 @@
-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)
diff --git a/SerovAA/password-generator/worker/Dockerfile b/SerovAA/password-generator/worker/Dockerfile
deleted file mode 100644
index d55b117..0000000
--- a/SerovAA/password-generator/worker/Dockerfile
+++ /dev/null
@@ -1,6 +0,0 @@
-FROM python:3.11-slim
-WORKDIR /app
-COPY requirements.txt .
-RUN pip install -r requirements.txt
-COPY worker.py .
-CMD ["python", "worker.py"]
diff --git a/SerovAA/password-generator/worker/requirements.txt b/SerovAA/password-generator/worker/requirements.txt
deleted file mode 100644
index 785cc92..0000000
--- a/SerovAA/password-generator/worker/requirements.txt
+++ /dev/null
@@ -1 +0,0 @@
-redis==5.0.1
diff --git a/SerovAA/password-generator/worker/worker.py b/SerovAA/password-generator/worker/worker.py
deleted file mode 100644
index 0e057c1..0000000
--- a/SerovAA/password-generator/worker/worker.py
+++ /dev/null
@@ -1,37 +0,0 @@
-import redis
-import json
-import random
-import string
-import os
-import time
-
-redis_client = redis.Redis(host=os.getenv('REDIS_HOST', 'localhost'), port=6379, db=0)
-
-def generate_password(length, use_digits, use_special):
- chars = string.ascii_letters
- if use_digits:
- chars += string.digits
- if use_special:
- chars += '!@#$%^&*()'
- return ''.join(random.choice(chars) for _ in range(length))
-
-while True:
- task_id = redis_client.brpop('password_tasks')[1].decode()
-
- # Simulate long processing
- time.sleep(2)
-
- task_data = json.loads(redis_client.get(f"task:{task_id}"))
- try:
- password = generate_password(
- task_data['length'],
- task_data['use_digits'],
- task_data['use_special']
- )
- task_data['status'] = 'completed'
- task_data['password'] = password
- except Exception as e:
- task_data['status'] = 'failed'
- task_data['error'] = str(e)
-
- redis_client.set(f"task:{task_id}", json.dumps(task_data))
From 6c73ba922e36d36796197aaf32e2d193bd16f150 Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 18:58:17 +0300
Subject: [PATCH 30/56] Update README.md
---
SerovAA/README.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/SerovAA/README.md b/SerovAA/README.md
index de8a03f..4ac54d0 100644
--- a/SerovAA/README.md
+++ b/SerovAA/README.md
@@ -51,6 +51,7 @@ pip install pytest
pytest tests/
----
+
## Особенности решения:
✅ Одна точка входа — Nginx на порту 8080
From d30b8b079508b492d138b1747cde733dabe263ea Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 18:58:32 +0300
Subject: [PATCH 31/56] Update README.md
---
SerovAA/README.md | 2 --
1 file changed, 2 deletions(-)
diff --git a/SerovAA/README.md b/SerovAA/README.md
index 4ac54d0..cacc8cb 100644
--- a/SerovAA/README.md
+++ b/SerovAA/README.md
@@ -50,8 +50,6 @@ docker compose down
pip install pytest
pytest tests/
-----
-
## Особенности решения:
✅ Одна точка входа — Nginx на порту 8080
From 68fa672cdd2d8720cc4907e9a45f21274b3e92e8 Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 18:59:00 +0300
Subject: [PATCH 32/56] Update README.md
From c09e3aeffd2da8bd9adb83f9b7fc597e6ca66120 Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 18:59:11 +0300
Subject: [PATCH 33/56] Update README.md
---
SerovAA/README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/SerovAA/README.md b/SerovAA/README.md
index cacc8cb..548120f 100644
--- a/SerovAA/README.md
+++ b/SerovAA/README.md
@@ -48,7 +48,7 @@ docker compose down
### Как запустить тесты локально:
```bash
pip install pytest
-pytest tests/
+pytest tests/'''
## Особенности решения:
From addfc4bc5a7608a4a27a8d36db865f5f07483991 Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 18:59:24 +0300
Subject: [PATCH 34/56] Update README.md
---
SerovAA/README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/SerovAA/README.md b/SerovAA/README.md
index 548120f..6a761dc 100644
--- a/SerovAA/README.md
+++ b/SerovAA/README.md
@@ -48,7 +48,7 @@ docker compose down
### Как запустить тесты локально:
```bash
pip install pytest
-pytest tests/'''
+pytest tests/```
## Особенности решения:
From 9e808a576212ea22aaf4ae24ac54d9c44eeab578 Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 19:00:00 +0300
Subject: [PATCH 35/56] Update README.md
---
SerovAA/README.md | 15 ++++++---------
1 file changed, 6 insertions(+), 9 deletions(-)
diff --git a/SerovAA/README.md b/SerovAA/README.md
index 6a761dc..8671778 100644
--- a/SerovAA/README.md
+++ b/SerovAA/README.md
@@ -50,14 +50,11 @@ docker compose down
pip install pytest
pytest tests/```
+----
## Особенности решения:
-✅ Одна точка входа — Nginx на порту 8080
-
-✅ Все сервисы в одной сети Docker
-
-✅ Worker выполняет "длительные задачи" (generation с delay)
-
-✅ Готово к запуску одной командой docker compose up
-
-✅ Полностью микросервисный (frontend, backend, worker, redis)
+- ✅ Одна точка входа — Nginx на порту 8080
+- ✅ Все сервисы в одной сети Docker
+- ✅ Worker выполняет "длительные задачи" (generation с delay)
+- ✅ Готово к запуску одной командой docker compose up
+- ✅ Полностью микросервисный (frontend, backend, worker, redis)
From 8e8b8597a527d04bdf9e5e7bd13be07db4b62b6e Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 19:00:12 +0300
Subject: [PATCH 36/56] Update README.md
---
SerovAA/README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/SerovAA/README.md b/SerovAA/README.md
index 8671778..b97580a 100644
--- a/SerovAA/README.md
+++ b/SerovAA/README.md
@@ -46,9 +46,9 @@ docker compose down
- ✅ Сборка Docker образов (docker compose build)
### Как запустить тесты локально:
-```bash
+bash
pip install pytest
-pytest tests/```
+pytest tests/
----
## Особенности решения:
From 952c57240d80a5d6ee75895d49523e49cb9504c4 Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 19:00:24 +0300
Subject: [PATCH 37/56] Update README.md
---
SerovAA/README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/SerovAA/README.md b/SerovAA/README.md
index b97580a..c5d7f94 100644
--- a/SerovAA/README.md
+++ b/SerovAA/README.md
@@ -46,9 +46,9 @@ docker compose down
- ✅ Сборка Docker образов (docker compose build)
### Как запустить тесты локально:
-bash
+''bash
pip install pytest
-pytest tests/
+pytest tests/''
----
## Особенности решения:
From c10e6f3d3fc4655a12ece8fa899347f89b1b6f9c Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 19:00:39 +0300
Subject: [PATCH 38/56] Update README.md
---
SerovAA/README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/SerovAA/README.md b/SerovAA/README.md
index c5d7f94..8c1663d 100644
--- a/SerovAA/README.md
+++ b/SerovAA/README.md
@@ -46,9 +46,9 @@ docker compose down
- ✅ Сборка Docker образов (docker compose build)
### Как запустить тесты локально:
-''bash
+'''bash
pip install pytest
-pytest tests/''
+pytest tests/'''
----
## Особенности решения:
From 19bbb652e8f5f670299decb5e7c2f8a2e9e28390 Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 19:01:14 +0300
Subject: [PATCH 39/56] Update README.md
---
SerovAA/README.md | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/SerovAA/README.md b/SerovAA/README.md
index 8c1663d..8d5dd3f 100644
--- a/SerovAA/README.md
+++ b/SerovAA/README.md
@@ -46,9 +46,9 @@ docker compose down
- ✅ Сборка Docker образов (docker compose build)
### Как запустить тесты локально:
-'''bash
-pip install pytest
-pytest tests/'''
+- bash
+- pip install pytest
+- pytest tests/
----
## Особенности решения:
From a45350df714d7d2abbd1a26aeb4cd39973bb3795 Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 19:07:29 +0300
Subject: [PATCH 40/56] Update ci.yml
---
SerovAA/.github/workflows/ci.yml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/SerovAA/.github/workflows/ci.yml b/SerovAA/.github/workflows/ci.yml
index c45ed54..f454988 100644
--- a/SerovAA/.github/workflows/ci.yml
+++ b/SerovAA/.github/workflows/ci.yml
@@ -1,6 +1,8 @@
name: CI Pipeline
on:
+ push:
+ branches: [ main, master ]
pull_request:
branches: [ main, master ]
From 2716fc0224416a691356dd0d3e7311e109ceb173 Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 19:08:37 +0300
Subject: [PATCH 41/56] Update ci.yml
---
SerovAA/.github/workflows/ci.yml | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/SerovAA/.github/workflows/ci.yml b/SerovAA/.github/workflows/ci.yml
index f454988..a9aaa55 100644
--- a/SerovAA/.github/workflows/ci.yml
+++ b/SerovAA/.github/workflows/ci.yml
@@ -18,7 +18,7 @@ jobs:
python-version: '3.11'
- name: Install backend dependencies
- working-directory: СеровАА/backend
+ working-directory: SerovAA/backend
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
@@ -27,7 +27,7 @@ jobs:
run: pip install pytest pytest-cov
- name: Run backend tests
- working-directory: СеровАА
+ working-directory: SerovAA
run: |
python -m pytest tests/test_backend.py -v --tb=short
@@ -42,7 +42,7 @@ jobs:
python-version: '3.11'
- name: Install worker dependencies
- working-directory: СеровАА/worker
+ working-directory: SerovAA/worker
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
@@ -51,7 +51,7 @@ jobs:
run: pip install pytest pytest-cov
- name: Run worker tests
- working-directory: СеровАА
+ working-directory: SerovAA
run: |
python -m pytest tests/test_worker.py -v --tb=short
@@ -62,5 +62,5 @@ jobs:
- uses: actions/checkout@v4
- name: Build Docker images
- working-directory: СеровАА
+ working-directory: SerovAA
run: docker compose build
From 8e4e519cd7e5041303a21160390dd1399295d9a1 Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 19:22:40 +0300
Subject: [PATCH 42/56] Create ci.yml
---
.github/workflows/ci.yml | 66 ++++++++++++++++++++++++++++++++++++++++
1 file changed, 66 insertions(+)
create mode 100644 .github/workflows/ci.yml
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..a9aaa55
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,66 @@
+name: CI Pipeline
+
+on:
+ push:
+ branches: [ main, master ]
+ pull_request:
+ branches: [ main, master ]
+
+jobs:
+ test-backend:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+
+ - name: Install backend dependencies
+ working-directory: SerovAA/backend
+ run: |
+ python -m pip install --upgrade pip
+ pip install -r requirements.txt
+
+ - name: Install dev dependencies
+ run: pip install pytest pytest-cov
+
+ - name: Run backend tests
+ working-directory: SerovAA
+ run: |
+ python -m pytest tests/test_backend.py -v --tb=short
+
+ test-worker:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+
+ - name: Install worker dependencies
+ working-directory: SerovAA/worker
+ run: |
+ python -m pip install --upgrade pip
+ pip install -r requirements.txt
+
+ - name: Install dev dependencies
+ run: pip install pytest pytest-cov
+
+ - name: Run worker tests
+ working-directory: SerovAA
+ run: |
+ python -m pytest tests/test_worker.py -v --tb=short
+
+ # Необязательная проверка: собрать Docker Compose (без запуска)
+ docker-build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Build Docker images
+ working-directory: SerovAA
+ run: docker compose build
From f1d664c1f92bd331b412b4f601c4b619efd9a760 Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 19:32:04 +0300
Subject: [PATCH 43/56] Delete SerovAA/.github/workflows directory
---
SerovAA/.github/workflows/ci.yml | 66 --------------------------------
1 file changed, 66 deletions(-)
delete mode 100644 SerovAA/.github/workflows/ci.yml
diff --git a/SerovAA/.github/workflows/ci.yml b/SerovAA/.github/workflows/ci.yml
deleted file mode 100644
index a9aaa55..0000000
--- a/SerovAA/.github/workflows/ci.yml
+++ /dev/null
@@ -1,66 +0,0 @@
-name: CI Pipeline
-
-on:
- push:
- branches: [ main, master ]
- pull_request:
- branches: [ main, master ]
-
-jobs:
- test-backend:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
-
- - name: Set up Python
- uses: actions/setup-python@v5
- with:
- python-version: '3.11'
-
- - name: Install backend dependencies
- working-directory: SerovAA/backend
- run: |
- python -m pip install --upgrade pip
- pip install -r requirements.txt
-
- - name: Install dev dependencies
- run: pip install pytest pytest-cov
-
- - name: Run backend tests
- working-directory: SerovAA
- run: |
- python -m pytest tests/test_backend.py -v --tb=short
-
- test-worker:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
-
- - name: Set up Python
- uses: actions/setup-python@v5
- with:
- python-version: '3.11'
-
- - name: Install worker dependencies
- working-directory: SerovAA/worker
- run: |
- python -m pip install --upgrade pip
- pip install -r requirements.txt
-
- - name: Install dev dependencies
- run: pip install pytest pytest-cov
-
- - name: Run worker tests
- working-directory: SerovAA
- run: |
- python -m pytest tests/test_worker.py -v --tb=short
-
- # Необязательная проверка: собрать Docker Compose (без запуска)
- docker-build:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
-
- - name: Build Docker images
- working-directory: SerovAA
- run: docker compose build
From 3248d6332e224605cfb9c55265c1edb92834c949 Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 19:36:42 +0300
Subject: [PATCH 44/56] Update ci.yml
---
.github/workflows/ci.yml | 67 +++++++++++++++++++++++-----------------
1 file changed, 39 insertions(+), 28 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a9aaa55..80a5b67 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -9,58 +9,69 @@ on:
jobs:
test-backend:
runs-on: ubuntu-latest
+ services:
+ redis:
+ image: redis:alpine
+ ports:
+ - 6379:6379
+ options: >-
+ --health-cmd "redis-cli ping"
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
steps:
- uses: actions/checkout@v4
-
- - name: Set up Python
- uses: actions/setup-python@v5
+ - uses: actions/setup-python@v5
with:
python-version: '3.11'
-
- - name: Install backend dependencies
+ - name: Install dependencies
working-directory: SerovAA/backend
- run: |
- python -m pip install --upgrade pip
- pip install -r requirements.txt
-
- - name: Install dev dependencies
- run: pip install pytest pytest-cov
-
+ run: pip install -r requirements.txt
+ - name: Install pytest
+ run: pip install pytest
- name: Run backend tests
working-directory: SerovAA
run: |
- python -m pytest tests/test_backend.py -v --tb=short
+ # Ждём Redis (хотя healthcheck уже есть, на всякий случай)
+ sleep 2
+ PYTHONPATH=. python -m pytest tests/test_backend.py -v --tb=short
+ env:
+ REDIS_HOST: localhost # чтобы backend точно знал, где Redis
test-worker:
runs-on: ubuntu-latest
+ services:
+ redis:
+ image: redis:alpine
+ ports:
+ - 6379:6379
+ options: >-
+ --health-cmd "redis-cli ping"
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
steps:
- uses: actions/checkout@v4
-
- - name: Set up Python
- uses: actions/setup-python@v5
+ - uses: actions/setup-python@v5
with:
python-version: '3.11'
-
- - name: Install worker dependencies
+ - name: Install dependencies
working-directory: SerovAA/worker
- run: |
- python -m pip install --upgrade pip
- pip install -r requirements.txt
-
- - name: Install dev dependencies
- run: pip install pytest pytest-cov
-
+ run: pip install -r requirements.txt
+ - name: Install pytest
+ run: pip install pytest
- name: Run worker tests
working-directory: SerovAA
run: |
- python -m pytest tests/test_worker.py -v --tb=short
+ sleep 2
+ PYTHONPATH=. python -m pytest tests/test_worker.py -v --tb=short
+ env:
+ REDIS_HOST: localhost
- # Необязательная проверка: собрать Docker Compose (без запуска)
docker-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
-
- name: Build Docker images
working-directory: SerovAA
run: docker compose build
From cca7fe203d645b8772c814fb5f20295b90a0f648 Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 19:43:32 +0300
Subject: [PATCH 45/56] Update worker.py
---
SerovAA/worker/worker.py | 39 +++++++++++++++++++--------------------
1 file changed, 19 insertions(+), 20 deletions(-)
diff --git a/SerovAA/worker/worker.py b/SerovAA/worker/worker.py
index 0e057c1..5b150af 100644
--- a/SerovAA/worker/worker.py
+++ b/SerovAA/worker/worker.py
@@ -15,23 +15,22 @@ def generate_password(length, use_digits, use_special):
chars += '!@#$%^&*()'
return ''.join(random.choice(chars) for _ in range(length))
-while True:
- task_id = redis_client.brpop('password_tasks')[1].decode()
-
- # Simulate long processing
- time.sleep(2)
-
- task_data = json.loads(redis_client.get(f"task:{task_id}"))
- try:
- password = generate_password(
- task_data['length'],
- task_data['use_digits'],
- task_data['use_special']
- )
- task_data['status'] = 'completed'
- task_data['password'] = password
- except Exception as e:
- task_data['status'] = 'failed'
- task_data['error'] = str(e)
-
- redis_client.set(f"task:{task_id}", json.dumps(task_data))
+# Этот код будет выполняться ТОЛЬКО при запуске worker.py как скрипта,
+# но НЕ при импорте из тестов
+if __name__ == "__main__":
+ while True:
+ task_id = redis_client.brpop('password_tasks')[1].decode()
+ time.sleep(2) # имитация долгой обработки
+ task_data = json.loads(redis_client.get(f"task:{task_id}"))
+ try:
+ password = generate_password(
+ task_data['length'],
+ task_data['use_digits'],
+ task_data['use_special']
+ )
+ task_data['status'] = 'completed'
+ task_data['password'] = password
+ except Exception as e:
+ task_data['status'] = 'failed'
+ task_data['error'] = str(e)
+ redis_client.set(f"task:{task_id}", json.dumps(task_data))
From 0fa9b97c253c482509c1f72ecd77814a2a239be3 Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 19:49:38 +0300
Subject: [PATCH 46/56] Update README.md
---
SerovAA/README.md | 97 +++++++++++++++++++++++++++--------------------
1 file changed, 55 insertions(+), 42 deletions(-)
diff --git a/SerovAA/README.md b/SerovAA/README.md
index 8d5dd3f..6c7fca5 100644
--- a/SerovAA/README.md
+++ b/SerovAA/README.md
@@ -1,60 +1,73 @@
-# Password Generator - Microservices Demo
+# Password Generator — микросервисное веб-приложение
-## Architecture
-- **Nginx**: Reverse proxy and static frontend server
-- **Frontend**: HTML/JS UI served by Nginx
-- **Backend (Flask)**: API endpoint for generating password tasks
-- **Worker (Python)**: Long-running password generation service
-- **Redis**: Task queue and result storage
+## Описание проекта
-## Run
+Генератор паролей с микросервисной архитектурой.
+Состоит из пяти компонентов:
+- **Nginx** — обратный прокси и точка входа (порт 8080)
+- **Frontend** — статический HTML/JS интерфейс
+- **Backend (Flask)** — API для создания задач генерации паролей
+- **Worker** — фоновый сервис, выполняющий длительную генерацию
+- **Redis** — брокер задач и хранилище результатов
+
+## Запуск
+
+bash
docker compose up -d
+Приложение будет доступно по адресу: http://localhost:8080
+
+## Остановить:
-## How it works
-- Frontend submits generation request to backend API
-- Backend creates task in Redis and returns task_id
-- Worker picks up task from Redis queue
-- Worker generates password (simulated 2s delay)
-- Frontend polls for result and displays password
+bash
+docker compose down
-----
-## Проверка работоспособности
+## Взаимодействие сервисов
+Пользователь вводит параметры пароля (длина, цифры, спецсимволы) и нажимает «Generate».
-### Клонируем репозиторий
-1. git clone https://github.com/SoftwareEngineering2026/Practice105.git
-2. cd Practice105/SerovAA/
+Frontend отправляет POST-запрос на /api/generate в Backend.
-### Создаём структуру и файлы (скопируй вышеуказанные файлы)
+Backend создаёт задачу в Redis и возвращает task_id.
-### Запускаем
-docker compose up -d
+Worker забирает задачу из очереди, генерирует пароль (с имитацией задержки 2 сек) и сохраняет результат.
-### Проверяем
-curl http://localhost:8080/api/generate -X POST -H "Content-Type: application/json" -d '{"length": 16, "use_digits": true, "use_special": true}'
+Frontend каждую секунду опрашивает /api/result/ и отображает пароль.
-### Останавливаем
-docker compose down
+## Проверка работоспособности (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/
## 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)
-Проект использует **GitHub Actions** для автоматической проверки при pull request в `main` ветку.
+✅ Все сервисы общаются через внутреннюю сеть Docker
-### Что проверяется:
-- ✅ Корректность API (backend тесты)
-- ✅ Генерация паролей (worker тесты)
-- ✅ Сборка Docker образов (docker compose build)
+✅ Длительные задачи вынесены в отдельный Worker (не блокируют Backend)
-### Как запустить тесты локально:
-- bash
-- pip install pytest
-- pytest tests/
+✅ Полностью микросервисная архитектура
-----
-## Особенности решения:
+✅ Запуск одной командой docker compose up
-- ✅ Одна точка входа — Nginx на порту 8080
-- ✅ Все сервисы в одной сети Docker
-- ✅ Worker выполняет "длительные задачи" (generation с delay)
-- ✅ Готово к запуску одной командой docker compose up
-- ✅ Полностью микросервисный (frontend, backend, worker, redis)
+✅ Автоматическое CI-тестирование при Pull Request
From 52ee81a39e01c716020b87400bbceee3e441e2c0 Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 19:49:59 +0300
Subject: [PATCH 47/56] Update README.md
---
SerovAA/README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/SerovAA/README.md b/SerovAA/README.md
index 6c7fca5..72a5ada 100644
--- a/SerovAA/README.md
+++ b/SerovAA/README.md
@@ -14,13 +14,13 @@
## Запуск
bash
-docker compose up -d
+- docker compose up -d
Приложение будет доступно по адресу: http://localhost:8080
## Остановить:
bash
-docker compose down
+- docker compose down
## Взаимодействие сервисов
Пользователь вводит параметры пароля (длина, цифры, спецсимволы) и нажимает «Generate».
From 39658c5a7c0007c93e69b62acd1b5e7e746a15fd Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 19:50:07 +0300
Subject: [PATCH 48/56] Update README.md
---
SerovAA/README.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/SerovAA/README.md b/SerovAA/README.md
index 72a5ada..c6c2b27 100644
--- a/SerovAA/README.md
+++ b/SerovAA/README.md
@@ -15,6 +15,7 @@
bash
- docker compose up -d
+
Приложение будет доступно по адресу: http://localhost:8080
## Остановить:
From 4f10ac5434f139b198c21363c17ef24d3bccaac4 Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 19:56:08 +0300
Subject: [PATCH 49/56] Update README.md
---
SerovAA/README.md | 4 ----
1 file changed, 4 deletions(-)
diff --git a/SerovAA/README.md b/SerovAA/README.md
index c6c2b27..950f8b8 100644
--- a/SerovAA/README.md
+++ b/SerovAA/README.md
@@ -25,13 +25,9 @@ bash
## Взаимодействие сервисов
Пользователь вводит параметры пароля (длина, цифры, спецсимволы) и нажимает «Generate».
-
Frontend отправляет POST-запрос на /api/generate в Backend.
-
Backend создаёт задачу в Redis и возвращает task_id.
-
Worker забирает задачу из очереди, генерирует пароль (с имитацией задержки 2 сек) и сохраняет результат.
-
Frontend каждую секунду опрашивает /api/result/ и отображает пароль.
## Проверка работоспособности (curl)
From b5cf90e9bbce8139738eba6f8c9cce1118ec572b Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 19:56:25 +0300
Subject: [PATCH 50/56] Update README.md
---
SerovAA/README.md | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/SerovAA/README.md b/SerovAA/README.md
index 950f8b8..a9de0c5 100644
--- a/SerovAA/README.md
+++ b/SerovAA/README.md
@@ -24,11 +24,11 @@ bash
- docker compose down
## Взаимодействие сервисов
-Пользователь вводит параметры пароля (длина, цифры, спецсимволы) и нажимает «Generate».
-Frontend отправляет POST-запрос на /api/generate в Backend.
-Backend создаёт задачу в Redis и возвращает task_id.
-Worker забирает задачу из очереди, генерирует пароль (с имитацией задержки 2 сек) и сохраняет результат.
-Frontend каждую секунду опрашивает /api/result/ и отображает пароль.
+- Пользователь вводит параметры пароля (длина, цифры, спецсимволы) и нажимает «Generate».
+- Frontend отправляет POST-запрос на /api/generate в Backend.
+- Backend создаёт задачу в Redis и возвращает task_id.
+- Worker забирает задачу из очереди, генерирует пароль (с имитацией задержки 2 сек) и сохраняет результат.
+- Frontend каждую секунду опрашивает /api/result/ и отображает пароль.
## Проверка работоспособности (curl)
Создать задачу на генерацию пароля:
From 2f2e78392ea5aa6510252390160dca42ceab0a08 Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 20:00:42 +0300
Subject: [PATCH 51/56] Update README.md
---
SerovAA/README.md | 5 -----
1 file changed, 5 deletions(-)
diff --git a/SerovAA/README.md b/SerovAA/README.md
index a9de0c5..e35d0e1 100644
--- a/SerovAA/README.md
+++ b/SerovAA/README.md
@@ -58,13 +58,8 @@ curl http://localhost:8080/api/result/
## Особенности реализации
✅ Единая точка входа — Nginx (порт 8080)
-
✅ Все сервисы общаются через внутреннюю сеть Docker
-
✅ Длительные задачи вынесены в отдельный Worker (не блокируют Backend)
-
✅ Полностью микросервисная архитектура
-
✅ Запуск одной командой docker compose up
-
✅ Автоматическое CI-тестирование при Pull Request
From c34a17caef7ea5b6719a51a79f9c20bb59af5a9f Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 20:01:26 +0300
Subject: [PATCH 52/56] Update README.md
---
SerovAA/README.md | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/SerovAA/README.md b/SerovAA/README.md
index e35d0e1..a9de0c5 100644
--- a/SerovAA/README.md
+++ b/SerovAA/README.md
@@ -58,8 +58,13 @@ curl http://localhost:8080/api/result/
## Особенности реализации
✅ Единая точка входа — Nginx (порт 8080)
+
✅ Все сервисы общаются через внутреннюю сеть Docker
+
✅ Длительные задачи вынесены в отдельный Worker (не блокируют Backend)
+
✅ Полностью микросервисная архитектура
+
✅ Запуск одной командой docker compose up
+
✅ Автоматическое CI-тестирование при Pull Request
From f365627c8ae369d0bbcb2e99f4dc7ccdd7b45c00 Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 20:13:12 +0300
Subject: [PATCH 53/56] Create success.png
---
SerovAA/screenshots/success.png | 1 +
1 file changed, 1 insertion(+)
create mode 100644 SerovAA/screenshots/success.png
diff --git a/SerovAA/screenshots/success.png b/SerovAA/screenshots/success.png
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/SerovAA/screenshots/success.png
@@ -0,0 +1 @@
+
From 9a9668102b1834a621450dc123df4f925462cd46 Mon Sep 17 00:00:00 2001
From: reSSSno <46084217+reSSSno@users.noreply.github.com>
Date: Sat, 23 May 2026 20:13:47 +0300
Subject: [PATCH 54/56] Add files via upload
---
SerovAA/screenshots/success.png | Bin 1 -> 88166 bytes
1 file changed, 0 insertions(+), 0 deletions(-)
diff --git a/SerovAA/screenshots/success.png b/SerovAA/screenshots/success.png
index 8b137891791fe96927ad78e64b0aad7bded08bdc..de3ee935f3408ac7b7f772102fae3450a842f596 100644
GIT binary patch
literal 88166
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