scriptgini 1.4.0__tar.gz → 1.5.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {scriptgini-1.4.0 → scriptgini-1.5.2}/PKG-INFO +6 -4
- {scriptgini-1.4.0 → scriptgini-1.5.2}/README.md +5 -3
- scriptgini-1.5.2/app/__init__.py +3 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/main.py +46 -0
- scriptgini-1.5.2/app/models/script_revision.py +37 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/routers/analytics.py +96 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/routers/api_key.py +28 -1
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/routers/scripts.py +289 -15
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/schemas/analytics.py +31 -0
- scriptgini-1.5.2/app/schemas/script_revision.py +48 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/pyproject.toml +1 -1
- {scriptgini-1.4.0 → scriptgini-1.5.2}/scriptgini.egg-info/PKG-INFO +6 -4
- {scriptgini-1.4.0 → scriptgini-1.5.2}/scriptgini.egg-info/SOURCES.txt +4 -1
- {scriptgini-1.4.0 → scriptgini-1.5.2}/tests/test_auth.py +88 -19
- {scriptgini-1.4.0 → scriptgini-1.5.2}/tests/test_coverage.py +18 -3
- {scriptgini-1.4.0 → scriptgini-1.5.2}/tests/test_sprint3_execution.py +1 -1
- scriptgini-1.5.2/tests/test_sprint6_coverage_lifecycle.py +502 -0
- scriptgini-1.4.0/app/__init__.py +0 -3
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/agents/__init__.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/agents/prompts.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/agents/script_gini_agent.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/cache.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/celery_app.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/config.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/database.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/llm/__init__.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/llm/provider.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/models/__init__.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/models/api_key.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/models/bulk_job.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/models/execution_job.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/models/generated_script.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/models/membership.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/models/organization.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/models/project.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/models/script_run.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/models/test_case.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/models/user.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/routers/__init__.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/routers/auth.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/routers/bulk_jobs.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/routers/demo.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/routers/execution.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/routers/organizations.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/routers/projects.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/routers/reports.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/routers/test_cases.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/schemas/__init__.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/schemas/api_key.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/schemas/auth.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/schemas/bulk_job.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/schemas/execution.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/schemas/generated_script.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/schemas/membership.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/schemas/organization.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/schemas/project.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/schemas/reports.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/schemas/test_case.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/services/api_key.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/services/auth.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/services/auth_dependencies.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/services/git_export.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/services/rbac.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/app/tasks.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/scriptgini.egg-info/dependency_links.txt +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/scriptgini.egg-info/top_level.txt +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/setup.cfg +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/tests/test_api.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/tests/test_infra_services_coverage.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/tests/test_sprint2_rbac.py +0 -0
- {scriptgini-1.4.0 → scriptgini-1.5.2}/tests/test_sprint5_reporting_analytics.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scriptgini
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.2
|
|
4
4
|
Summary: Agentic AI system that converts functional test cases into automation test scripts.
|
|
5
5
|
Author: ScriptGini Team
|
|
6
6
|
License: Proprietary
|
|
@@ -16,7 +16,7 @@ Description-Content-Type: text/markdown
|
|
|
16
16
|
|
|
17
17
|
> **Enterprise-grade Agentic AI system that converts functional test cases into high-quality, review-ready automation test scripts.**
|
|
18
18
|
|
|
19
|
-
Current release: v1.
|
|
19
|
+
Current release: v1.5.2 (Sprint 6 hardening + API key audit/revocation patch)
|
|
20
20
|
|
|
21
21
|
---
|
|
22
22
|
|
|
@@ -39,8 +39,10 @@ ScriptGini is an AI-powered test automation engine built for Quality Engineering
|
|
|
39
39
|
- **Hardened execution sandbox** — Script runs use isolated Python mode, static safety validation, and restricted environment variables
|
|
40
40
|
- **Bulk job orchestration** — Project-level bulk generate and bulk run with pollable job status
|
|
41
41
|
- **Run analytics dashboard** — Project-level pass/fail/timeout metrics and recent failure feed
|
|
42
|
+
- **Coverage analytics** — Module-level coverage and execution visibility for test cases
|
|
42
43
|
- **Richer test case intake** — Import `.txt`, `.md`, `.json`, `.csv`, `.feature`, `.yml/.yaml`, and `.xlsx`
|
|
43
44
|
- **Import preview mapping** — Preview parsed scenarios in the UI before creating a project workspace
|
|
45
|
+
- **Script lifecycle tracking** — Refactor, version history, diff, and rollback support
|
|
44
46
|
- **REST API** — FastAPI with auto-generated Swagger UI
|
|
45
47
|
- **Alembic migrations** — Safe, versioned schema management over SQLite
|
|
46
48
|
|
|
@@ -442,9 +444,9 @@ The project follows an **enterprise-grade development roadmap** with 6 sprints c
|
|
|
442
444
|
| **Sprint 1** | IAM Core | 30-36pts | 🟡 Core delivered (auth hardening pending) |
|
|
443
445
|
| **Sprint 2** | RBAC + Multi-Tenancy | 32-38pts | 🟡 Core delivered (RBAC hardening pending) |
|
|
444
446
|
| **Sprint 3** | Durable Execution | 34-40pts | ✅ Completed (Redis + Celery queue foundation) |
|
|
445
|
-
| **Sprint 4** | Security & Hardening | 30-36pts |
|
|
447
|
+
| **Sprint 4** | Security & Hardening | 30-36pts | ✅ Hardening increment completed (isolation + negative tests + audit controls) |
|
|
446
448
|
| **Sprint 5** | Reporting & Analytics | 28-34pts | ✅ Completed (Reports APIs, trends/flakiness, retention cleanup) |
|
|
447
|
-
| **Sprint 6** |
|
|
449
|
+
| **Sprint 6** | Coverage Analytics, Script Lifecycle, and DX | 24-30pts | ✅ Completed (coverage analytics, refactor/version history/diff/rollback, 100% quality gate) |
|
|
448
450
|
|
|
449
451
|
---
|
|
450
452
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> **Enterprise-grade Agentic AI system that converts functional test cases into high-quality, review-ready automation test scripts.**
|
|
4
4
|
|
|
5
|
-
Current release: v1.
|
|
5
|
+
Current release: v1.5.2 (Sprint 6 hardening + API key audit/revocation patch)
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -25,8 +25,10 @@ ScriptGini is an AI-powered test automation engine built for Quality Engineering
|
|
|
25
25
|
- **Hardened execution sandbox** — Script runs use isolated Python mode, static safety validation, and restricted environment variables
|
|
26
26
|
- **Bulk job orchestration** — Project-level bulk generate and bulk run with pollable job status
|
|
27
27
|
- **Run analytics dashboard** — Project-level pass/fail/timeout metrics and recent failure feed
|
|
28
|
+
- **Coverage analytics** — Module-level coverage and execution visibility for test cases
|
|
28
29
|
- **Richer test case intake** — Import `.txt`, `.md`, `.json`, `.csv`, `.feature`, `.yml/.yaml`, and `.xlsx`
|
|
29
30
|
- **Import preview mapping** — Preview parsed scenarios in the UI before creating a project workspace
|
|
31
|
+
- **Script lifecycle tracking** — Refactor, version history, diff, and rollback support
|
|
30
32
|
- **REST API** — FastAPI with auto-generated Swagger UI
|
|
31
33
|
- **Alembic migrations** — Safe, versioned schema management over SQLite
|
|
32
34
|
|
|
@@ -428,9 +430,9 @@ The project follows an **enterprise-grade development roadmap** with 6 sprints c
|
|
|
428
430
|
| **Sprint 1** | IAM Core | 30-36pts | 🟡 Core delivered (auth hardening pending) |
|
|
429
431
|
| **Sprint 2** | RBAC + Multi-Tenancy | 32-38pts | 🟡 Core delivered (RBAC hardening pending) |
|
|
430
432
|
| **Sprint 3** | Durable Execution | 34-40pts | ✅ Completed (Redis + Celery queue foundation) |
|
|
431
|
-
| **Sprint 4** | Security & Hardening | 30-36pts |
|
|
433
|
+
| **Sprint 4** | Security & Hardening | 30-36pts | ✅ Hardening increment completed (isolation + negative tests + audit controls) |
|
|
432
434
|
| **Sprint 5** | Reporting & Analytics | 28-34pts | ✅ Completed (Reports APIs, trends/flakiness, retention cleanup) |
|
|
433
|
-
| **Sprint 6** |
|
|
435
|
+
| **Sprint 6** | Coverage Analytics, Script Lifecycle, and DX | 24-30pts | ✅ Completed (coverage analytics, refactor/version history/diff/rollback, 100% quality gate) |
|
|
434
436
|
|
|
435
437
|
---
|
|
436
438
|
|
|
@@ -10,6 +10,8 @@ from fastapi.responses import JSONResponse
|
|
|
10
10
|
from fastapi.responses import FileResponse
|
|
11
11
|
from fastapi.middleware.cors import CORSMiddleware
|
|
12
12
|
from fastapi.staticfiles import StaticFiles
|
|
13
|
+
from fastapi.exceptions import RequestValidationError
|
|
14
|
+
from starlette.exceptions import HTTPException as StarletteHTTPException
|
|
13
15
|
|
|
14
16
|
from app import __version__
|
|
15
17
|
from app.config import settings
|
|
@@ -114,6 +116,50 @@ app.add_middleware(
|
|
|
114
116
|
)
|
|
115
117
|
|
|
116
118
|
|
|
119
|
+
def _error_envelope(request: Request, code: str, message: str, context: dict | None = None, status_code: int = 500) -> JSONResponse:
|
|
120
|
+
request_id = getattr(request.state, "request_id", None) or request.headers.get("X-Request-ID", "unknown")
|
|
121
|
+
return JSONResponse(
|
|
122
|
+
status_code=status_code,
|
|
123
|
+
content={
|
|
124
|
+
"error": {
|
|
125
|
+
"code": code,
|
|
126
|
+
"message": message,
|
|
127
|
+
"context": context or {},
|
|
128
|
+
"request_id": request_id,
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
headers={"X-Request-ID": request_id},
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@app.exception_handler(StarletteHTTPException)
|
|
136
|
+
async def http_exception_handler(request: Request, exc: StarletteHTTPException) -> JSONResponse:
|
|
137
|
+
code = {
|
|
138
|
+
400: "bad_request",
|
|
139
|
+
401: "unauthorized",
|
|
140
|
+
403: "forbidden",
|
|
141
|
+
404: "not_found",
|
|
142
|
+
409: "conflict",
|
|
143
|
+
422: "unprocessable_entity",
|
|
144
|
+
429: "rate_limit_exceeded",
|
|
145
|
+
500: "internal_server_error",
|
|
146
|
+
503: "service_unavailable",
|
|
147
|
+
504: "gateway_timeout",
|
|
148
|
+
}.get(exc.status_code, f"http_{exc.status_code}")
|
|
149
|
+
return _error_envelope(request, code, str(exc.detail), status_code=exc.status_code)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@app.exception_handler(RequestValidationError)
|
|
153
|
+
async def validation_exception_handler(request: Request, exc: RequestValidationError) -> JSONResponse:
|
|
154
|
+
return _error_envelope(
|
|
155
|
+
request,
|
|
156
|
+
"validation_error",
|
|
157
|
+
"Request validation failed",
|
|
158
|
+
context={"errors": exc.errors()},
|
|
159
|
+
status_code=422,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
117
163
|
@app.middleware("http")
|
|
118
164
|
async def apply_security_controls(request: Request, call_next):
|
|
119
165
|
request_id = request.headers.get("X-Request-ID") or str(uuid4())
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
|
|
4
|
+
from sqlalchemy import String, Text, DateTime, ForeignKey, Enum as SAEnum, Integer
|
|
5
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
6
|
+
|
|
7
|
+
from app.database import Base
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RevisionChangeType(str, enum.Enum):
|
|
11
|
+
generated = "generated"
|
|
12
|
+
refactored = "refactored"
|
|
13
|
+
rolled_back = "rolled_back"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ScriptRevision(Base):
|
|
17
|
+
__tablename__ = "script_revisions"
|
|
18
|
+
|
|
19
|
+
id: Mapped[int] = mapped_column(primary_key=True, index=True)
|
|
20
|
+
script_id: Mapped[int] = mapped_column(
|
|
21
|
+
ForeignKey("generated_scripts.id", ondelete="CASCADE"), nullable=False, index=True
|
|
22
|
+
)
|
|
23
|
+
project_id: Mapped[int] = mapped_column(
|
|
24
|
+
ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True
|
|
25
|
+
)
|
|
26
|
+
test_case_id: Mapped[int] = mapped_column(
|
|
27
|
+
ForeignKey("test_cases.id", ondelete="CASCADE"), nullable=False, index=True
|
|
28
|
+
)
|
|
29
|
+
revision_number: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
|
30
|
+
script_content: Mapped[str] = mapped_column(Text, nullable=False)
|
|
31
|
+
change_type: Mapped[RevisionChangeType] = mapped_column(
|
|
32
|
+
SAEnum(RevisionChangeType), default=RevisionChangeType.generated, nullable=False
|
|
33
|
+
)
|
|
34
|
+
change_summary: Mapped[str | None] = mapped_column(String(1024), nullable=True)
|
|
35
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
36
|
+
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
|
37
|
+
)
|
|
@@ -8,7 +8,11 @@ from app.database import get_db
|
|
|
8
8
|
from app.models.project import Project
|
|
9
9
|
from app.models.script_run import ScriptRun, ScriptRunStatus
|
|
10
10
|
from app.models.test_case import TestCase
|
|
11
|
+
from app.models.generated_script import GeneratedScript
|
|
11
12
|
from app.schemas.analytics import (
|
|
13
|
+
CoverageModuleItem,
|
|
14
|
+
CoverageResponse,
|
|
15
|
+
CoverageTestCaseItem,
|
|
12
16
|
FlakinessItemResponse,
|
|
13
17
|
FlakinessResponse,
|
|
14
18
|
RecentFailureResponse,
|
|
@@ -196,3 +200,95 @@ def get_flakiness(
|
|
|
196
200
|
min_runs=min_runs,
|
|
197
201
|
items=items,
|
|
198
202
|
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@insights_router.get("/coverage", response_model=CoverageResponse)
|
|
206
|
+
def get_coverage(
|
|
207
|
+
project_id: int | None = None,
|
|
208
|
+
db: Session = Depends(get_db),
|
|
209
|
+
):
|
|
210
|
+
"""Map test cases to scripted/executed/passed state, grouped by project module (derived from test case title prefix)."""
|
|
211
|
+
if project_id is not None:
|
|
212
|
+
project = db.query(Project).filter(Project.id == project_id).first()
|
|
213
|
+
if not project:
|
|
214
|
+
raise HTTPException(status_code=404, detail="Project not found")
|
|
215
|
+
|
|
216
|
+
tc_query = db.query(TestCase)
|
|
217
|
+
if project_id is not None:
|
|
218
|
+
tc_query = tc_query.filter(TestCase.project_id == project_id)
|
|
219
|
+
test_cases = tc_query.all()
|
|
220
|
+
|
|
221
|
+
# Collect latest script per test_case_id
|
|
222
|
+
script_query = db.query(GeneratedScript).order_by(GeneratedScript.test_case_id, GeneratedScript.id.desc())
|
|
223
|
+
if project_id is not None:
|
|
224
|
+
script_query = script_query.filter(GeneratedScript.project_id == project_id)
|
|
225
|
+
scripted_by_tc: dict[int, GeneratedScript] = {}
|
|
226
|
+
for gs in script_query.all():
|
|
227
|
+
if gs.test_case_id not in scripted_by_tc:
|
|
228
|
+
scripted_by_tc[gs.test_case_id] = gs
|
|
229
|
+
|
|
230
|
+
# Collect latest run per test_case_id
|
|
231
|
+
run_query = db.query(ScriptRun).order_by(ScriptRun.test_case_id, ScriptRun.id.desc())
|
|
232
|
+
if project_id is not None:
|
|
233
|
+
run_query = run_query.filter(ScriptRun.project_id == project_id)
|
|
234
|
+
run_by_tc: dict[int, ScriptRun] = {}
|
|
235
|
+
for sr in run_query.all():
|
|
236
|
+
if sr.test_case_id not in run_by_tc:
|
|
237
|
+
run_by_tc[sr.test_case_id] = sr
|
|
238
|
+
|
|
239
|
+
# Group by module (first word of title or "General")
|
|
240
|
+
modules_map: dict[str, list[CoverageTestCaseItem]] = {}
|
|
241
|
+
for tc in test_cases:
|
|
242
|
+
parts = tc.title.split()
|
|
243
|
+
module = parts[0] if parts else "General"
|
|
244
|
+
|
|
245
|
+
has_script = tc.id in scripted_by_tc
|
|
246
|
+
has_run = tc.id in run_by_tc
|
|
247
|
+
last_run_success: bool | None = None
|
|
248
|
+
if has_run:
|
|
249
|
+
last_run_success = run_by_tc[tc.id].success
|
|
250
|
+
|
|
251
|
+
item = CoverageTestCaseItem(
|
|
252
|
+
test_case_id=tc.id,
|
|
253
|
+
title=tc.title,
|
|
254
|
+
format=tc.format.value,
|
|
255
|
+
has_script=has_script,
|
|
256
|
+
has_run=has_run,
|
|
257
|
+
last_run_success=last_run_success,
|
|
258
|
+
)
|
|
259
|
+
modules_map.setdefault(module, []).append(item)
|
|
260
|
+
|
|
261
|
+
module_items: list[CoverageModuleItem] = []
|
|
262
|
+
for module_name, cases in sorted(modules_map.items()):
|
|
263
|
+
total = len(cases)
|
|
264
|
+
scripted = sum(1 for c in cases if c.has_script)
|
|
265
|
+
executed = sum(1 for c in cases if c.has_run)
|
|
266
|
+
passed = sum(1 for c in cases if c.last_run_success is True)
|
|
267
|
+
module_items.append(
|
|
268
|
+
CoverageModuleItem(
|
|
269
|
+
module=module_name,
|
|
270
|
+
total_cases=total,
|
|
271
|
+
scripted_cases=scripted,
|
|
272
|
+
executed_cases=executed,
|
|
273
|
+
passed_cases=passed,
|
|
274
|
+
coverage_rate=round(scripted / total * 100, 2) if total else 0.0,
|
|
275
|
+
execution_rate=round(executed / total * 100, 2) if total else 0.0,
|
|
276
|
+
test_cases=cases,
|
|
277
|
+
)
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
total_cases = len(test_cases)
|
|
281
|
+
scripted_cases = sum(1 for tc in test_cases if tc.id in scripted_by_tc)
|
|
282
|
+
executed_cases = sum(1 for tc in test_cases if tc.id in run_by_tc)
|
|
283
|
+
passed_cases = sum(1 for tc in test_cases if run_by_tc.get(tc.id) and run_by_tc[tc.id].success)
|
|
284
|
+
|
|
285
|
+
return CoverageResponse(
|
|
286
|
+
project_id=project_id,
|
|
287
|
+
total_cases=total_cases,
|
|
288
|
+
scripted_cases=scripted_cases,
|
|
289
|
+
executed_cases=executed_cases,
|
|
290
|
+
passed_cases=passed_cases,
|
|
291
|
+
overall_coverage_rate=round(scripted_cases / total_cases * 100, 2) if total_cases else 0.0,
|
|
292
|
+
overall_execution_rate=round(executed_cases / total_cases * 100, 2) if total_cases else 0.0,
|
|
293
|
+
modules=module_items,
|
|
294
|
+
)
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
1
3
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
2
4
|
from sqlalchemy.orm import Session
|
|
3
5
|
|
|
6
|
+
from app.config import settings
|
|
4
7
|
from app.database import get_db
|
|
5
8
|
from app.services import api_key as api_key_service
|
|
6
9
|
from app.services.auth_dependencies import get_current_user
|
|
@@ -13,6 +16,26 @@ from app.schemas.api_key import (
|
|
|
13
16
|
from app.models.api_key import APIKey
|
|
14
17
|
|
|
15
18
|
router = APIRouter(prefix="/auth/api-keys", tags=["auth"])
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _audit_api_key_event(
|
|
23
|
+
event: str,
|
|
24
|
+
actor_user_id: int,
|
|
25
|
+
api_key_id: int | None,
|
|
26
|
+
api_key_prefix: str | None,
|
|
27
|
+
result: str,
|
|
28
|
+
) -> None:
|
|
29
|
+
if not settings.SECURITY_AUDIT_LOG_ENABLED:
|
|
30
|
+
return
|
|
31
|
+
logger.info(
|
|
32
|
+
"security_audit event=%s actor_user_id=%s api_key_id=%s api_key_prefix=%s result=%s",
|
|
33
|
+
event,
|
|
34
|
+
actor_user_id,
|
|
35
|
+
api_key_id,
|
|
36
|
+
api_key_prefix,
|
|
37
|
+
result,
|
|
38
|
+
)
|
|
16
39
|
|
|
17
40
|
|
|
18
41
|
@router.post("", response_model=APIKeyRevealResponse, status_code=status.HTTP_201_CREATED)
|
|
@@ -36,6 +59,7 @@ def create_api_key(
|
|
|
36
59
|
request.scopes,
|
|
37
60
|
request.expires_at,
|
|
38
61
|
)
|
|
62
|
+
_audit_api_key_event("api_key_created", current_user.id, api_key.id, api_key.prefix, "success")
|
|
39
63
|
|
|
40
64
|
return APIKeyRevealResponse(
|
|
41
65
|
id=api_key.id,
|
|
@@ -89,6 +113,7 @@ def update_api_key(
|
|
|
89
113
|
existing_key = api_key_service.get_api_key_by_id(db, key_id)
|
|
90
114
|
|
|
91
115
|
if not existing_key:
|
|
116
|
+
_audit_api_key_event("api_key_revoked", current_user.id, key_id, None, "not_found")
|
|
92
117
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="API key not found")
|
|
93
118
|
|
|
94
119
|
# Check that the key belongs to the current user
|
|
@@ -120,7 +145,9 @@ def delete_api_key(
|
|
|
120
145
|
|
|
121
146
|
# Check that the key belongs to the current user
|
|
122
147
|
if existing_key.user_id != current_user.id:
|
|
148
|
+
_audit_api_key_event("api_key_revoked", current_user.id, existing_key.id, existing_key.prefix, "forbidden")
|
|
123
149
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized to delete this API key")
|
|
124
150
|
|
|
125
|
-
api_key_service.
|
|
151
|
+
api_key_service.revoke_api_key(db, key_id)
|
|
152
|
+
_audit_api_key_event("api_key_revoked", current_user.id, existing_key.id, existing_key.prefix, "success")
|
|
126
153
|
return None
|