Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 29 additions & 19 deletions api/controllers/console/workspace/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,21 +179,35 @@ def _serialize_account(account) -> dict[str, Any]:
return AccountResponse.model_validate(account, from_attributes=True).model_dump(mode="json")


def _to_timestamp(value: datetime | int | None) -> int | None:
if isinstance(value, datetime):
return int(value.timestamp())
return value
def _to_timestamp(value: datetime | int | str | None) -> int | None:
if value is None:
return None
if isinstance(value, int):
return value

dt: datetime
if isinstance(value, str):
normalized = value.replace("Z", "+00:00") if value.endswith("Z") else value
dt = datetime.fromisoformat(normalized)
else:
dt = value
if dt.tzinfo is None:
dt = dt.replace(tzinfo=pytz.utc)
else:
dt = dt.astimezone(pytz.utc)
return int(dt.timestamp())


class AccountIntegrateResponse(ResponseModel):
id: str | None = None
provider: str
created_at: int | None = None
is_bound: bool
link: str | None = None

@field_validator("created_at", mode="before")
@classmethod
def _normalize_created_at(cls, value: datetime | int | None) -> int | None:
def _normalize_created_at(cls, value: datetime | int | str | None) -> int | None:
return _to_timestamp(value)


Expand All @@ -213,14 +227,14 @@ class EducationStatusResponse(ResponseModel):

@field_validator("expire_at", mode="before")
@classmethod
def _normalize_expire_at(cls, value: datetime | int | None) -> int | None:
def _normalize_expire_at(cls, value: datetime | int | str | None) -> int | None:
return _to_timestamp(value)


class EducationAutocompleteResponse(ResponseModel):
data: list[str] = Field(default_factory=list)
curr_page: int | None = None
has_next: bool | None = None
data: list[str]
curr_page: int
has_next: bool


register_schema_models(
Expand Down Expand Up @@ -436,9 +450,7 @@ def get(self):
}
)

return AccountIntegrateListResponse(
data=[AccountIntegrateResponse.model_validate(item) for item in integrate_data]
).model_dump(mode="json")
return AccountIntegrateListResponse.model_validate({"data": integrate_data}).model_dump(mode="json")


@console_ns.route("/account/delete/verify")
Expand Down Expand Up @@ -500,7 +512,7 @@ def get(self):
account, _ = current_account_with_tenant()

return EducationVerifyResponse.model_validate(
BillingService.EducationIdentity.verify(account.id, account.email) or {}
BillingService.EducationIdentity.verify(account.id, account.email)
).model_dump(mode="json")


Expand Down Expand Up @@ -529,11 +541,9 @@ def post(self):
def get(self):
account, _ = current_account_with_tenant()

res = BillingService.EducationIdentity.status(account.id) or {}
# convert expire_at to UTC timestamp from isoformat
if res and "expire_at" in res:
res["expire_at"] = datetime.fromisoformat(res["expire_at"]).astimezone(pytz.utc)
return EducationStatusResponse.model_validate(res).model_dump(mode="json")
return EducationStatusResponse.model_validate(BillingService.EducationIdentity.status(account.id)).model_dump(
mode="json"
)


@console_ns.route("/account/education/autocomplete")
Expand All @@ -550,7 +560,7 @@ def get(self):
args = EducationAutocompleteQuery.model_validate(payload)

return EducationAutocompleteResponse.model_validate(
BillingService.EducationIdentity.autocomplete(args.keywords, args.page, args.limit) or {}
BillingService.EducationIdentity.autocomplete(args.keywords, args.page, args.limit)
).model_dump(mode="json")


Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from datetime import UTC, datetime
from unittest.mock import MagicMock, PropertyMock, patch

import pytest
Expand All @@ -14,6 +15,7 @@
AccountDeleteVerifyApi,
AccountInitApi,
AccountIntegrateApi,
AccountIntegrateResponse,
AccountInterfaceLanguageApi,
AccountInterfaceThemeApi,
AccountNameApi,
Expand All @@ -23,6 +25,7 @@
ChangeEmailCheckApi,
ChangeEmailResetApi,
CheckEmailUnique,
EducationStatusResponse,
)
from controllers.console.workspace.error import (
AccountAlreadyInitedError,
Expand Down Expand Up @@ -207,6 +210,19 @@ def test_get_integrates(self, app):
assert "data" in result
assert len(result["data"]) == 2

def test_integrate_response_accepts_z_timestamp(self):
response = AccountIntegrateResponse.model_validate(
{"provider": "google", "created_at": "2024-01-01T00:00:00Z", "is_bound": True}
)

assert response.created_at == 1704067200

def test_education_status_accepts_naive_iso_timestamp(self):
response = EducationStatusResponse.model_validate({"expire_at": "2024-01-01T00:00:00"})

expected = int(datetime(2024, 1, 1, 0, 0, 0, tzinfo=UTC).timestamp())
assert response.expire_at == expected


class TestAccountDeleteApi:
def test_delete_verify_success(self, app):
Expand Down
Loading