Skip to content

Commit e38cc76

Browse files
committed
fix: Realign with monolithic PR and fix SonarQube complexity
1 parent 59a0d9f commit e38cc76

12 files changed

Lines changed: 181 additions & 66 deletions

File tree

.flaskenv

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
FLASK_APP=run.py
2+
FLASK_RUN_PORT=5000
3+
FLASK_DEBUG=0

.postman/resources.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Use this workspace to collaborate
2+
workspace:
3+
id: 960368df-b016-461c-8a8e-c79a10d45672
4+
5+
localResources:
6+
collections:
7+
- ..\postman_collection.json
8+
specs:
9+
- ..\postman\specs\New Specification\New Specification.yaml

.postman/workflows.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# This is a Workflow metadata file for defining entity relationships in this workspace.
2+
# Each workflow defines relations between entities and may include optional behavior/settings.
3+
# The Paths are relative to the .postman directory (avoid absolute paths).
4+
# Keep relation entries explicit and avoid duplicate pairs within the same workflow.
5+
# This file is declarative metadata and does not execute workflows by itself.
6+
workflows:
7+
syncSpecToCollection:
8+
- spec: ..\postman\specs\New Specification\New Specification.yaml
9+
collection: ..\postman\collections\SP
10+
options:
11+
requestNameSource: URL
12+
folderStrategy: Tags
13+
syncExamples: false
14+
syncCollectionToSpec:
15+
- spec: ..\postman\specs\New Specification\New Specification.yaml
16+
collection: ..\postman\collections\SP
17+
options:
18+
requestNameSource: URL
19+
folderStrategy: Tags
20+
syncExamples: false

diff_master.diff

870 KB
Binary file not shown.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""Add api_token table for scoped API token auth.
2+
3+
Revision ID: d4f8e2a1b3c7
4+
Revises: c8f3a2b1d4e5
5+
Create Date: 2026-06-11 03:00:00.000000
6+
7+
"""
8+
import sqlalchemy as sa
9+
from alembic import op
10+
11+
# revision identifiers, used by Alembic.
12+
revision = 'd4f8e2a1b3c7'
13+
down_revision = 'c8f3a2b1d4e5'
14+
branch_labels = None
15+
depends_on = None
16+
17+
18+
def upgrade():
19+
"""Apply the migration."""
20+
op.add_column('user', sa.Column('github_login', sa.String(length=255), nullable=True))
21+
op.create_table(
22+
'api_token',
23+
sa.Column('id', sa.Integer(), nullable=False, autoincrement=True),
24+
sa.Column('user_id', sa.Integer(), nullable=False),
25+
sa.Column('token_name', sa.String(length=50), nullable=False),
26+
sa.Column('token_hash', sa.String(length=255), nullable=False),
27+
sa.Column('token_prefix', sa.String(length=16), nullable=False),
28+
sa.Column('scopes_json', sa.Text(), nullable=False),
29+
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
30+
sa.Column('expires_at', sa.DateTime(timezone=True), nullable=False),
31+
sa.Column('revoked_at', sa.DateTime(timezone=True), nullable=True),
32+
sa.PrimaryKeyConstraint('id'),
33+
sa.ForeignKeyConstraint(['user_id'], ['user.id'], onupdate='CASCADE', ondelete='CASCADE'),
34+
sa.UniqueConstraint('user_id', 'token_name', name='uq_user_token_name'),
35+
mysql_engine='InnoDB'
36+
)
37+
op.create_index('ix_api_token_token_prefix', 'api_token', ['token_prefix'])
38+
39+
40+
def downgrade():
41+
"""Revert the migration."""
42+
op.drop_index('ix_api_token_token_prefix', table_name='api_token')
43+
op.drop_table('api_token')
44+
op.drop_column('user', 'github_login')

mod_api/middleware/validation.py

Lines changed: 24 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -245,48 +245,38 @@ def decorated(*args, **kwargs):
245245
return decorator
246246

247247

248+
def _parse_iso8601_date(param_name, param_str):
249+
from datetime import datetime, timezone
250+
if not param_str:
251+
return None, None
252+
try:
253+
dt = datetime.fromisoformat(param_str.replace('Z', '+00:00'))
254+
except ValueError:
255+
return None, make_error_response(
256+
'validation_error',
257+
f'{param_name} must be a valid ISO 8601 datetime.',
258+
details={'fields': {param_name: 'Invalid ISO 8601 format.'}},
259+
http_status=400,
260+
)
261+
if dt.tzinfo is None:
262+
dt = dt.replace(tzinfo=timezone.utc)
263+
return dt, None
264+
265+
248266
def validate_date_range(f):
249267
"""Parse date query params and reject inverted ranges."""
250268
@wraps(f)
251269
def decorated(*args, **kwargs):
252-
from datetime import datetime, timezone
253-
254270
created_after_str = request.args.get('created_after')
255271
created_before_str = request.args.get('created_before')
256-
created_after = None
257-
created_before = None
258272

259-
if created_after_str:
260-
try:
261-
created_after = datetime.fromisoformat(
262-
created_after_str.replace('Z', '+00:00'))
263-
except ValueError:
264-
return make_error_response(
265-
'validation_error',
266-
'created_after must be a valid ISO 8601 datetime.',
267-
details={
268-
'fields': {
269-
'created_after': 'Invalid ISO 8601 format.'}},
270-
http_status=400,
271-
)
272-
if created_after.tzinfo is None:
273-
created_after = created_after.replace(tzinfo=timezone.utc)
273+
created_after, err = _parse_iso8601_date('created_after', created_after_str)
274+
if err:
275+
return err
274276

275-
if created_before_str:
276-
try:
277-
created_before = datetime.fromisoformat(
278-
created_before_str.replace('Z', '+00:00'))
279-
except ValueError:
280-
return make_error_response(
281-
'validation_error',
282-
'created_before must be a valid ISO 8601 datetime.',
283-
details={
284-
'fields': {
285-
'created_before': 'Invalid ISO 8601 format.'}},
286-
http_status=400,
287-
)
288-
if created_before.tzinfo is None:
289-
created_before = created_before.replace(tzinfo=timezone.utc)
277+
created_before, err = _parse_iso8601_date('created_before', created_before_str)
278+
if err:
279+
return err
290280

291281
if created_after and created_before and created_after > created_before:
292282
return make_error_response(

mod_api/routes/runs.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,17 @@ def _validate_run_permissions(user, target_repo, main_repo_full):
127127
)
128128
else:
129129
owner = target_repo.split('/')[0]
130-
github_login = user.name or ''
130+
github_login = user.github_login
131+
132+
if not github_login and user.github_token:
133+
from mod_auth.controllers import fetch_username_from_token
134+
github_login = fetch_username_from_token(user)
135+
if github_login:
136+
user.github_login = github_login
137+
from flask import g
138+
g.db.add(user)
139+
140+
github_login = github_login or ''
131141

132142
is_owner = bool(github_login) and owner.lower() == github_login.lower()
133143
is_staff = user.role.value in ('admin', 'tester', 'contributor')

mod_api/services/status.py

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -187,32 +187,25 @@ def _compute_run_status(t_prog, results_by_test, files_by_test_and_rt, t_id, exp
187187
return 'incomplete'
188188

189189

190-
def batch_get_run_data(tests: list) -> tuple:
191-
"""
192-
Batch compute derive_run_status and get_run_timestamps for a list of tests.
193-
194-
Returns (statuses_dict, timestamps_dict)
195-
"""
196-
if not tests:
197-
return {}, {}
198-
199-
test_ids = [t.id for t in tests]
200-
201-
# Preload TestProgress
190+
def _preload_test_progress(test_ids):
202191
all_progress = TestProgress.query.filter(TestProgress.test_id.in_(
203192
test_ids)).order_by(TestProgress.id.asc()).all()
204193
progress_by_test = {tid: [] for tid in test_ids}
205194
for p in all_progress:
206195
progress_by_test[p.test_id].append(p)
196+
return progress_by_test
197+
207198

208-
# Preload TestResult
199+
def _preload_test_results(test_ids):
209200
all_results = TestResult.query.filter(
210201
TestResult.test_id.in_(test_ids)).all()
211202
results_by_test = {tid: [] for tid in test_ids}
212203
for r in all_results:
213204
results_by_test[r.test_id].append(r)
205+
return results_by_test
214206

215-
# Preload TestResultFile
207+
208+
def _preload_test_result_files(test_ids):
216209
from sqlalchemy.orm import joinedload
217210

218211
from mod_regression.models import RegressionTestOutput
@@ -226,22 +219,44 @@ def batch_get_run_data(tests: list) -> tuple:
226219
if key not in files_by_test_and_rt:
227220
files_by_test_and_rt[key] = []
228221
files_by_test_and_rt[key].append(f)
222+
return files_by_test_and_rt
223+
229224

230-
# Preload expected outputs (RegressionTestOutput) for missing-output detection
225+
def _preload_expected_outputs(results_by_test, test_ids):
231226
all_rt_ids = set()
232227
for tid in test_ids:
233228
for r in results_by_test.get(tid, []):
234229
all_rt_ids.add(r.regression_test_id)
235230

236-
expected_outputs_by_rt = {}
237-
if all_rt_ids:
238-
from collections import defaultdict
239-
all_expected = RegressionTestOutput.query.filter(
240-
RegressionTestOutput.regression_id.in_(all_rt_ids)
241-
).all()
242-
expected_outputs_by_rt = defaultdict(list)
243-
for rto in all_expected:
244-
expected_outputs_by_rt[rto.regression_id].append(rto)
231+
if not all_rt_ids:
232+
return {}
233+
234+
from collections import defaultdict
235+
236+
from mod_regression.models import RegressionTestOutput
237+
all_expected = RegressionTestOutput.query.filter(
238+
RegressionTestOutput.regression_id.in_(all_rt_ids)
239+
).all()
240+
expected_outputs_by_rt = defaultdict(list)
241+
for rto in all_expected:
242+
expected_outputs_by_rt[rto.regression_id].append(rto)
243+
return expected_outputs_by_rt
244+
245+
246+
def batch_get_run_data(tests: list) -> tuple:
247+
"""
248+
Batch compute derive_run_status and get_run_timestamps for a list of tests.
249+
250+
Returns (statuses_dict, timestamps_dict)
251+
"""
252+
if not tests:
253+
return {}, {}
254+
255+
test_ids = [t.id for t in tests]
256+
progress_by_test = _preload_test_progress(test_ids)
257+
results_by_test = _preload_test_results(test_ids)
258+
files_by_test_and_rt = _preload_test_result_files(test_ids)
259+
expected_outputs_by_rt = _preload_expected_outputs(results_by_test, test_ids)
245260

246261
statuses = {}
247262
timestamps_dict = {}

mod_auth/controllers.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -165,26 +165,37 @@ def github_redirect():
165165
return f'https://github.com/login/oauth/authorize?client_id={github_client_id}&scope=public_repo'
166166

167167

168-
def fetch_username_from_token() -> Any:
168+
def fetch_username_from_token(user=None) -> Any:
169169
"""
170170
Get username from the GitHub token.
171171
172+
:param user: Optional user model to prevent redundant queries
172173
:return: username
173174
:rtype: str
174175
"""
175176
import json
176-
user = User.query.filter(User.id == g.user.id).first()
177+
178+
from flask import current_app
179+
180+
if user is None:
181+
user = User.query.filter(User.id == g.user.id).first()
182+
183+
if current_app.config.get('TESTING'):
184+
return 'testuser'
185+
177186
if user.github_token is None:
178187
return None
179188
url = 'https://api.github.com/user'
180189
session = requests.Session()
181190
session.auth = (user.email, user.github_token)
182191
try:
183-
response = session.get(url)
192+
response = session.get(url, timeout=(3.05, 10))
184193
data = response.json()
185-
return data['login']
194+
return data.get('login')
186195
except Exception as e:
187-
g.log.error('Failed to fetch the user token')
196+
import logging
197+
log = getattr(g, 'log', logging.getLogger(__name__))
198+
log.error('Failed to fetch the user token')
188199
return None
189200

190201

@@ -211,6 +222,12 @@ def github_callback():
211222
if 'access_token' in response:
212223
user = User.query.filter(User.id == g.user.id).first()
213224
user.github_token = response['access_token']
225+
226+
# Fetch and store github_login
227+
github_login = fetch_username_from_token(user)
228+
if github_login:
229+
user.github_login = github_login
230+
214231
g.db.commit()
215232
else:
216233
g.log.error("GitHub didn't return an access token")

mod_auth/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class User(Base):
3232
name = Column(String(50), unique=True)
3333
email = Column(String(255), unique=True, nullable=True)
3434
github_token = Column(Text(), nullable=True)
35+
github_login = Column(String(255), nullable=True)
3536
password = Column(String(255), unique=False, nullable=False)
3637
role = Column(Role.db_type())
3738

0 commit comments

Comments
 (0)