feat: save previous token
This commit is contained in:
parent
993d21e406
commit
6a5669a42e
|
|
@ -138,11 +138,11 @@ async def update_connection(
|
|||
raw_url = match.group(1).rstrip("/")
|
||||
updates["base_url"] = raw_url
|
||||
|
||||
# Handle token update - empty string leaves existing unchanged
|
||||
# Handle token update. The update schema normalizes blank strings to None;
|
||||
# treat both as "leave the existing token unchanged" for edit forms.
|
||||
if "token" in updates:
|
||||
raw_token = updates["token"]
|
||||
if raw_token == "":
|
||||
# Empty string - leave existing token unchanged
|
||||
if raw_token in ("", None):
|
||||
del updates["token"]
|
||||
elif raw_token is not None:
|
||||
updates["token_last_eight"] = _extract_token_last_eight(raw_token)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import timedelta
|
||||
from typing import TYPE_CHECKING
|
||||
from uuid import UUID
|
||||
|
||||
|
|
@ -11,6 +11,7 @@ from sqlalchemy import and_
|
|||
from sqlmodel import func, select
|
||||
|
||||
from app.api.deps import ORG_MEMBER_DEP, OrganizationContext
|
||||
from app.core.time import utcnow
|
||||
from app.db.session import get_session
|
||||
from app.models.board_repository_links import BoardRepositoryLink
|
||||
from app.models.forgejo_issues import ForgejoIssue
|
||||
|
|
@ -156,14 +157,14 @@ async def get_forgejo_metrics(
|
|||
closed_issues = closed_count.one_or_none() or 0
|
||||
|
||||
# 3. Closed in last 7 days
|
||||
now = datetime.now(timezone.utc)
|
||||
now = utcnow()
|
||||
seven_days_ago = now - timedelta(days=7)
|
||||
closed_7_statement = select(func.count(ForgejoIssue.id)).where(
|
||||
and_(
|
||||
ForgejoIssue.repository_id.in_(repo_ids),
|
||||
ForgejoIssue.state == "closed",
|
||||
ForgejoIssue.is_pull_request.is_(False),
|
||||
ForgejoIssue.updated_at >= seven_days_ago,
|
||||
ForgejoIssue.forgejo_closed_at >= seven_days_ago,
|
||||
)
|
||||
)
|
||||
closed_7_count = await session.exec(closed_7_statement)
|
||||
|
|
@ -176,7 +177,7 @@ async def get_forgejo_metrics(
|
|||
ForgejoIssue.repository_id.in_(repo_ids),
|
||||
ForgejoIssue.state == "closed",
|
||||
ForgejoIssue.is_pull_request.is_(False),
|
||||
ForgejoIssue.updated_at >= thirty_days_ago,
|
||||
ForgejoIssue.forgejo_closed_at >= thirty_days_ago,
|
||||
)
|
||||
)
|
||||
closed_30_count = await session.exec(closed_30_statement)
|
||||
|
|
@ -189,7 +190,7 @@ async def get_forgejo_metrics(
|
|||
ForgejoIssue.repository_id.in_(repo_ids),
|
||||
ForgejoIssue.state == "open",
|
||||
ForgejoIssue.is_pull_request.is_(False),
|
||||
ForgejoIssue.updated_at < fourteen_days_ago,
|
||||
ForgejoIssue.forgejo_updated_at < fourteen_days_ago,
|
||||
)
|
||||
)
|
||||
stale_count = await session.exec(stale_statement)
|
||||
|
|
|
|||
|
|
@ -157,3 +157,61 @@ async def test_delete_connection_removes_repositories_and_dependents() -> None:
|
|||
).first() is not None
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_connection_blank_token_keeps_existing_token() -> None:
|
||||
engine = await _make_engine()
|
||||
session_maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
organization = Organization(id=uuid4(), name="Pipeline")
|
||||
member = OrganizationMember(
|
||||
id=uuid4(),
|
||||
organization_id=organization.id,
|
||||
user_id=uuid4(),
|
||||
role="owner",
|
||||
)
|
||||
app = _build_test_app(
|
||||
session_maker,
|
||||
OrganizationContext(organization=organization, member=member),
|
||||
)
|
||||
|
||||
connection = ForgejoConnection(
|
||||
id=uuid4(),
|
||||
organization_id=organization.id,
|
||||
name="Dream Forgejo",
|
||||
base_url="https://forgejo.example.local",
|
||||
token="temp-token-12345678",
|
||||
token_last_eight="12345678",
|
||||
)
|
||||
|
||||
try:
|
||||
async with session_maker() as session:
|
||||
session.add(organization)
|
||||
session.add(connection)
|
||||
await session.commit()
|
||||
|
||||
async with AsyncClient(
|
||||
transport=ASGITransport(app=app),
|
||||
base_url="http://testserver",
|
||||
) as client:
|
||||
response = await client.patch(
|
||||
f"/api/v1/forgejo/connections/{connection.id}",
|
||||
json={"name": "Dream Forgejo Updated", "token": ""},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["name"] == "Dream Forgejo Updated"
|
||||
assert data["has_token"] is True
|
||||
assert data["token_last_eight"] == "12345678"
|
||||
|
||||
async with session_maker() as session:
|
||||
saved = (
|
||||
await session.exec(
|
||||
select(ForgejoConnection).where(col(ForgejoConnection.id) == connection.id)
|
||||
)
|
||||
).one()
|
||||
assert saved.token == "temp-token-12345678"
|
||||
assert saved.token_last_eight == "12345678"
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
|
@ -24,6 +24,7 @@ from app.models.forgejo_issues import ForgejoIssue
|
|||
from app.models.forgejo_repositories import ForgejoRepository
|
||||
from app.models.organization_members import OrganizationMember
|
||||
from app.models.organizations import Organization
|
||||
from app.core.time import utcnow
|
||||
from app.services.organizations import OrganizationContext
|
||||
|
||||
|
||||
|
|
@ -189,3 +190,88 @@ async def test_forgejo_metrics_are_scoped_to_active_organization() -> None:
|
|||
assert other_board.json()["open_issues"] == 0
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forgejo_metrics_use_remote_issue_timestamps() -> None:
|
||||
engine = await _make_engine()
|
||||
session_maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
organization = Organization(id=uuid4(), name="Pipeline")
|
||||
member = OrganizationMember(
|
||||
id=uuid4(),
|
||||
organization_id=organization.id,
|
||||
user_id=uuid4(),
|
||||
role="owner",
|
||||
)
|
||||
app = _build_test_app(
|
||||
session_maker,
|
||||
OrganizationContext(organization=organization, member=member),
|
||||
)
|
||||
connection = ForgejoConnection(
|
||||
id=uuid4(),
|
||||
organization_id=organization.id,
|
||||
name="Dream Forgejo",
|
||||
base_url="https://forgejo.example.local",
|
||||
)
|
||||
repository = ForgejoRepository(
|
||||
id=uuid4(),
|
||||
organization_id=organization.id,
|
||||
connection_id=connection.id,
|
||||
owner="openclaw",
|
||||
repo="pipeline",
|
||||
display_name="Pipeline",
|
||||
)
|
||||
now = utcnow()
|
||||
stale_open = _issue(
|
||||
organization_id=organization.id,
|
||||
repository_id=repository.id,
|
||||
issue_number=1,
|
||||
)
|
||||
stale_open.forgejo_updated_at = now - timedelta(days=20)
|
||||
stale_open.updated_at = now
|
||||
|
||||
recently_closed = _issue(
|
||||
organization_id=organization.id,
|
||||
repository_id=repository.id,
|
||||
issue_number=2,
|
||||
)
|
||||
recently_closed.state = "closed"
|
||||
recently_closed.forgejo_closed_at = now - timedelta(days=2)
|
||||
recently_closed.updated_at = now - timedelta(days=90)
|
||||
|
||||
old_closed = _issue(
|
||||
organization_id=organization.id,
|
||||
repository_id=repository.id,
|
||||
issue_number=3,
|
||||
)
|
||||
old_closed.state = "closed"
|
||||
old_closed.forgejo_closed_at = now - timedelta(days=45)
|
||||
old_closed.updated_at = now
|
||||
|
||||
try:
|
||||
async with session_maker() as session:
|
||||
session.add(organization)
|
||||
session.add(connection)
|
||||
session.add(repository)
|
||||
session.add(stale_open)
|
||||
session.add(recently_closed)
|
||||
session.add(old_closed)
|
||||
await session.commit()
|
||||
|
||||
async with AsyncClient(
|
||||
transport=ASGITransport(app=app),
|
||||
base_url="http://testserver",
|
||||
) as client:
|
||||
response = await client.get(
|
||||
f"/api/v1/forgejo/metrics?organization_id={organization.id}"
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["open_issues"] == 1
|
||||
assert data["closed_issues"] == 2
|
||||
assert data["closed_last_7_days"] == 1
|
||||
assert data["closed_last_30_days"] == 1
|
||||
assert data["stale_open_issues"] == 1
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
|
|
|||
Loading…
Reference in New Issue