diff --git a/Makefile b/Makefile index 36277a8..24d27ac 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help dev build-frontend install run test deploy +.PHONY: help dev build-frontend install run serve test deploy FRONTEND_DIR := web/frontend @@ -7,9 +7,10 @@ help: @echo " make install — установить зависимости frontend (npm install)" @echo " make dev — запустить frontend в dev-режиме (vite, hot-reload)" @echo " make build-frontend — собрать production-билд frontend в $(FRONTEND_DIR)/dist/" - @echo " make run — запустить API-сервер (uvicorn)" + @echo " make run — запустить API-сервер в dev-режиме (uvicorn --reload)" + @echo " make serve — запустить API-сервер в prod-режиме (uvicorn, без --reload)" @echo " make test — запустить все тесты (pytest + vitest)" - @echo " make deploy — собрать frontend и запустить API-сервер" + @echo " make deploy — установить python-зависимости, собрать frontend и запустить prod-сервер" install: cd $(FRONTEND_DIR) && npm install @@ -23,8 +24,13 @@ build-frontend: run: uvicorn web.api:app --reload --host 0.0.0.0 --port 8000 +serve: + uvicorn web.api:app --host 0.0.0.0 --port 8000 + test: pytest tests/ cd $(FRONTEND_DIR) && npm run test -deploy: build-frontend run +deploy: build-frontend + pip install -r requirements.txt + $(MAKE) serve diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1284d21 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +click>=8.0 +fastapi>=0.110 +uvicorn>=0.29 +cryptography>=41.0 +python-multipart>=0.0.9 diff --git a/tests/test_kin_fix_006_regression.py b/tests/test_kin_fix_006_regression.py new file mode 100644 index 0000000..c2aca3c --- /dev/null +++ b/tests/test_kin_fix_006_regression.py @@ -0,0 +1,156 @@ +"""Regression tests for KIN-FIX-006: 'ssh_key' must be a valid auth_type. + +Root cause: VALID_AUTH_TYPES did not include 'ssh_key', causing 422 on POST credentials. +Fix: VALID_AUTH_TYPES = {"password", "key", "ssh_key"} (web/api.py line 1028). + +Acceptance criteria: + 1. POST /projects/{id}/environments with auth_type='ssh_key' returns 201 (not 422) + 2. auth_type='key' still returns 201 + 3. auth_type='password' still returns 201 + 4. auth_type='ftp' (invalid) returns 422 +""" +import pytest +from unittest.mock import patch, MagicMock + + +# --------------------------------------------------------------------------- +# Fixture +# --------------------------------------------------------------------------- + +@pytest.fixture +def client(tmp_path): + import web.api as api_module + api_module.DB_PATH = tmp_path / "test_fix006.db" + # Re-import app after setting DB_PATH so init_db uses the new path + from importlib import reload + import web.api + reload(web.api) + api_module.DB_PATH = tmp_path / "test_fix006.db" + from web.api import app + from fastapi.testclient import TestClient + c = TestClient(app) + c.post("/api/projects", json={"id": "testproj", "name": "Test Project", "path": "/testproj"}) + return c + + +# --------------------------------------------------------------------------- +# Tests: VALID_AUTH_TYPES validation +# --------------------------------------------------------------------------- + +def test_create_environment_ssh_key_auth_type_returns_201(client): + """Regression KIN-FIX-006: auth_type='ssh_key' must return 201, not 422.""" + r = client.post("/api/projects/testproj/environments", json={ + "name": "prod-ssh", + "host": "10.0.0.1", + "username": "deploy", + "auth_type": "ssh_key", + "auth_value": "-----BEGIN RSA PRIVATE KEY-----", + }) + assert r.status_code == 201, ( + f"auth_type='ssh_key' must be accepted (201), got {r.status_code}: {r.text}" + ) + + +def test_create_environment_key_auth_type_still_valid(client): + """auth_type='key' must still return 201 after the fix.""" + r = client.post("/api/projects/testproj/environments", json={ + "name": "prod-key", + "host": "10.0.0.2", + "username": "deploy", + "auth_type": "key", + "auth_value": "keydata", + }) + assert r.status_code == 201, ( + f"auth_type='key' must still be valid (201), got {r.status_code}: {r.text}" + ) + + +def test_create_environment_password_auth_type_still_valid(client): + """auth_type='password' must still return 201 after the fix.""" + r = client.post("/api/projects/testproj/environments", json={ + "name": "prod-pass", + "host": "10.0.0.3", + "username": "root", + "auth_type": "password", + "auth_value": "s3cr3t", + }) + assert r.status_code == 201, ( + f"auth_type='password' must still be valid (201), got {r.status_code}: {r.text}" + ) + + +def test_create_environment_invalid_auth_type_returns_422(client): + """Invalid auth_type (e.g. 'ftp') must return 422 Unprocessable Entity.""" + r = client.post("/api/projects/testproj/environments", json={ + "name": "prod-ftp", + "host": "10.0.0.4", + "username": "ftpuser", + "auth_type": "ftp", + "auth_value": "password123", + }) + assert r.status_code == 422, ( + f"auth_type='ftp' must be rejected (422), got {r.status_code}: {r.text}" + ) + + +def test_create_environment_empty_auth_type_returns_422(client): + """Empty string auth_type must return 422.""" + r = client.post("/api/projects/testproj/environments", json={ + "name": "prod-empty", + "host": "10.0.0.5", + "username": "root", + "auth_type": "", + }) + assert r.status_code == 422, ( + f"auth_type='' must be rejected (422), got {r.status_code}: {r.text}" + ) + + +def test_create_environment_default_auth_type_is_password(client): + """Default auth_type (omitted) must be 'password' and return 201.""" + r = client.post("/api/projects/testproj/environments", json={ + "name": "prod-default", + "host": "10.0.0.6", + "username": "root", + "auth_value": "pass", + # auth_type intentionally omitted — defaults to 'password' + }) + assert r.status_code == 201, ( + f"Default auth_type must be accepted (201), got {r.status_code}: {r.text}" + ) + + +# --------------------------------------------------------------------------- +# Test: VALID_AUTH_TYPES content (unit-level) +# --------------------------------------------------------------------------- + +def test_valid_auth_types_contains_ssh_key(): + """Unit: VALID_AUTH_TYPES set must include 'ssh_key'.""" + from web.api import VALID_AUTH_TYPES + assert "ssh_key" in VALID_AUTH_TYPES, ( + f"VALID_AUTH_TYPES must contain 'ssh_key', got: {VALID_AUTH_TYPES}" + ) + + +def test_valid_auth_types_contains_key(): + """Unit: VALID_AUTH_TYPES set must include 'key'.""" + from web.api import VALID_AUTH_TYPES + assert "key" in VALID_AUTH_TYPES, ( + f"VALID_AUTH_TYPES must contain 'key', got: {VALID_AUTH_TYPES}" + ) + + +def test_valid_auth_types_contains_password(): + """Unit: VALID_AUTH_TYPES set must include 'password'.""" + from web.api import VALID_AUTH_TYPES + assert "password" in VALID_AUTH_TYPES, ( + f"VALID_AUTH_TYPES must contain 'password', got: {VALID_AUTH_TYPES}" + ) + + +def test_valid_auth_types_excludes_ftp(): + """Unit: VALID_AUTH_TYPES must NOT include 'ftp'.""" + from web.api import VALID_AUTH_TYPES + assert "ftp" not in VALID_AUTH_TYPES, ( + f"VALID_AUTH_TYPES must not contain 'ftp', got: {VALID_AUTH_TYPES}" + )