kin: KIN-049 Кнопка Deploy на странице задачи после approve. Для каждого проекта настраивается deploy-команда (git push, scp, ssh restart). В Settings проекта.
This commit is contained in:
parent
860ef3f6c9
commit
d50bd703ae
11 changed files with 517 additions and 61 deletions
|
|
@ -939,3 +939,135 @@ def test_patch_task_title_and_brief_text_together(client):
|
|||
data = r.json()
|
||||
assert data["title"] == "Совместное"
|
||||
assert data["brief"]["text"] == "и описание"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# KIN-049 — Deploy: миграция, PATCH deploy_command, POST /deploy
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_deploy_command_column_exists_in_schema(client):
|
||||
"""Миграция: PRAGMA table_info(projects) подтверждает наличие deploy_command (decision #74)."""
|
||||
from core.db import init_db
|
||||
conn = init_db(api_module.DB_PATH)
|
||||
cols = {row[1] for row in conn.execute("PRAGMA table_info(projects)").fetchall()}
|
||||
conn.close()
|
||||
assert "deploy_command" in cols
|
||||
|
||||
|
||||
def test_patch_project_deploy_command_persisted_via_sql(client):
|
||||
"""PATCH с deploy_command сохраняется в БД — прямой SQL (decision #55)."""
|
||||
client.patch("/api/projects/p1", json={"deploy_command": "echo hello"})
|
||||
|
||||
from core.db import init_db
|
||||
conn = init_db(api_module.DB_PATH)
|
||||
row = conn.execute("SELECT deploy_command FROM projects WHERE id = 'p1'").fetchone()
|
||||
conn.close()
|
||||
assert row is not None
|
||||
assert row[0] == "echo hello"
|
||||
|
||||
|
||||
def test_patch_project_deploy_command_returned_in_response(client):
|
||||
"""После PATCH ответ содержит обновлённый deploy_command."""
|
||||
r = client.patch("/api/projects/p1", json={"deploy_command": "git push origin main"})
|
||||
assert r.status_code == 200
|
||||
assert r.json()["deploy_command"] == "git push origin main"
|
||||
|
||||
|
||||
def test_patch_project_deploy_command_empty_string_clears_to_null(client):
|
||||
"""PATCH с пустой строкой очищает deploy_command → NULL (decision #68)."""
|
||||
client.patch("/api/projects/p1", json={"deploy_command": "echo hello"})
|
||||
client.patch("/api/projects/p1", json={"deploy_command": ""})
|
||||
|
||||
from core.db import init_db
|
||||
conn = init_db(api_module.DB_PATH)
|
||||
row = conn.execute("SELECT deploy_command FROM projects WHERE id = 'p1'").fetchone()
|
||||
conn.close()
|
||||
assert row[0] is None
|
||||
|
||||
|
||||
def test_deploy_project_executes_command_returns_stdout(client):
|
||||
"""POST /deploy — команда echo → stdout присутствует в ответе."""
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
client.patch("/api/projects/p1", json={"deploy_command": "echo deployed"})
|
||||
|
||||
mock_result = MagicMock()
|
||||
mock_result.returncode = 0
|
||||
mock_result.stdout = "deployed\n"
|
||||
mock_result.stderr = ""
|
||||
|
||||
with patch("web.api.subprocess.run", return_value=mock_result):
|
||||
r = client.post("/api/projects/p1/deploy")
|
||||
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert data["success"] is True
|
||||
assert data["exit_code"] == 0
|
||||
assert "deployed" in data["stdout"]
|
||||
assert "duration_seconds" in data
|
||||
|
||||
|
||||
def test_deploy_project_without_deploy_command_returns_400(client):
|
||||
"""POST /deploy для проекта без deploy_command → 400."""
|
||||
r = client.post("/api/projects/p1/deploy")
|
||||
assert r.status_code == 400
|
||||
|
||||
|
||||
def test_deploy_project_not_found_returns_404(client):
|
||||
"""POST /deploy для несуществующего проекта → 404."""
|
||||
r = client.post("/api/projects/NOPE/deploy")
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
def test_deploy_project_failed_command_returns_success_false(client):
|
||||
"""POST /deploy — ненулевой exit_code → success=False (команда выполнилась, но упала)."""
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
client.patch("/api/projects/p1", json={"deploy_command": "exit 1"})
|
||||
|
||||
mock_result = MagicMock()
|
||||
mock_result.returncode = 1
|
||||
mock_result.stdout = ""
|
||||
mock_result.stderr = "error occurred"
|
||||
|
||||
with patch("web.api.subprocess.run", return_value=mock_result):
|
||||
r = client.post("/api/projects/p1/deploy")
|
||||
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert data["success"] is False
|
||||
assert data["exit_code"] == 1
|
||||
assert "error occurred" in data["stderr"]
|
||||
|
||||
|
||||
def test_deploy_project_timeout_returns_504(client):
|
||||
"""POST /deploy — timeout → 504."""
|
||||
from unittest.mock import patch
|
||||
import subprocess
|
||||
|
||||
client.patch("/api/projects/p1", json={"deploy_command": "sleep 100"})
|
||||
|
||||
with patch("web.api.subprocess.run", side_effect=subprocess.TimeoutExpired("sleep 100", 60)):
|
||||
r = client.post("/api/projects/p1/deploy")
|
||||
|
||||
assert r.status_code == 504
|
||||
|
||||
|
||||
def test_task_full_includes_project_deploy_command(client):
|
||||
"""GET /api/tasks/{id}/full включает project_deploy_command из таблицы projects."""
|
||||
client.patch("/api/projects/p1", json={"deploy_command": "git push"})
|
||||
|
||||
r = client.get("/api/tasks/P1-001/full")
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert "project_deploy_command" in data
|
||||
assert data["project_deploy_command"] == "git push"
|
||||
|
||||
|
||||
def test_task_full_project_deploy_command_none_when_not_set(client):
|
||||
"""GET /api/tasks/{id}/full возвращает project_deploy_command=None когда не задана."""
|
||||
r = client.get("/api/tasks/P1-001/full")
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert "project_deploy_command" in data
|
||||
assert data["project_deploy_command"] is None
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue