scriptgini 1.5.2__tar.gz → 1.5.4__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.5.2 → scriptgini-1.5.4}/PKG-INFO +2 -2
- {scriptgini-1.5.2 → scriptgini-1.5.4}/README.md +1 -1
- scriptgini-1.5.4/app/__init__.py +3 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/routers/analytics.py +14 -1
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/routers/api_key.py +2 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/routers/bulk_jobs.py +13 -1
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/routers/projects.py +38 -16
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/routers/scripts.py +62 -6
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/routers/test_cases.py +39 -5
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/services/auth_dependencies.py +63 -19
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/services/rbac.py +37 -2
- {scriptgini-1.5.2 → scriptgini-1.5.4}/pyproject.toml +2 -1
- {scriptgini-1.5.2 → scriptgini-1.5.4}/scriptgini.egg-info/PKG-INFO +2 -2
- {scriptgini-1.5.2 → scriptgini-1.5.4}/tests/test_auth.py +55 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/tests/test_infra_services_coverage.py +26 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/tests/test_sprint2_rbac.py +87 -1
- {scriptgini-1.5.2 → scriptgini-1.5.4}/tests/test_sprint3_execution.py +2 -3
- {scriptgini-1.5.2 → scriptgini-1.5.4}/tests/test_sprint5_reporting_analytics.py +28 -21
- {scriptgini-1.5.2 → scriptgini-1.5.4}/tests/test_sprint6_coverage_lifecycle.py +0 -5
- scriptgini-1.5.2/app/__init__.py +0 -3
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/agents/__init__.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/agents/prompts.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/agents/script_gini_agent.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/cache.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/celery_app.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/config.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/database.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/llm/__init__.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/llm/provider.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/main.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/models/__init__.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/models/api_key.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/models/bulk_job.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/models/execution_job.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/models/generated_script.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/models/membership.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/models/organization.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/models/project.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/models/script_revision.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/models/script_run.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/models/test_case.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/models/user.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/routers/__init__.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/routers/auth.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/routers/demo.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/routers/execution.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/routers/organizations.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/routers/reports.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/schemas/__init__.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/schemas/analytics.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/schemas/api_key.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/schemas/auth.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/schemas/bulk_job.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/schemas/execution.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/schemas/generated_script.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/schemas/membership.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/schemas/organization.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/schemas/project.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/schemas/reports.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/schemas/script_revision.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/schemas/test_case.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/services/api_key.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/services/auth.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/services/git_export.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/app/tasks.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/scriptgini.egg-info/SOURCES.txt +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/scriptgini.egg-info/dependency_links.txt +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/scriptgini.egg-info/top_level.txt +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/setup.cfg +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/tests/test_api.py +0 -0
- {scriptgini-1.5.2 → scriptgini-1.5.4}/tests/test_coverage.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scriptgini
|
|
3
|
-
Version: 1.5.
|
|
3
|
+
Version: 1.5.4
|
|
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.5.
|
|
19
|
+
Current release: v1.5.4 (Sprint 6 patch - RBAC enforcement hardening with 100% passing tests and 100% statement coverage)
|
|
20
20
|
|
|
21
21
|
---
|
|
22
22
|
|
|
@@ -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.
|
|
5
|
+
Current release: v1.5.4 (Sprint 6 patch - RBAC enforcement hardening with 100% passing tests and 100% statement coverage)
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -20,16 +20,23 @@ from app.schemas.analytics import (
|
|
|
20
20
|
TrendPointResponse,
|
|
21
21
|
TrendsResponse,
|
|
22
22
|
)
|
|
23
|
+
from app.services import rbac as rbac_service
|
|
24
|
+
from app.services.auth_dependencies import require_optional_auth_with_scopes
|
|
23
25
|
|
|
24
26
|
router = APIRouter(prefix="/projects/{project_id}/analytics", tags=["Run Analytics"])
|
|
25
27
|
insights_router = APIRouter(prefix="/analytics", tags=["Run Analytics"])
|
|
26
28
|
|
|
27
29
|
|
|
28
30
|
@router.get("/runs", response_model=RunAnalyticsResponse)
|
|
29
|
-
def get_run_analytics(
|
|
31
|
+
def get_run_analytics(
|
|
32
|
+
project_id: int,
|
|
33
|
+
current_user=Depends(require_optional_auth_with_scopes({"analytics:read"})),
|
|
34
|
+
db: Session = Depends(get_db),
|
|
35
|
+
):
|
|
30
36
|
project = db.query(Project).filter(Project.id == project_id).first()
|
|
31
37
|
if not project:
|
|
32
38
|
raise HTTPException(status_code=404, detail="Project not found")
|
|
39
|
+
rbac_service.ensure_project_read_access(db, project_id, getattr(current_user, "id", None))
|
|
33
40
|
|
|
34
41
|
aggregate = (
|
|
35
42
|
db.query(
|
|
@@ -98,12 +105,14 @@ def get_trends(
|
|
|
98
105
|
project_id: int | None = None,
|
|
99
106
|
start_date: date | None = None,
|
|
100
107
|
end_date: date | None = None,
|
|
108
|
+
current_user=Depends(require_optional_auth_with_scopes({"analytics:read"})),
|
|
101
109
|
db: Session = Depends(get_db),
|
|
102
110
|
):
|
|
103
111
|
if project_id is not None:
|
|
104
112
|
project = db.query(Project).filter(Project.id == project_id).first()
|
|
105
113
|
if not project:
|
|
106
114
|
raise HTTPException(status_code=404, detail="Project not found")
|
|
115
|
+
rbac_service.ensure_project_read_access(db, project_id, getattr(current_user, "id", None))
|
|
107
116
|
|
|
108
117
|
start_dt, end_dt = _resolve_datetime_range(start_date, end_date)
|
|
109
118
|
bucket_expr = func.date(ScriptRun.created_at)
|
|
@@ -148,12 +157,14 @@ def get_flakiness(
|
|
|
148
157
|
start_date: date | None = None,
|
|
149
158
|
end_date: date | None = None,
|
|
150
159
|
min_runs: int = 3,
|
|
160
|
+
current_user=Depends(require_optional_auth_with_scopes({"analytics:read"})),
|
|
151
161
|
db: Session = Depends(get_db),
|
|
152
162
|
):
|
|
153
163
|
if project_id is not None:
|
|
154
164
|
project = db.query(Project).filter(Project.id == project_id).first()
|
|
155
165
|
if not project:
|
|
156
166
|
raise HTTPException(status_code=404, detail="Project not found")
|
|
167
|
+
rbac_service.ensure_project_read_access(db, project_id, getattr(current_user, "id", None))
|
|
157
168
|
|
|
158
169
|
min_runs = max(1, min_runs)
|
|
159
170
|
start_dt, end_dt = _resolve_datetime_range(start_date, end_date)
|
|
@@ -205,6 +216,7 @@ def get_flakiness(
|
|
|
205
216
|
@insights_router.get("/coverage", response_model=CoverageResponse)
|
|
206
217
|
def get_coverage(
|
|
207
218
|
project_id: int | None = None,
|
|
219
|
+
current_user=Depends(require_optional_auth_with_scopes({"analytics:read"})),
|
|
208
220
|
db: Session = Depends(get_db),
|
|
209
221
|
):
|
|
210
222
|
"""Map test cases to scripted/executed/passed state, grouped by project module (derived from test case title prefix)."""
|
|
@@ -212,6 +224,7 @@ def get_coverage(
|
|
|
212
224
|
project = db.query(Project).filter(Project.id == project_id).first()
|
|
213
225
|
if not project:
|
|
214
226
|
raise HTTPException(status_code=404, detail="Project not found")
|
|
227
|
+
rbac_service.ensure_project_read_access(db, project_id, getattr(current_user, "id", None))
|
|
215
228
|
|
|
216
229
|
tc_query = db.query(TestCase)
|
|
217
230
|
if project_id is not None:
|
|
@@ -118,6 +118,7 @@ def update_api_key(
|
|
|
118
118
|
|
|
119
119
|
# Check that the key belongs to the current user
|
|
120
120
|
if existing_key.user_id != current_user.id:
|
|
121
|
+
_audit_api_key_event("api_key_revoked", current_user.id, existing_key.id, existing_key.prefix, "forbidden")
|
|
121
122
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized to update this API key")
|
|
122
123
|
|
|
123
124
|
api_key = api_key_service.update_api_key(
|
|
@@ -141,6 +142,7 @@ def delete_api_key(
|
|
|
141
142
|
existing_key = api_key_service.get_api_key_by_id(db, key_id)
|
|
142
143
|
|
|
143
144
|
if not existing_key:
|
|
145
|
+
_audit_api_key_event("api_key_revoked", current_user.id, key_id, None, "not_found")
|
|
144
146
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="API key not found")
|
|
145
147
|
|
|
146
148
|
# Check that the key belongs to the current user
|
|
@@ -19,6 +19,8 @@ from app.routers.scripts import (
|
|
|
19
19
|
_get_project_or_404,
|
|
20
20
|
_run_python_script,
|
|
21
21
|
)
|
|
22
|
+
from app.services import rbac as rbac_service
|
|
23
|
+
from app.services.auth_dependencies import require_optional_auth_with_scopes
|
|
22
24
|
|
|
23
25
|
router = APIRouter(prefix="/projects/{project_id}/scripts", tags=["Bulk Jobs"])
|
|
24
26
|
logger = logging.getLogger(__name__)
|
|
@@ -219,9 +221,11 @@ def _run_bulk_execution(job_id: int, request: BulkRunRequest):
|
|
|
219
221
|
def bulk_generate_scripts(
|
|
220
222
|
project_id: int,
|
|
221
223
|
payload: BulkGenerateRequest,
|
|
224
|
+
current_user=Depends(require_optional_auth_with_scopes({"bulk-jobs:write"})),
|
|
222
225
|
db: Session = Depends(get_db),
|
|
223
226
|
):
|
|
224
227
|
_get_project_or_404(project_id, db)
|
|
228
|
+
rbac_service.ensure_project_manager_access(db, project_id, getattr(current_user, "id", None))
|
|
225
229
|
test_cases = _resolve_test_cases(project_id, payload.test_case_ids, db)
|
|
226
230
|
if not test_cases:
|
|
227
231
|
raise HTTPException(status_code=400, detail="No test cases found for bulk generation")
|
|
@@ -253,9 +257,11 @@ def bulk_generate_scripts(
|
|
|
253
257
|
def bulk_run_scripts(
|
|
254
258
|
project_id: int,
|
|
255
259
|
payload: BulkRunRequest,
|
|
260
|
+
current_user=Depends(require_optional_auth_with_scopes({"bulk-jobs:write"})),
|
|
256
261
|
db: Session = Depends(get_db),
|
|
257
262
|
):
|
|
258
263
|
_get_project_or_404(project_id, db)
|
|
264
|
+
rbac_service.ensure_project_manager_access(db, project_id, getattr(current_user, "id", None))
|
|
259
265
|
test_cases = _resolve_test_cases(project_id, payload.test_case_ids, db)
|
|
260
266
|
if not test_cases:
|
|
261
267
|
raise HTTPException(status_code=400, detail="No test cases found for bulk run")
|
|
@@ -284,7 +290,13 @@ def bulk_run_scripts(
|
|
|
284
290
|
|
|
285
291
|
|
|
286
292
|
@router.get("/bulk-jobs/{job_id}", response_model=BulkJobResponse)
|
|
287
|
-
def get_bulk_job(
|
|
293
|
+
def get_bulk_job(
|
|
294
|
+
project_id: int,
|
|
295
|
+
job_id: int,
|
|
296
|
+
current_user=Depends(require_optional_auth_with_scopes({"bulk-jobs:read"})),
|
|
297
|
+
db: Session = Depends(get_db),
|
|
298
|
+
):
|
|
288
299
|
_get_project_or_404(project_id, db)
|
|
300
|
+
rbac_service.ensure_project_read_access(db, project_id, getattr(current_user, "id", None))
|
|
289
301
|
job = _get_bulk_job_or_404(project_id, job_id, db)
|
|
290
302
|
return _serialize_bulk_job(job, db)
|
|
@@ -6,27 +6,36 @@ from app.models.project import Project
|
|
|
6
6
|
from app.schemas.membership import ProjectMemberResponse, ProjectMemberUpsertRequest
|
|
7
7
|
from app.schemas.project import ProjectCreate, ProjectUpdate, ProjectResponse
|
|
8
8
|
from app.services import rbac as rbac_service
|
|
9
|
-
from app.services.auth_dependencies import require_auth_with_scopes
|
|
9
|
+
from app.services.auth_dependencies import require_auth_with_scopes, require_optional_auth_with_scopes
|
|
10
10
|
|
|
11
11
|
router = APIRouter(prefix="/projects", tags=["Projects"])
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
def _ensure_project_manager(db: Session, project_id: int, user_id: int) -> None:
|
|
15
|
-
|
|
16
|
-
if not existing_members:
|
|
17
|
-
return
|
|
18
|
-
|
|
19
|
-
membership = rbac_service.get_user_project_membership(db, project_id, user_id)
|
|
20
|
-
if membership is None or membership.role not in rbac_service.MANAGER_ROLES:
|
|
21
|
-
raise HTTPException(status_code=403, detail="Insufficient project role")
|
|
15
|
+
rbac_service.ensure_project_manager_access(db, project_id, user_id)
|
|
22
16
|
|
|
23
17
|
|
|
24
18
|
@router.post("/", response_model=ProjectResponse, status_code=status.HTTP_201_CREATED)
|
|
25
|
-
def create_project(
|
|
19
|
+
def create_project(
|
|
20
|
+
payload: ProjectCreate,
|
|
21
|
+
current_user=Depends(require_optional_auth_with_scopes({"projects:write"})),
|
|
22
|
+
db: Session = Depends(get_db),
|
|
23
|
+
):
|
|
26
24
|
project = Project(**payload.model_dump())
|
|
27
25
|
db.add(project)
|
|
28
26
|
db.commit()
|
|
29
27
|
db.refresh(project)
|
|
28
|
+
|
|
29
|
+
# Auto-bootstrap project owner membership for authenticated creators.
|
|
30
|
+
if current_user is not None:
|
|
31
|
+
rbac_service.upsert_project_member(
|
|
32
|
+
db=db,
|
|
33
|
+
project_id=project.id,
|
|
34
|
+
user_id=current_user.id,
|
|
35
|
+
role=rbac_service.Role.owner,
|
|
36
|
+
is_active=True,
|
|
37
|
+
)
|
|
38
|
+
|
|
30
39
|
return project
|
|
31
40
|
|
|
32
41
|
|
|
@@ -36,18 +45,29 @@ def list_projects(skip: int = 0, limit: int = 50, db: Session = Depends(get_db))
|
|
|
36
45
|
|
|
37
46
|
|
|
38
47
|
@router.get("/{project_id}", response_model=ProjectResponse)
|
|
39
|
-
def get_project(
|
|
48
|
+
def get_project(
|
|
49
|
+
project_id: int,
|
|
50
|
+
current_user=Depends(require_optional_auth_with_scopes({"projects:read"})),
|
|
51
|
+
db: Session = Depends(get_db),
|
|
52
|
+
):
|
|
40
53
|
project = db.query(Project).filter(Project.id == project_id).first()
|
|
41
54
|
if not project:
|
|
42
55
|
raise HTTPException(status_code=404, detail="Project not found")
|
|
56
|
+
rbac_service.ensure_project_read_access(db, project_id, getattr(current_user, "id", None))
|
|
43
57
|
return project
|
|
44
58
|
|
|
45
59
|
|
|
46
60
|
@router.patch("/{project_id}", response_model=ProjectResponse)
|
|
47
|
-
def update_project(
|
|
61
|
+
def update_project(
|
|
62
|
+
project_id: int,
|
|
63
|
+
payload: ProjectUpdate,
|
|
64
|
+
current_user=Depends(require_optional_auth_with_scopes({"projects:write"})),
|
|
65
|
+
db: Session = Depends(get_db),
|
|
66
|
+
):
|
|
48
67
|
project = db.query(Project).filter(Project.id == project_id).first()
|
|
49
68
|
if not project:
|
|
50
69
|
raise HTTPException(status_code=404, detail="Project not found")
|
|
70
|
+
rbac_service.ensure_project_manager_access(db, project_id, getattr(current_user, "id", None))
|
|
51
71
|
for field, value in payload.model_dump(exclude_none=True).items():
|
|
52
72
|
setattr(project, field, value)
|
|
53
73
|
db.commit()
|
|
@@ -56,10 +76,15 @@ def update_project(project_id: int, payload: ProjectUpdate, db: Session = Depend
|
|
|
56
76
|
|
|
57
77
|
|
|
58
78
|
@router.delete("/{project_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
59
|
-
def delete_project(
|
|
79
|
+
def delete_project(
|
|
80
|
+
project_id: int,
|
|
81
|
+
current_user=Depends(require_optional_auth_with_scopes({"projects:write"})),
|
|
82
|
+
db: Session = Depends(get_db),
|
|
83
|
+
):
|
|
60
84
|
project = db.query(Project).filter(Project.id == project_id).first()
|
|
61
85
|
if not project:
|
|
62
86
|
raise HTTPException(status_code=404, detail="Project not found")
|
|
87
|
+
rbac_service.ensure_project_manager_access(db, project_id, getattr(current_user, "id", None))
|
|
63
88
|
db.delete(project)
|
|
64
89
|
db.commit()
|
|
65
90
|
|
|
@@ -70,10 +95,7 @@ def list_project_members(
|
|
|
70
95
|
current_user=Depends(require_auth_with_scopes({"members:read"})),
|
|
71
96
|
db: Session = Depends(get_db),
|
|
72
97
|
):
|
|
73
|
-
rbac_service.
|
|
74
|
-
membership = rbac_service.get_user_project_membership(db, project_id, current_user.id)
|
|
75
|
-
if membership is None or membership.role not in rbac_service.READ_ROLES:
|
|
76
|
-
raise HTTPException(status_code=403, detail="Insufficient project role")
|
|
98
|
+
rbac_service.ensure_project_read_access(db, project_id, current_user.id)
|
|
77
99
|
return rbac_service.list_project_members(db, project_id)
|
|
78
100
|
|
|
79
101
|
|
|
@@ -33,6 +33,8 @@ from app.models.script_revision import ScriptRevision, RevisionChangeType
|
|
|
33
33
|
from app.agents.script_gini_agent import run_agent
|
|
34
34
|
from app.llm.provider import LLMProvider, get_llm_diagnostics
|
|
35
35
|
from app.services.git_export import export_generated_script
|
|
36
|
+
from app.services import rbac as rbac_service
|
|
37
|
+
from app.services.auth_dependencies import require_optional_auth_with_scopes
|
|
36
38
|
from app.tasks import process_script_generation_job
|
|
37
39
|
|
|
38
40
|
logger = logging.getLogger(__name__)
|
|
@@ -174,8 +176,15 @@ def _provider_readiness(provider: LLMProvider) -> tuple[bool, str]:
|
|
|
174
176
|
|
|
175
177
|
|
|
176
178
|
@router.get("/providers/{provider}/ready")
|
|
177
|
-
def provider_ready(
|
|
179
|
+
def provider_ready(
|
|
180
|
+
project_id: int,
|
|
181
|
+
tc_id: int,
|
|
182
|
+
provider: LLMProvider,
|
|
183
|
+
current_user=Depends(require_optional_auth_with_scopes({"scripts:read"})),
|
|
184
|
+
db: Session = Depends(get_db),
|
|
185
|
+
):
|
|
178
186
|
_get_project_or_404(project_id, db)
|
|
187
|
+
rbac_service.ensure_project_read_access(db, project_id, getattr(current_user, "id", None))
|
|
179
188
|
_get_tc_or_404(project_id, tc_id, db)
|
|
180
189
|
ok, detail = _provider_readiness(provider)
|
|
181
190
|
return {"ready": ok, "detail": detail}
|
|
@@ -464,10 +473,12 @@ def generate_script(
|
|
|
464
473
|
project_id: int,
|
|
465
474
|
tc_id: int,
|
|
466
475
|
payload: GenerateScriptRequest,
|
|
476
|
+
current_user=Depends(require_optional_auth_with_scopes({"scripts:write"})),
|
|
467
477
|
db: Session = Depends(get_db),
|
|
468
478
|
):
|
|
469
479
|
"""Kick off async script generation. Poll GET /scripts/{id} for the result."""
|
|
470
480
|
project = _get_project_or_404(project_id, db)
|
|
481
|
+
rbac_service.ensure_project_manager_access(db, project_id, getattr(current_user, "id", None))
|
|
471
482
|
_get_tc_or_404(project_id, tc_id, db)
|
|
472
483
|
|
|
473
484
|
framework = (payload.framework or project.default_framework).value
|
|
@@ -506,9 +517,11 @@ def list_scripts(
|
|
|
506
517
|
tc_id: int,
|
|
507
518
|
skip: int = 0,
|
|
508
519
|
limit: int = 50,
|
|
520
|
+
current_user=Depends(require_optional_auth_with_scopes({"scripts:read"})),
|
|
509
521
|
db: Session = Depends(get_db),
|
|
510
522
|
):
|
|
511
523
|
_get_project_or_404(project_id, db)
|
|
524
|
+
rbac_service.ensure_project_read_access(db, project_id, getattr(current_user, "id", None))
|
|
512
525
|
_get_tc_or_404(project_id, tc_id, db)
|
|
513
526
|
return (
|
|
514
527
|
db.query(GeneratedScript)
|
|
@@ -521,15 +534,29 @@ def list_scripts(
|
|
|
521
534
|
|
|
522
535
|
|
|
523
536
|
@router.get("/{script_id}", response_model=GeneratedScriptResponse)
|
|
524
|
-
def get_script(
|
|
537
|
+
def get_script(
|
|
538
|
+
project_id: int,
|
|
539
|
+
tc_id: int,
|
|
540
|
+
script_id: int,
|
|
541
|
+
current_user=Depends(require_optional_auth_with_scopes({"scripts:read"})),
|
|
542
|
+
db: Session = Depends(get_db),
|
|
543
|
+
):
|
|
525
544
|
_get_project_or_404(project_id, db)
|
|
545
|
+
rbac_service.ensure_project_read_access(db, project_id, getattr(current_user, "id", None))
|
|
526
546
|
_get_tc_or_404(project_id, tc_id, db)
|
|
527
547
|
return _get_script_or_404(project_id, tc_id, script_id, db)
|
|
528
548
|
|
|
529
549
|
|
|
530
550
|
@router.get("/{script_id}/runs", response_model=list[ScriptRunResponse])
|
|
531
|
-
def list_script_runs(
|
|
551
|
+
def list_script_runs(
|
|
552
|
+
project_id: int,
|
|
553
|
+
tc_id: int,
|
|
554
|
+
script_id: int,
|
|
555
|
+
current_user=Depends(require_optional_auth_with_scopes({"scripts:read"})),
|
|
556
|
+
db: Session = Depends(get_db),
|
|
557
|
+
):
|
|
532
558
|
_get_project_or_404(project_id, db)
|
|
559
|
+
rbac_service.ensure_project_read_access(db, project_id, getattr(current_user, "id", None))
|
|
533
560
|
_get_tc_or_404(project_id, tc_id, db)
|
|
534
561
|
_get_script_or_404(project_id, tc_id, script_id, db)
|
|
535
562
|
return (
|
|
@@ -541,8 +568,15 @@ def list_script_runs(project_id: int, tc_id: int, script_id: int, db: Session =
|
|
|
541
568
|
|
|
542
569
|
|
|
543
570
|
@router.post("/{script_id}/run", response_model=ScriptRunResponse)
|
|
544
|
-
def run_script(
|
|
571
|
+
def run_script(
|
|
572
|
+
project_id: int,
|
|
573
|
+
tc_id: int,
|
|
574
|
+
script_id: int,
|
|
575
|
+
current_user=Depends(require_optional_auth_with_scopes({"scripts:write"})),
|
|
576
|
+
db: Session = Depends(get_db),
|
|
577
|
+
):
|
|
545
578
|
project = _get_project_or_404(project_id, db)
|
|
579
|
+
rbac_service.ensure_project_manager_access(db, project_id, getattr(current_user, "id", None))
|
|
546
580
|
_get_tc_or_404(project_id, tc_id, db)
|
|
547
581
|
script = _get_script_or_404(project_id, tc_id, script_id, db)
|
|
548
582
|
|
|
@@ -582,8 +616,15 @@ def run_script(project_id: int, tc_id: int, script_id: int, db: Session = Depend
|
|
|
582
616
|
|
|
583
617
|
|
|
584
618
|
@router.post("/{script_id}/github-export")
|
|
585
|
-
def github_export_script(
|
|
619
|
+
def github_export_script(
|
|
620
|
+
project_id: int,
|
|
621
|
+
tc_id: int,
|
|
622
|
+
script_id: int,
|
|
623
|
+
current_user=Depends(require_optional_auth_with_scopes({"scripts:write"})),
|
|
624
|
+
db: Session = Depends(get_db),
|
|
625
|
+
):
|
|
586
626
|
project = _get_project_or_404(project_id, db)
|
|
627
|
+
rbac_service.ensure_project_manager_access(db, project_id, getattr(current_user, "id", None))
|
|
587
628
|
tc = _get_tc_or_404(project_id, tc_id, db)
|
|
588
629
|
script = _get_script_or_404(project_id, tc_id, script_id, db)
|
|
589
630
|
|
|
@@ -619,8 +660,15 @@ def github_export_script(project_id: int, tc_id: int, script_id: int, db: Sessio
|
|
|
619
660
|
|
|
620
661
|
|
|
621
662
|
@router.delete("/{script_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
622
|
-
def delete_script(
|
|
663
|
+
def delete_script(
|
|
664
|
+
project_id: int,
|
|
665
|
+
tc_id: int,
|
|
666
|
+
script_id: int,
|
|
667
|
+
current_user=Depends(require_optional_auth_with_scopes({"scripts:write"})),
|
|
668
|
+
db: Session = Depends(get_db),
|
|
669
|
+
):
|
|
623
670
|
_get_project_or_404(project_id, db)
|
|
671
|
+
rbac_service.ensure_project_manager_access(db, project_id, getattr(current_user, "id", None))
|
|
624
672
|
script = _get_script_or_404(project_id, tc_id, script_id, db)
|
|
625
673
|
db.delete(script)
|
|
626
674
|
db.commit()
|
|
@@ -666,10 +714,12 @@ def refactor_script(
|
|
|
666
714
|
tc_id: int,
|
|
667
715
|
script_id: int,
|
|
668
716
|
payload: RefactorScriptRequest,
|
|
717
|
+
current_user=Depends(require_optional_auth_with_scopes({"scripts:write"})),
|
|
669
718
|
db: Session = Depends(get_db),
|
|
670
719
|
):
|
|
671
720
|
"""Submit an error log; the LLM heals the script and saves a new revision."""
|
|
672
721
|
project = _get_project_or_404(project_id, db)
|
|
722
|
+
rbac_service.ensure_project_manager_access(db, project_id, getattr(current_user, "id", None))
|
|
673
723
|
tc = _get_tc_or_404(project_id, tc_id, db)
|
|
674
724
|
script = _get_script_or_404(project_id, tc_id, script_id, db)
|
|
675
725
|
|
|
@@ -727,9 +777,11 @@ def get_version_history(
|
|
|
727
777
|
project_id: int,
|
|
728
778
|
tc_id: int,
|
|
729
779
|
script_id: int,
|
|
780
|
+
current_user=Depends(require_optional_auth_with_scopes({"scripts:read"})),
|
|
730
781
|
db: Session = Depends(get_db),
|
|
731
782
|
):
|
|
732
783
|
_get_project_or_404(project_id, db)
|
|
784
|
+
rbac_service.ensure_project_read_access(db, project_id, getattr(current_user, "id", None))
|
|
733
785
|
_get_tc_or_404(project_id, tc_id, db)
|
|
734
786
|
_get_script_or_404(project_id, tc_id, script_id, db)
|
|
735
787
|
|
|
@@ -752,10 +804,12 @@ def get_revision_diff(
|
|
|
752
804
|
tc_id: int,
|
|
753
805
|
script_id: int,
|
|
754
806
|
revision_id: int,
|
|
807
|
+
current_user=Depends(require_optional_auth_with_scopes({"scripts:read"})),
|
|
755
808
|
db: Session = Depends(get_db),
|
|
756
809
|
):
|
|
757
810
|
"""Return a unified diff between the revision just before `revision_id` and `revision_id`."""
|
|
758
811
|
_get_project_or_404(project_id, db)
|
|
812
|
+
rbac_service.ensure_project_read_access(db, project_id, getattr(current_user, "id", None))
|
|
759
813
|
_get_tc_or_404(project_id, tc_id, db)
|
|
760
814
|
_get_script_or_404(project_id, tc_id, script_id, db)
|
|
761
815
|
|
|
@@ -803,10 +857,12 @@ def rollback_script(
|
|
|
803
857
|
tc_id: int,
|
|
804
858
|
script_id: int,
|
|
805
859
|
revision_id: int,
|
|
860
|
+
current_user=Depends(require_optional_auth_with_scopes({"scripts:write"})),
|
|
806
861
|
db: Session = Depends(get_db),
|
|
807
862
|
):
|
|
808
863
|
"""Restore a script to a previously saved revision and record a rolled_back snapshot."""
|
|
809
864
|
_get_project_or_404(project_id, db)
|
|
865
|
+
rbac_service.ensure_project_manager_access(db, project_id, getattr(current_user, "id", None))
|
|
810
866
|
_get_tc_or_404(project_id, tc_id, db)
|
|
811
867
|
script = _get_script_or_404(project_id, tc_id, script_id, db)
|
|
812
868
|
|
|
@@ -5,6 +5,8 @@ from app.database import get_db
|
|
|
5
5
|
from app.models.project import Project
|
|
6
6
|
from app.models.test_case import TestCase
|
|
7
7
|
from app.schemas.test_case import TestCaseCreate, TestCaseUpdate, TestCaseResponse
|
|
8
|
+
from app.services import rbac as rbac_service
|
|
9
|
+
from app.services.auth_dependencies import require_optional_auth_with_scopes
|
|
8
10
|
|
|
9
11
|
router = APIRouter(prefix="/projects/{project_id}/test-cases", tags=["Test Cases"])
|
|
10
12
|
|
|
@@ -17,8 +19,14 @@ def _get_project_or_404(project_id: int, db: Session) -> Project:
|
|
|
17
19
|
|
|
18
20
|
|
|
19
21
|
@router.post("/", response_model=TestCaseResponse, status_code=status.HTTP_201_CREATED)
|
|
20
|
-
def create_test_case(
|
|
22
|
+
def create_test_case(
|
|
23
|
+
project_id: int,
|
|
24
|
+
payload: TestCaseCreate,
|
|
25
|
+
current_user=Depends(require_optional_auth_with_scopes({"test-cases:write"})),
|
|
26
|
+
db: Session = Depends(get_db),
|
|
27
|
+
):
|
|
21
28
|
_get_project_or_404(project_id, db)
|
|
29
|
+
rbac_service.ensure_project_manager_access(db, project_id, getattr(current_user, "id", None))
|
|
22
30
|
tc = TestCase(project_id=project_id, **payload.model_dump())
|
|
23
31
|
db.add(tc)
|
|
24
32
|
db.commit()
|
|
@@ -27,14 +35,27 @@ def create_test_case(project_id: int, payload: TestCaseCreate, db: Session = Dep
|
|
|
27
35
|
|
|
28
36
|
|
|
29
37
|
@router.get("/", response_model=list[TestCaseResponse])
|
|
30
|
-
def list_test_cases(
|
|
38
|
+
def list_test_cases(
|
|
39
|
+
project_id: int,
|
|
40
|
+
skip: int = 0,
|
|
41
|
+
limit: int = 100,
|
|
42
|
+
current_user=Depends(require_optional_auth_with_scopes({"test-cases:read"})),
|
|
43
|
+
db: Session = Depends(get_db),
|
|
44
|
+
):
|
|
31
45
|
_get_project_or_404(project_id, db)
|
|
46
|
+
rbac_service.ensure_project_read_access(db, project_id, getattr(current_user, "id", None))
|
|
32
47
|
return db.query(TestCase).filter(TestCase.project_id == project_id).offset(skip).limit(limit).all()
|
|
33
48
|
|
|
34
49
|
|
|
35
50
|
@router.get("/{tc_id}", response_model=TestCaseResponse)
|
|
36
|
-
def get_test_case(
|
|
51
|
+
def get_test_case(
|
|
52
|
+
project_id: int,
|
|
53
|
+
tc_id: int,
|
|
54
|
+
current_user=Depends(require_optional_auth_with_scopes({"test-cases:read"})),
|
|
55
|
+
db: Session = Depends(get_db),
|
|
56
|
+
):
|
|
37
57
|
_get_project_or_404(project_id, db)
|
|
58
|
+
rbac_service.ensure_project_read_access(db, project_id, getattr(current_user, "id", None))
|
|
38
59
|
tc = db.query(TestCase).filter(TestCase.project_id == project_id, TestCase.id == tc_id).first()
|
|
39
60
|
if not tc:
|
|
40
61
|
raise HTTPException(status_code=404, detail="Test case not found")
|
|
@@ -42,8 +63,15 @@ def get_test_case(project_id: int, tc_id: int, db: Session = Depends(get_db)):
|
|
|
42
63
|
|
|
43
64
|
|
|
44
65
|
@router.patch("/{tc_id}", response_model=TestCaseResponse)
|
|
45
|
-
def update_test_case(
|
|
66
|
+
def update_test_case(
|
|
67
|
+
project_id: int,
|
|
68
|
+
tc_id: int,
|
|
69
|
+
payload: TestCaseUpdate,
|
|
70
|
+
current_user=Depends(require_optional_auth_with_scopes({"test-cases:write"})),
|
|
71
|
+
db: Session = Depends(get_db),
|
|
72
|
+
):
|
|
46
73
|
_get_project_or_404(project_id, db)
|
|
74
|
+
rbac_service.ensure_project_manager_access(db, project_id, getattr(current_user, "id", None))
|
|
47
75
|
tc = db.query(TestCase).filter(TestCase.project_id == project_id, TestCase.id == tc_id).first()
|
|
48
76
|
if not tc:
|
|
49
77
|
raise HTTPException(status_code=404, detail="Test case not found")
|
|
@@ -55,8 +83,14 @@ def update_test_case(project_id: int, tc_id: int, payload: TestCaseUpdate, db: S
|
|
|
55
83
|
|
|
56
84
|
|
|
57
85
|
@router.delete("/{tc_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
58
|
-
def delete_test_case(
|
|
86
|
+
def delete_test_case(
|
|
87
|
+
project_id: int,
|
|
88
|
+
tc_id: int,
|
|
89
|
+
current_user=Depends(require_optional_auth_with_scopes({"test-cases:write"})),
|
|
90
|
+
db: Session = Depends(get_db),
|
|
91
|
+
):
|
|
59
92
|
_get_project_or_404(project_id, db)
|
|
93
|
+
rbac_service.ensure_project_manager_access(db, project_id, getattr(current_user, "id", None))
|
|
60
94
|
tc = db.query(TestCase).filter(TestCase.project_id == project_id, TestCase.id == tc_id).first()
|
|
61
95
|
if not tc:
|
|
62
96
|
raise HTTPException(status_code=404, detail="Test case not found")
|
|
@@ -9,6 +9,28 @@ from app.services import auth as auth_service
|
|
|
9
9
|
from app.services import api_key as api_key_service
|
|
10
10
|
|
|
11
11
|
security = HTTPBearer()
|
|
12
|
+
optional_security = HTTPBearer(auto_error=False)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _resolve_user_or_api_key_identity(token: str, db: Session):
|
|
16
|
+
# First try to verify as JWT token.
|
|
17
|
+
token_data = auth_service.verify_token(token, token_type="access")
|
|
18
|
+
if token_data:
|
|
19
|
+
user = auth_service.get_user_by_id(db, token_data.user_id)
|
|
20
|
+
if user and user.is_active:
|
|
21
|
+
return user
|
|
22
|
+
|
|
23
|
+
# Then try key_id:key_secret API key format.
|
|
24
|
+
if ":" in token:
|
|
25
|
+
key_id, key_secret = token.split(":", 1)
|
|
26
|
+
api_key = api_key_service.verify_api_key(db, key_id, key_secret)
|
|
27
|
+
if api_key:
|
|
28
|
+
user = auth_service.get_user_by_id(db, api_key.user_id)
|
|
29
|
+
if user and user.is_active:
|
|
30
|
+
user._api_key = api_key
|
|
31
|
+
return user
|
|
32
|
+
|
|
33
|
+
return None
|
|
12
34
|
|
|
13
35
|
|
|
14
36
|
def get_current_user(
|
|
@@ -66,25 +88,9 @@ def get_current_user_or_api_key(
|
|
|
66
88
|
return {"user_id": current_user.id}
|
|
67
89
|
"""
|
|
68
90
|
token = credentials.credentials
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if token_data:
|
|
73
|
-
user = auth_service.get_user_by_id(db, token_data.user_id)
|
|
74
|
-
if user and user.is_active:
|
|
75
|
-
return user
|
|
76
|
-
|
|
77
|
-
# Try to parse as API key (key_id:key_secret format)
|
|
78
|
-
if ":" in token:
|
|
79
|
-
key_id, key_secret = token.split(":", 1)
|
|
80
|
-
api_key = api_key_service.verify_api_key(db, key_id, key_secret)
|
|
81
|
-
if api_key:
|
|
82
|
-
# Return the user associated with the API key
|
|
83
|
-
user = auth_service.get_user_by_id(db, api_key.user_id)
|
|
84
|
-
if user and user.is_active:
|
|
85
|
-
# Attach api_key to user for scope checking in routes
|
|
86
|
-
user._api_key = api_key
|
|
87
|
-
return user
|
|
91
|
+
user = _resolve_user_or_api_key_identity(token, db)
|
|
92
|
+
if user is not None:
|
|
93
|
+
return user
|
|
88
94
|
|
|
89
95
|
raise HTTPException(
|
|
90
96
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
@@ -93,6 +99,24 @@ def get_current_user_or_api_key(
|
|
|
93
99
|
)
|
|
94
100
|
|
|
95
101
|
|
|
102
|
+
def get_optional_current_user_or_api_key(
|
|
103
|
+
credentials: HTTPAuthorizationCredentials | None = Depends(optional_security),
|
|
104
|
+
db: Session = Depends(get_db),
|
|
105
|
+
):
|
|
106
|
+
if credentials is None:
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
user = _resolve_user_or_api_key_identity(credentials.credentials, db)
|
|
110
|
+
if user is not None:
|
|
111
|
+
return user
|
|
112
|
+
|
|
113
|
+
raise HTTPException(
|
|
114
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
115
|
+
detail="Invalid or expired credentials",
|
|
116
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
96
120
|
def require_auth_with_scopes(required_scopes: set[str] | None = None) -> Callable:
|
|
97
121
|
required_scopes = required_scopes or set()
|
|
98
122
|
|
|
@@ -108,3 +132,23 @@ def require_auth_with_scopes(required_scopes: set[str] | None = None) -> Callabl
|
|
|
108
132
|
return current_user
|
|
109
133
|
|
|
110
134
|
return _dependency
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def require_optional_auth_with_scopes(required_scopes: set[str] | None = None) -> Callable:
|
|
138
|
+
required_scopes = required_scopes or set()
|
|
139
|
+
|
|
140
|
+
def _dependency(current_user=Depends(get_optional_current_user_or_api_key)):
|
|
141
|
+
if current_user is None:
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
api_key = getattr(current_user, "_api_key", None)
|
|
145
|
+
if api_key is not None:
|
|
146
|
+
granted_scopes = set(api_key.scopes or [])
|
|
147
|
+
if not required_scopes.issubset(granted_scopes):
|
|
148
|
+
raise HTTPException(
|
|
149
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
150
|
+
detail="API key does not have required scopes",
|
|
151
|
+
)
|
|
152
|
+
return current_user
|
|
153
|
+
|
|
154
|
+
return _dependency
|