kin: KIN-ARCH-001 Добавить серверную валидацию ssh_host для operations-проектов
This commit is contained in:
parent
af554e15fa
commit
ba04e7ad84
3 changed files with 141 additions and 5 deletions
44
core/db.py
44
core/db.py
|
|
@ -13,7 +13,7 @@ SCHEMA = """
|
||||||
CREATE TABLE IF NOT EXISTS projects (
|
CREATE TABLE IF NOT EXISTS projects (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
path TEXT NOT NULL,
|
path TEXT CHECK (path IS NOT NULL OR project_type = 'operations'),
|
||||||
tech_stack JSON,
|
tech_stack JSON,
|
||||||
status TEXT DEFAULT 'active',
|
status TEXT DEFAULT 'active',
|
||||||
priority INTEGER DEFAULT 5,
|
priority INTEGER DEFAULT 5,
|
||||||
|
|
@ -364,6 +364,48 @@ def _migrate(conn: sqlite3.Connection):
|
||||||
""")
|
""")
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
# Migrate projects.path from NOT NULL to nullable (KIN-ARCH-003)
|
||||||
|
# SQLite doesn't support ALTER COLUMN, so we recreate the table.
|
||||||
|
path_col_rows = conn.execute("PRAGMA table_info(projects)").fetchall()
|
||||||
|
path_col = next((r for r in path_col_rows if r[1] == "path"), None)
|
||||||
|
if path_col and path_col[3] == 1: # notnull == 1, migration needed
|
||||||
|
conn.executescript("""
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
CREATE TABLE projects_new (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
path TEXT CHECK (path IS NOT NULL OR project_type = 'operations'),
|
||||||
|
tech_stack JSON,
|
||||||
|
status TEXT DEFAULT 'active',
|
||||||
|
priority INTEGER DEFAULT 5,
|
||||||
|
pm_prompt TEXT,
|
||||||
|
claude_md_path TEXT,
|
||||||
|
forgejo_repo TEXT,
|
||||||
|
language TEXT DEFAULT 'ru',
|
||||||
|
execution_mode TEXT NOT NULL DEFAULT 'review',
|
||||||
|
deploy_command TEXT,
|
||||||
|
project_type TEXT DEFAULT 'development',
|
||||||
|
ssh_host TEXT,
|
||||||
|
ssh_user TEXT,
|
||||||
|
ssh_key_path TEXT,
|
||||||
|
ssh_proxy_jump TEXT,
|
||||||
|
description TEXT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
autocommit_enabled INTEGER DEFAULT 0,
|
||||||
|
obsidian_vault_path TEXT
|
||||||
|
);
|
||||||
|
INSERT INTO projects_new
|
||||||
|
SELECT id, name, path, tech_stack, status, priority,
|
||||||
|
pm_prompt, claude_md_path, forgejo_repo, language,
|
||||||
|
execution_mode, deploy_command, project_type,
|
||||||
|
ssh_host, ssh_user, ssh_key_path, ssh_proxy_jump,
|
||||||
|
description, created_at, autocommit_enabled, obsidian_vault_path
|
||||||
|
FROM projects;
|
||||||
|
DROP TABLE projects;
|
||||||
|
ALTER TABLE projects_new RENAME TO projects;
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
|
""")
|
||||||
|
|
||||||
# Rename legacy 'auto' → 'auto_complete' (KIN-063)
|
# Rename legacy 'auto' → 'auto_complete' (KIN-063)
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE projects SET execution_mode = 'auto_complete' WHERE execution_mode = 'auto'"
|
"UPDATE projects SET execution_mode = 'auto_complete' WHERE execution_mode = 'auto'"
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ def create_project(
|
||||||
conn: sqlite3.Connection,
|
conn: sqlite3.Connection,
|
||||||
id: str,
|
id: str,
|
||||||
name: str,
|
name: str,
|
||||||
path: str,
|
path: str | None = None,
|
||||||
tech_stack: list | None = None,
|
tech_stack: list | None = None,
|
||||||
status: str = "active",
|
status: str = "active",
|
||||||
priority: int = 5,
|
priority: int = 5,
|
||||||
|
|
|
||||||
|
|
@ -1311,16 +1311,110 @@ def test_patch_project_invalid_type_returns_400(client):
|
||||||
|
|
||||||
|
|
||||||
def test_create_operations_project_without_ssh_host_allowed(client):
|
def test_create_operations_project_without_ssh_host_allowed(client):
|
||||||
"""KIN-071: API не валидирует ssh_host на стороне бэкенда — проект создаётся без него."""
|
"""Регрессионный тест KIN-ARCH-001: воспроизводит СЛОМАННОЕ поведение до фикса.
|
||||||
|
|
||||||
|
До фикса: POST operations-проекта без ssh_host возвращал 200.
|
||||||
|
После фикса: должен возвращать 422 (Pydantic model_validator).
|
||||||
|
Этот тест НАМЕРЕННО проверяет, что старое поведение больше не существует.
|
||||||
|
"""
|
||||||
r = client.post("/api/projects", json={
|
r = client.post("/api/projects", json={
|
||||||
"id": "srv2",
|
"id": "srv2",
|
||||||
"name": "Server No SSH",
|
"name": "Server No SSH",
|
||||||
"path": "",
|
"path": "",
|
||||||
"project_type": "operations",
|
"project_type": "operations",
|
||||||
})
|
})
|
||||||
|
# Фикс KIN-ARCH-001: был 200, стал 422
|
||||||
|
assert r.status_code == 422, (
|
||||||
|
"Регрессия KIN-ARCH-001: POST operations-проекта без ssh_host "
|
||||||
|
"должен возвращать 422, а не 200"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# KIN-ARCH-001 — серверная валидация ssh_host для operations-проектов
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def test_kin_arch_001_operations_without_ssh_host_returns_422(client):
|
||||||
|
"""Регрессионный тест KIN-ARCH-001: POST /api/projects с project_type='operations'
|
||||||
|
и без ssh_host → 422 Unprocessable Entity."""
|
||||||
|
r = client.post("/api/projects", json={
|
||||||
|
"id": "ops_no_ssh",
|
||||||
|
"name": "Ops Without SSH",
|
||||||
|
"path": "",
|
||||||
|
"project_type": "operations",
|
||||||
|
})
|
||||||
|
assert r.status_code == 422
|
||||||
|
|
||||||
|
|
||||||
|
def test_kin_arch_001_operations_with_empty_ssh_host_returns_422(client):
|
||||||
|
"""Регрессионный тест KIN-ARCH-001: пустая строка в ssh_host считается отсутствующим
|
||||||
|
значением → 422."""
|
||||||
|
r = client.post("/api/projects", json={
|
||||||
|
"id": "ops_empty_ssh",
|
||||||
|
"name": "Ops Empty SSH",
|
||||||
|
"path": "",
|
||||||
|
"project_type": "operations",
|
||||||
|
"ssh_host": "",
|
||||||
|
})
|
||||||
|
assert r.status_code == 422
|
||||||
|
|
||||||
|
|
||||||
|
def test_kin_arch_001_operations_with_valid_ssh_host_returns_200(client):
|
||||||
|
"""Регрессионный тест KIN-ARCH-001: POST /api/projects с project_type='operations'
|
||||||
|
и корректным ssh_host → 200, проект создаётся."""
|
||||||
|
r = client.post("/api/projects", json={
|
||||||
|
"id": "ops_with_ssh",
|
||||||
|
"name": "Ops With SSH",
|
||||||
|
"path": "",
|
||||||
|
"project_type": "operations",
|
||||||
|
"ssh_host": "10.0.0.42",
|
||||||
|
})
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
assert r.json()["project_type"] == "operations"
|
data = r.json()
|
||||||
assert r.json()["ssh_host"] is None
|
assert data["project_type"] == "operations"
|
||||||
|
assert data["ssh_host"] == "10.0.0.42"
|
||||||
|
|
||||||
|
|
||||||
|
def test_kin_arch_001_development_without_ssh_host_allowed(client):
|
||||||
|
"""Регрессионный тест KIN-ARCH-001: project_type='development' без ssh_host
|
||||||
|
должен создаваться без ошибок — валидатор срабатывает только для operations."""
|
||||||
|
r = client.post("/api/projects", json={
|
||||||
|
"id": "dev_no_ssh",
|
||||||
|
"name": "Dev No SSH",
|
||||||
|
"path": "/dev",
|
||||||
|
"project_type": "development",
|
||||||
|
})
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json()["project_type"] == "development"
|
||||||
|
|
||||||
|
|
||||||
|
def test_kin_arch_001_research_without_ssh_host_allowed(client):
|
||||||
|
"""Регрессионный тест KIN-ARCH-001: project_type='research' без ssh_host
|
||||||
|
должен создаваться без ошибок."""
|
||||||
|
r = client.post("/api/projects", json={
|
||||||
|
"id": "res_no_ssh",
|
||||||
|
"name": "Research No SSH",
|
||||||
|
"path": "/research",
|
||||||
|
"project_type": "research",
|
||||||
|
})
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json()["project_type"] == "research"
|
||||||
|
|
||||||
|
|
||||||
|
def test_kin_arch_001_422_error_message_mentions_ssh_host(client):
|
||||||
|
"""Регрессионный тест KIN-ARCH-001: тело 422-ответа содержит сообщение об ошибке
|
||||||
|
с упоминанием ssh_host."""
|
||||||
|
r = client.post("/api/projects", json={
|
||||||
|
"id": "ops_err_msg",
|
||||||
|
"name": "Check Error Message",
|
||||||
|
"path": "",
|
||||||
|
"project_type": "operations",
|
||||||
|
})
|
||||||
|
assert r.status_code == 422
|
||||||
|
body = r.json()
|
||||||
|
# Pydantic возвращает detail со списком ошибок
|
||||||
|
detail_str = str(body)
|
||||||
|
assert "ssh_host" in detail_str
|
||||||
|
|
||||||
|
|
||||||
def test_create_research_project_type_accepted(client):
|
def test_create_research_project_type_accepted(client):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue