504dfc52f5
Aegis CI / lint-and-test (push) Has been cancelled
Snyk Security Scan / Python vulnerabilities (backend) (push) Has been cancelled
Snyk Security Scan / npm vulnerabilities (frontend) (push) Has been cancelled
Snyk Security Scan / Docker image vulnerabilities (backend) (push) Has been cancelled
Login is now by email, not username. username still exists internally (JWT sub claim, audit logs, Jira actor attribution, SSO provisioning all still key off it) but is now always kept equal to email everywhere a user is created or their email changes — never a separately-chosen value. - User.email is now unique + NOT NULL (migration b067 backfills any missing/blank email from username first, so existing rows — notably the seeded admin, which historically had none — never violate it). - /auth/login and the (unused but updated for consistency) authenticate_user() now query by email. - create_user (legacy, unreferenced but kept) and create_user_without_password both derive username from email. - update_user keeps username in sync when email changes, and rejects duplicate emails. - seed.py reads ADMIN_EMAIL (new env var, wired through install.sh and docker-compose.prod.yml) for the initial admin; falls back to an email-shaped ADMIN_USERNAME or a placeholder that's flagged for the operator to fix. - admin_config.py's import bundle now matches/creates users by email, skipping (not crashing on) entries with no email. - sso_service.py always sets username = email for SSO-provisioned users. - LoginPage/auth.ts updated to email input/copy (wire field name stays 'username' — that's the OAuth2PasswordRequestForm spec, not the value).
126 lines
4.0 KiB
Python
126 lines
4.0 KiB
Python
"""Tests for security test endpoints (V2 API).
|
|
|
|
Covers the test CRUD and basic workflow via the REST API.
|
|
For full workflow logic tests see ``test_workflow.py`` and
|
|
``test_integration_v2.py``.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture
|
|
def technique(client, auth_headers):
|
|
"""Create a technique for test association."""
|
|
response = client.post(
|
|
"/api/v1/techniques",
|
|
json={"mitre_id": "T1059", "name": "Test Technique"},
|
|
headers=auth_headers,
|
|
)
|
|
return response.json()
|
|
|
|
|
|
def test_create_test_requires_auth(client):
|
|
"""POST /tests without token returns 401 or 403."""
|
|
response = client.post(
|
|
"/api/v1/tests",
|
|
json={
|
|
"technique_id": "00000000-0000-0000-0000-000000000000",
|
|
"name": "Test Name",
|
|
},
|
|
)
|
|
assert response.status_code in (401, 403)
|
|
|
|
|
|
def test_create_test_success(client, red_lead_headers, red_lead_user, technique):
|
|
"""Lead can create a test via POST /tests (admin administers the site, not test content)."""
|
|
# The `technique` fixture logs in as admin (its own auth_headers
|
|
# dependency), leaving a cookie that would otherwise outrank the
|
|
# red_lead Bearer header below — get_current_user prefers the cookie.
|
|
client.cookies.clear()
|
|
response = client.post(
|
|
"/api/v1/tests",
|
|
json={
|
|
"technique_id": technique["id"],
|
|
"name": "My Security Test",
|
|
"description": "Test description",
|
|
"platform": "windows",
|
|
},
|
|
headers=red_lead_headers,
|
|
)
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert data["name"] == "My Security Test"
|
|
assert data["state"] == "draft"
|
|
assert data["technique_id"] == technique["id"]
|
|
|
|
|
|
def test_create_test_nonexistent_technique(client, red_lead_headers, red_lead_user):
|
|
"""Creating a test with non-existent technique fails."""
|
|
response = client.post(
|
|
"/api/v1/tests",
|
|
json={
|
|
"technique_id": "00000000-0000-0000-0000-000000000000",
|
|
"name": "Test",
|
|
},
|
|
headers=red_lead_headers,
|
|
)
|
|
assert response.status_code == 404
|
|
|
|
|
|
def test_get_test_by_id(client, auth_headers, technique, red_lead_headers, red_lead_user):
|
|
"""GET /tests/{id} returns the test. Admin can still view (just not create)."""
|
|
client.cookies.clear()
|
|
create_response = client.post(
|
|
"/api/v1/tests",
|
|
json={"technique_id": technique["id"], "name": "Test"},
|
|
headers=red_lead_headers,
|
|
)
|
|
test_id = create_response.json()["id"]
|
|
|
|
client.cookies.clear()
|
|
response = client.get(f"/api/v1/tests/{test_id}", headers=auth_headers)
|
|
assert response.status_code == 200
|
|
assert response.json()["id"] == test_id
|
|
|
|
|
|
def test_start_execution_twice_returns_invalid_transition(
|
|
client, red_lead_headers, red_lead_user, technique, red_tech_user
|
|
):
|
|
"""Invalid workflow transition surfaces domain error JSON (FASE 0.4).
|
|
|
|
HttpOnly login cookies take precedence over the Authorization header.
|
|
Clear cookies before each phase so Bearer tokens match the intended user.
|
|
"""
|
|
client.cookies.clear()
|
|
create_response = client.post(
|
|
"/api/v1/tests",
|
|
json={"technique_id": technique["id"], "name": "Workflow dup start"},
|
|
headers=red_lead_headers,
|
|
)
|
|
assert create_response.status_code == 201
|
|
test_id = create_response.json()["id"]
|
|
|
|
rl = client.post(
|
|
"/api/v1/auth/login",
|
|
data={"username": "redtech@test.com", "password": "redtech123"},
|
|
)
|
|
assert rl.status_code == 200
|
|
red_headers = {"Authorization": f"Bearer {rl.json()['access_token']}"}
|
|
client.cookies.clear()
|
|
|
|
first = client.post(
|
|
f"/api/v1/tests/{test_id}/start-execution",
|
|
headers=red_headers,
|
|
)
|
|
assert first.status_code == 200
|
|
|
|
client.cookies.clear()
|
|
second = client.post(
|
|
f"/api/v1/tests/{test_id}/start-execution",
|
|
headers=red_headers,
|
|
)
|
|
assert second.status_code == 400
|
|
body = second.json()
|
|
assert body.get("code") == "INVALID_TRANSITION"
|
|
assert "detail" in body
|