Compare commits

..

7 Commits

22 changed files with 387 additions and 431 deletions

View File

@ -0,0 +1,282 @@
name: Dependabot Alert to Feishu
on:
schedule:
- cron: "0 2 * * *"
workflow_dispatch:
permissions:
contents: read
security-events: read
jobs:
notify-feishu:
runs-on: ubuntu-latest
steps:
- name: Validate webhook secret
env:
FEISHU_WEBHOOK: ${{ secrets.FEISHU_WEBHOOK }}
run: |
set -euo pipefail
if [ -z "${FEISHU_WEBHOOK:-}" ]; then
echo "FEISHU_WEBHOOK secret is not configured."
exit 1
fi
- name: Notify Feishu from event or API polling
env:
FEISHU_WEBHOOK: ${{ secrets.FEISHU_WEBHOOK }}
GITHUB_TOKEN: ${{ github.token }}
REPOSITORY: ${{ github.repository }}
run: |
set -euo pipefail
MAX_ITEMS="${MAX_ITEMS:-10}"
SUMMARY_MAX_LEN="${SUMMARY_MAX_LEN:-140}"
build_payload() {
local alerts_json="$1"
local alert_count critical_count high_count header_template
local summary_line summary_line_2 summary_line_3 generated_at
local stats_elements visible_items hidden_count table_rows_data
alert_count="$(echo "${alerts_json}" | jq 'length')"
critical_count="$(echo "${alerts_json}" | jq '[.[] | select((.security_advisory.severity // "") == "critical")] | length')"
high_count="$(echo "${alerts_json}" | jq '[.[] | select((.security_advisory.severity // "") == "high")] | length')"
header_template="orange"
if [ "${critical_count}" -gt 0 ]; then
header_template="red"
fi
summary_line="🚨 嗨,这里是您的 EE&CE 企业级🐮🐴在线打工播报员~"
summary_line_2="当前系统雷达已锁定一批**高风险依赖告警**,建议优先处理,不然它们可能比 KPI 先“爆炸”💥"
summary_line_3=$'📌 已为您智能筛选:\n仅展示 **未分配负责人的 High / Critical 告警**\n也就是说——没人背锅但锅已经烧起来了🔥\n\n请尽快认领处理拯救系统于水火之中 🙏'
generated_at="$(date -u '+%Y-%m-%d %H:%M:%S UTC')"
stats_elements="$(jq -n \
--argjson total "${alert_count}" \
--argjson critical "${critical_count}" \
--argjson high "${high_count}" '
[
{
tag: "column_set",
flex_mode: "trisect",
horizontal_spacing: "small",
columns: [
{
tag: "column",
width: "weighted",
weight: 1,
padding: "8px",
background_style: "grey",
elements: [{tag: "markdown", content: "**待处理告警**\n" + ($total|tostring)}]
},
{
tag: "column",
width: "weighted",
weight: 1,
padding: "8px",
background_style: "grey",
elements: [{tag: "markdown", content: "**🔴 严重风险**\n" + ($critical|tostring)}]
},
{
tag: "column",
width: "weighted",
weight: 1,
padding: "8px",
background_style: "grey",
elements: [{tag: "markdown", content: "**🟠 高风险**\n" + ($high|tostring)}]
}
]
}
]')"
if [ "${alert_count}" -eq 0 ]; then
jq -n \
--arg title "Dependabot Security Alerts" \
--arg subtitle "${REPOSITORY}" \
--arg summary "🚨 嗨,这里是您的 EE&CE 企业级🐮🐴在线打工播报员~" \
--arg summary2 "当前没有待处理的高风险依赖告警,继续保持,今天可以安心下班。" \
--arg summary3 $'📌 已为您智能筛选:\n仅展示 **未分配负责人的 High / Critical 告警**\n当前结果为空系统暂时平稳' \
--arg generatedAt "${generated_at}" '
{
msg_type: "interactive",
card: {
schema: "2.0",
config: {wide_screen_mode: true},
header: {
template: "green",
title: {tag: "plain_text", content: $title},
subtitle: {tag: "plain_text", content: $subtitle}
},
body: {
elements: [
{tag: "markdown", content: $summary},
{tag: "markdown", content: $summary2},
{tag: "markdown", content: $summary3},
{
tag: "column_set",
flex_mode: "trisect",
horizontal_spacing: "small",
columns: [
{
tag: "column",
width: "weighted",
weight: 1,
padding: "8px",
background_style: "grey",
elements: [{tag: "markdown", content: "**待处理告警**\n0"}]
},
{
tag: "column",
width: "weighted",
weight: 1,
padding: "8px",
background_style: "grey",
elements: [{tag: "markdown", content: "**🔴 严重风险**\n0"}]
},
{
tag: "column",
width: "weighted",
weight: 1,
padding: "8px",
background_style: "grey",
elements: [{tag: "markdown", content: "**🟠 高风险**\n0"}]
}
]
},
{
tag: "div",
text: {
tag: "plain_text",
content: ("通知时间:" + $generatedAt),
text_color: "grey",
text_align: "right"
}
}
]
}
}
}'
return 0
fi
visible_items="$(echo "${alerts_json}" | jq --argjson max "${MAX_ITEMS}" '.[:$max]')"
hidden_count="$(echo "${alerts_json}" | jq --argjson max "${MAX_ITEMS}" 'if length > $max then length - $max else 0 end')"
table_rows_data="$(echo "${visible_items}" | jq -c \
--argjson maxLen "${SUMMARY_MAX_LEN}" '
map(
. as $a |
($a.number // "unknown") as $number |
($a.security_advisory.severity // "unknown") as $severity |
($a.dependency.package.name // "unknown") as $package |
($a.security_advisory.summary // "N/A") as $summary |
($a.html_url // "") as $url |
(
if ($summary | length) > $maxLen
then ($summary[0:$maxLen] + "...")
else $summary
end
) as $summaryShort |
{
level: (if $severity == "critical" then "🔴 critical" else "🟠 high" end),
alert_id: ("#" + ($number|tostring)),
package: ("`" + $package + "`"),
details: ($summaryShort + "\n[View alert](" + $url + ")")
}
)')"
jq -n \
--arg title "Dependabot Security Alerts" \
--arg subtitle "${REPOSITORY}" \
--arg summary "${summary_line}" \
--arg summary2 "${summary_line_2}" \
--arg summary3 "${summary_line_3}" \
--arg generatedAt "${generated_at}" \
--arg headerTemplate "${header_template}" \
--argjson stats "${stats_elements}" \
--argjson tableRows "${table_rows_data}" \
--argjson hidden "${hidden_count}" '
{
msg_type: "interactive",
card: {
schema: "2.0",
config: {wide_screen_mode: true},
header: {
template: $headerTemplate,
title: {tag: "plain_text", content: $title},
subtitle: {tag: "plain_text", content: $subtitle}
},
body: {
elements:
(
[
{tag: "markdown", content: $summary},
{tag: "markdown", content: $summary2},
{tag: "markdown", content: $summary3},
{tag: "hr"},
$stats[0],
{tag: "hr"},
{
tag: "table",
page_size: 10,
row_height: "auto",
header_style: {
text_align: "left",
text_size: "normal",
text_color: "grey",
bold: true,
lines: 1
},
columns: [
{name: "level", display_name: "Level", data_type: "text", width: "120px"},
{name: "alert_id", display_name: "ID", data_type: "text", width: "90px"},
{name: "package", display_name: "Package", data_type: "lark_md", width: "140px"},
{name: "details", display_name: "Summary / Link", data_type: "lark_md", width: "auto"}
],
rows: $tableRows
}
]
+ (
if $hidden > 0
then [
{tag: "hr"},
{tag: "markdown", content: ("还有 " + ($hidden|tostring) + " 条告警未展示,请点击仓库安全页查看全部。")}
]
else []
end
)
+ [
{
tag: "div",
text: {
tag: "plain_text",
content: ("通知时间:" + $generatedAt),
text_color: "grey",
text_align: "right"
}
}
]
)
}
}
}'
}
api_url="https://api.github.com/repos/${REPOSITORY}/dependabot/alerts?state=open&severity=high,critical&assignee=none&per_page=100"
alerts_json="$(curl -sS -f -L \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
-H "X-GitHub-Api-Version: 2026-03-10" \
"$api_url")"
filtered_json="$(echo "${alerts_json}" | jq '[.[] | select(((.security_advisory.severity // "") == "high" or (.security_advisory.severity // "") == "critical") and ((.assignees | length) == 0))]')"
alert_count="$(echo "${filtered_json}" | jq 'length')"
echo "Filtered dependabot alerts count: ${alert_count}"
payload="$(build_payload "${filtered_json}")"
curl -sS -f -X POST "${FEISHU_WEBHOOK}" \
-H "Content-Type: application/json" \
-d "${payload}"

View File

@ -134,6 +134,7 @@ class EducationAutocompleteQuery(BaseModel):
class ChangeEmailSendPayload(BaseModel):
email: EmailStr
language: str | None = None
phase: str | None = None
token: str | None = None
@ -547,17 +548,13 @@ class ChangeEmailSendEmailApi(Resource):
account = None
user_email = None
email_for_sending = args.email.lower()
send_phase = AccountService.CHANGE_EMAIL_PHASE_OLD
if args.token is not None:
send_phase = AccountService.CHANGE_EMAIL_PHASE_NEW
if args.phase is not None and args.phase == "new_email":
if args.token is None:
raise InvalidTokenError()
reset_data = AccountService.get_change_email_data(args.token)
if reset_data is None:
raise InvalidTokenError()
reset_token_phase = reset_data.get(AccountService.CHANGE_EMAIL_TOKEN_PHASE_KEY)
if reset_token_phase != AccountService.CHANGE_EMAIL_PHASE_OLD_VERIFIED:
raise InvalidTokenError()
user_email = reset_data.get("email", "")
if user_email.lower() != current_user.email.lower():
@ -577,7 +574,7 @@ class ChangeEmailSendEmailApi(Resource):
email=email_for_sending,
old_email=user_email,
language=language,
phase=send_phase,
phase=args.phase,
)
return {"result": "success", "data": token}
@ -612,26 +609,12 @@ class ChangeEmailCheckApi(Resource):
AccountService.add_change_email_error_rate_limit(user_email)
raise EmailCodeError()
phase_transitions: dict[str, str] = {
AccountService.CHANGE_EMAIL_PHASE_OLD: AccountService.CHANGE_EMAIL_PHASE_OLD_VERIFIED,
AccountService.CHANGE_EMAIL_PHASE_NEW: AccountService.CHANGE_EMAIL_PHASE_NEW_VERIFIED,
}
token_phase = token_data.get(AccountService.CHANGE_EMAIL_TOKEN_PHASE_KEY)
if not isinstance(token_phase, str):
raise InvalidTokenError()
refreshed_phase = phase_transitions.get(token_phase)
if refreshed_phase is None:
raise InvalidTokenError()
# Verified, revoke the first token
AccountService.revoke_change_email_token(args.token)
# Refresh token data by generating a new token
_, new_token = AccountService.generate_change_email_token(
user_email,
code=args.code,
old_email=token_data.get("old_email"),
additional_data={AccountService.CHANGE_EMAIL_TOKEN_PHASE_KEY: refreshed_phase},
user_email, code=args.code, old_email=token_data.get("old_email"), additional_data={}
)
AccountService.reset_change_email_error_rate_limit(user_email)
@ -661,22 +644,13 @@ class ChangeEmailResetApi(Resource):
if not reset_data:
raise InvalidTokenError()
token_phase = reset_data.get(AccountService.CHANGE_EMAIL_TOKEN_PHASE_KEY)
if token_phase != AccountService.CHANGE_EMAIL_PHASE_NEW_VERIFIED:
raise InvalidTokenError()
token_email = reset_data.get("email")
normalized_token_email = token_email.lower() if isinstance(token_email, str) else token_email
if normalized_token_email != normalized_new_email:
raise InvalidTokenError()
AccountService.revoke_change_email_token(args.token)
old_email = reset_data.get("old_email", "")
current_user, _ = current_account_with_tenant()
if current_user.email.lower() != old_email.lower():
raise AccountNotFound()
AccountService.revoke_change_email_token(args.token)
updated_account = AccountService.update_account_email(current_user, email=normalized_new_email)
AccountService.send_change_email_completed_notify_email(

View File

@ -4,7 +4,6 @@ import logging
import secrets
import uuid
from datetime import UTC, datetime, timedelta
from enum import StrEnum
from hashlib import sha256
from typing import Any, cast
@ -91,25 +90,12 @@ class TokenPair(BaseModel):
csrf_token: str
class ChangeEmailPhase(StrEnum):
OLD = "old_email"
OLD_VERIFIED = "old_email_verified"
NEW = "new_email"
NEW_VERIFIED = "new_email_verified"
REFRESH_TOKEN_PREFIX = "refresh_token:"
ACCOUNT_REFRESH_TOKEN_PREFIX = "account_refresh_token:"
REFRESH_TOKEN_EXPIRY = timedelta(days=dify_config.REFRESH_TOKEN_EXPIRE_DAYS)
class AccountService:
CHANGE_EMAIL_TOKEN_PHASE_KEY = "email_change_phase"
CHANGE_EMAIL_PHASE_OLD = ChangeEmailPhase.OLD
CHANGE_EMAIL_PHASE_OLD_VERIFIED = ChangeEmailPhase.OLD_VERIFIED
CHANGE_EMAIL_PHASE_NEW = ChangeEmailPhase.NEW
CHANGE_EMAIL_PHASE_NEW_VERIFIED = ChangeEmailPhase.NEW_VERIFIED
reset_password_rate_limiter = RateLimiter(prefix="reset_password_rate_limit", max_attempts=1, time_window=60 * 1)
email_register_rate_limiter = RateLimiter(prefix="email_register_rate_limit", max_attempts=1, time_window=60 * 1)
email_code_login_rate_limiter = RateLimiter(
@ -566,20 +552,13 @@ class AccountService:
raise ValueError("Email must be provided.")
if not phase:
raise ValueError("phase must be provided.")
if phase not in (cls.CHANGE_EMAIL_PHASE_OLD, cls.CHANGE_EMAIL_PHASE_NEW):
raise ValueError("phase must be one of old_email or new_email.")
if cls.change_email_rate_limiter.is_rate_limited(account_email):
from controllers.console.auth.error import EmailChangeRateLimitExceededError
raise EmailChangeRateLimitExceededError(int(cls.change_email_rate_limiter.time_window / 60))
code, token = cls.generate_change_email_token(
account_email,
account,
old_email=old_email,
additional_data={cls.CHANGE_EMAIL_TOKEN_PHASE_KEY: phase},
)
code, token = cls.generate_change_email_token(account_email, account, old_email=old_email)
send_change_mail_task.delay(
language=language,

View File

@ -1,11 +1,12 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timedelta
from datetime import timedelta
from decimal import Decimal
from uuid import uuid4
from graphon.nodes.human_input.entities import FormDefinition, UserAction
from libs.datetime_utils import naive_utc_now
from models.account import Account, Tenant, TenantAccountJoin
from models.enums import ConversationFromSource, InvokeFrom
from models.execution_extra_content import HumanInputContent
@ -117,7 +118,7 @@ def create_human_input_message_fixture(db_session) -> HumanInputMessageFixture:
inputs=[],
user_actions=[UserAction(id=action_id, title=action_text)],
rendered_content="Rendered block",
expiration_time=datetime.utcnow() + timedelta(days=1),
expiration_time=naive_utc_now() + timedelta(days=1),
node_title=node_title,
display_in_ui=True,
)
@ -129,7 +130,7 @@ def create_human_input_message_fixture(db_session) -> HumanInputMessageFixture:
form_definition=form_definition.model_dump_json(),
rendered_content="Rendered block",
status=HumanInputFormStatus.SUBMITTED,
expiration_time=datetime.utcnow() + timedelta(days=1),
expiration_time=naive_utc_now() + timedelta(days=1),
selected_action_id=action_id,
)
db_session.add(form)

View File

@ -7,7 +7,7 @@ from __future__ import annotations
from collections.abc import Generator
from dataclasses import dataclass
from datetime import datetime, timedelta
from datetime import timedelta
from decimal import Decimal
from uuid import uuid4
@ -17,6 +17,7 @@ from sqlalchemy.orm import Session, sessionmaker
from graphon.nodes.human_input.entities import FormDefinition, UserAction
from graphon.nodes.human_input.enums import HumanInputFormStatus
from libs.datetime_utils import naive_utc_now
from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole
from models.enums import ConversationFromSource, InvokeFrom
from models.execution_extra_content import ExecutionExtraContent, HumanInputContent
@ -174,7 +175,7 @@ def _create_submitted_form(
action_title: str = "Approve",
node_title: str = "Approval",
) -> HumanInputForm:
expiration_time = datetime.utcnow() + timedelta(days=1)
expiration_time = naive_utc_now() + timedelta(days=1)
form_definition = FormDefinition(
form_content="content",
inputs=[],
@ -207,7 +208,7 @@ def _create_waiting_form(
workflow_run_id: str,
default_values: dict | None = None,
) -> HumanInputForm:
expiration_time = datetime.utcnow() + timedelta(days=1)
expiration_time = naive_utc_now() + timedelta(days=1)
form_definition = FormDefinition(
form_content="content",
inputs=[],

View File

@ -954,16 +954,6 @@ class TestWorkflowAppService:
assert result_with_new_email["total"] == 3
assert all(log.created_by_role == CreatorUserRole.ACCOUNT for log in result_with_new_email["data"])
# Create another account in a different tenant using the original email.
# Querying by the old email should still fail for this app's tenant.
cross_tenant_account = AccountService.create_account(
email=original_email,
name=fake.name(),
interface_language="en-US",
password=fake.password(length=12),
)
TenantService.create_owner_tenant_if_not_exist(cross_tenant_account, name=fake.company())
# Old email unbound, is unexpected input, should raise ValueError
with pytest.raises(ValueError) as exc_info:
service.get_paginate_workflow_app_logs(

View File

@ -26,6 +26,7 @@ from controllers.console.datasets.rag_pipeline.rag_pipeline_workflow import (
RagPipelineWorkflowLastRunApi,
)
from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError
from libs.datetime_utils import naive_utc_now
from services.errors.app import IsDraftWorkflowError, WorkflowHashNotEqualError, WorkflowNotFoundError
from services.errors.llm import InvokeRateLimitError
@ -372,7 +373,7 @@ class TestPublishedPipelineApis:
workflow = MagicMock(
id="w1",
created_at=datetime.utcnow(),
created_at=naive_utc_now(),
)
session = MagicMock()

View File

@ -1,4 +1,3 @@
from datetime import datetime
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
@ -25,6 +24,7 @@ from controllers.console.datasets.error import (
)
from core.errors.error import LLMBadRequestError, ProviderTokenNotInitError
from core.rag.index_processor.constant.index_type import IndexStructureType
from libs.datetime_utils import naive_utc_now
from models.dataset import ChildChunk, DocumentSegment
from models.model import UploadFile
@ -54,8 +54,8 @@ def _segment():
disabled_by=None,
status="normal",
created_by="u1",
created_at=datetime.utcnow(),
updated_at=datetime.utcnow(),
created_at=naive_utc_now(),
updated_at=naive_utc_now(),
updated_by="u1",
indexing_at=None,
completed_at=None,

View File

@ -4,7 +4,6 @@ from unittest.mock import MagicMock, patch
import pytest
from flask import Flask, g
from controllers.console.auth.error import InvalidTokenError
from controllers.console.workspace.account import (
AccountDeleteUpdateFeedbackApi,
ChangeEmailCheckApi,
@ -53,7 +52,7 @@ class TestChangeEmailSend:
@patch("controllers.console.workspace.account.extract_remote_ip", return_value="127.0.0.1")
@patch("libs.login.check_csrf_token", return_value=None)
@patch("controllers.console.wraps.FeatureService.get_system_features")
def test_should_infer_new_email_phase_from_token(
def test_should_normalize_new_email_phase(
self,
mock_features,
mock_csrf,
@ -69,16 +68,13 @@ class TestChangeEmailSend:
mock_features.return_value = SimpleNamespace(enable_change_email=True)
mock_account = _build_account("current@example.com", "acc1")
mock_current_account.return_value = (mock_account, None)
mock_get_change_data.return_value = {
"email": "current@example.com",
AccountService.CHANGE_EMAIL_TOKEN_PHASE_KEY: AccountService.CHANGE_EMAIL_PHASE_OLD_VERIFIED,
}
mock_get_change_data.return_value = {"email": "current@example.com"}
mock_send_email.return_value = "token-abc"
with app.test_request_context(
"/account/change-email",
method="POST",
json={"email": "New@Example.com", "language": "en-US", "token": "token-123"},
json={"email": "New@Example.com", "language": "en-US", "phase": "new_email", "token": "token-123"},
):
_set_logged_in_user(_build_account("tester@example.com", "tester"))
response = ChangeEmailSendEmailApi().post()
@ -95,107 +91,6 @@ class TestChangeEmailSend:
mock_is_ip_limit.assert_called_once_with("127.0.0.1")
mock_csrf.assert_called_once()
@patch("controllers.console.wraps.db")
@patch("controllers.console.workspace.account.current_account_with_tenant")
@patch("controllers.console.workspace.account.db")
@patch("controllers.console.workspace.account.Session")
@patch("controllers.console.workspace.account.AccountService.get_account_by_email_with_case_fallback")
@patch("controllers.console.workspace.account.AccountService.send_change_email_email")
@patch("controllers.console.workspace.account.AccountService.is_email_send_ip_limit", return_value=False)
@patch("controllers.console.workspace.account.extract_remote_ip", return_value="127.0.0.1")
@patch("libs.login.check_csrf_token", return_value=None)
@patch("controllers.console.wraps.FeatureService.get_system_features")
def test_should_ignore_client_phase_and_use_old_phase_when_token_missing(
self,
mock_features,
mock_csrf,
mock_extract_ip,
mock_is_ip_limit,
mock_send_email,
mock_get_account_by_email,
mock_session_cls,
mock_account_db,
mock_current_account,
mock_db,
app,
):
_mock_wraps_db(mock_db)
mock_features.return_value = SimpleNamespace(enable_change_email=True)
mock_current_account.return_value = (_build_account("current@example.com", "current"), None)
existing_account = _build_account("old@example.com", "acc-old")
mock_get_account_by_email.return_value = existing_account
mock_send_email.return_value = "token-legacy"
mock_session = MagicMock()
mock_session_cm = MagicMock()
mock_session_cm.__enter__.return_value = mock_session
mock_session_cm.__exit__.return_value = None
mock_session_cls.return_value = mock_session_cm
mock_account_db.engine = MagicMock()
with app.test_request_context(
"/account/change-email",
method="POST",
json={"email": "old@example.com", "language": "en-US", "phase": "new_email"},
):
_set_logged_in_user(_build_account("tester@example.com", "tester"))
response = ChangeEmailSendEmailApi().post()
assert response == {"result": "success", "data": "token-legacy"}
mock_get_account_by_email.assert_called_once_with("old@example.com", session=mock_session)
mock_send_email.assert_called_once_with(
account=existing_account,
email="old@example.com",
old_email="old@example.com",
language="en-US",
phase=AccountService.CHANGE_EMAIL_PHASE_OLD,
)
mock_extract_ip.assert_called_once()
mock_is_ip_limit.assert_called_once_with("127.0.0.1")
mock_csrf.assert_called_once()
@patch("controllers.console.wraps.db")
@patch("controllers.console.workspace.account.current_account_with_tenant")
@patch("controllers.console.workspace.account.AccountService.get_change_email_data")
@patch("controllers.console.workspace.account.AccountService.send_change_email_email")
@patch("controllers.console.workspace.account.AccountService.is_email_send_ip_limit", return_value=False)
@patch("controllers.console.workspace.account.extract_remote_ip", return_value="127.0.0.1")
@patch("libs.login.check_csrf_token", return_value=None)
@patch("controllers.console.wraps.FeatureService.get_system_features")
def test_should_reject_unverified_old_email_token_for_new_email_phase(
self,
mock_features,
mock_csrf,
mock_extract_ip,
mock_is_ip_limit,
mock_send_email,
mock_get_change_data,
mock_current_account,
mock_db,
app,
):
_mock_wraps_db(mock_db)
mock_features.return_value = SimpleNamespace(enable_change_email=True)
mock_account = _build_account("current@example.com", "acc1")
mock_current_account.return_value = (mock_account, None)
mock_get_change_data.return_value = {
"email": "current@example.com",
AccountService.CHANGE_EMAIL_TOKEN_PHASE_KEY: AccountService.CHANGE_EMAIL_PHASE_OLD,
}
with app.test_request_context(
"/account/change-email",
method="POST",
json={"email": "New@Example.com", "language": "en-US", "token": "token-123"},
):
_set_logged_in_user(_build_account("tester@example.com", "tester"))
with pytest.raises(InvalidTokenError):
ChangeEmailSendEmailApi().post()
mock_send_email.assert_not_called()
mock_extract_ip.assert_called_once()
mock_is_ip_limit.assert_called_once_with("127.0.0.1")
mock_csrf.assert_called_once()
class TestChangeEmailValidity:
@patch("controllers.console.wraps.db")
@ -227,12 +122,7 @@ class TestChangeEmailValidity:
mock_account = _build_account("user@example.com", "acc2")
mock_current_account.return_value = (mock_account, None)
mock_is_rate_limit.return_value = False
mock_get_data.return_value = {
"email": "user@example.com",
"code": "1234",
"old_email": "old@example.com",
AccountService.CHANGE_EMAIL_TOKEN_PHASE_KEY: AccountService.CHANGE_EMAIL_PHASE_OLD,
}
mock_get_data.return_value = {"email": "user@example.com", "code": "1234", "old_email": "old@example.com"}
mock_generate_token.return_value = (None, "new-token")
with app.test_request_context(
@ -248,76 +138,11 @@ class TestChangeEmailValidity:
mock_add_rate.assert_not_called()
mock_revoke_token.assert_called_once_with("token-123")
mock_generate_token.assert_called_once_with(
"user@example.com",
code="1234",
old_email="old@example.com",
additional_data={
AccountService.CHANGE_EMAIL_TOKEN_PHASE_KEY: AccountService.CHANGE_EMAIL_PHASE_OLD_VERIFIED
},
"user@example.com", code="1234", old_email="old@example.com", additional_data={}
)
mock_reset_rate.assert_called_once_with("user@example.com")
mock_csrf.assert_called_once()
@patch("controllers.console.wraps.db")
@patch("controllers.console.workspace.account.current_account_with_tenant")
@patch("controllers.console.workspace.account.AccountService.reset_change_email_error_rate_limit")
@patch("controllers.console.workspace.account.AccountService.generate_change_email_token")
@patch("controllers.console.workspace.account.AccountService.revoke_change_email_token")
@patch("controllers.console.workspace.account.AccountService.add_change_email_error_rate_limit")
@patch("controllers.console.workspace.account.AccountService.get_change_email_data")
@patch("controllers.console.workspace.account.AccountService.is_change_email_error_rate_limit")
@patch("libs.login.check_csrf_token", return_value=None)
@patch("controllers.console.wraps.FeatureService.get_system_features")
def test_should_refresh_new_email_phase_to_verified(
self,
mock_features,
mock_csrf,
mock_is_rate_limit,
mock_get_data,
mock_add_rate,
mock_revoke_token,
mock_generate_token,
mock_reset_rate,
mock_current_account,
mock_db,
app,
):
_mock_wraps_db(mock_db)
mock_features.return_value = SimpleNamespace(enable_change_email=True)
mock_account = _build_account("old@example.com", "acc2")
mock_current_account.return_value = (mock_account, None)
mock_is_rate_limit.return_value = False
mock_get_data.return_value = {
"email": "new@example.com",
"code": "5678",
"old_email": "old@example.com",
AccountService.CHANGE_EMAIL_TOKEN_PHASE_KEY: AccountService.CHANGE_EMAIL_PHASE_NEW,
}
mock_generate_token.return_value = (None, "new-phase-token")
with app.test_request_context(
"/account/change-email/validity",
method="POST",
json={"email": "New@Example.com", "code": "5678", "token": "token-456"},
):
_set_logged_in_user(_build_account("tester@example.com", "tester"))
response = ChangeEmailCheckApi().post()
assert response == {"is_valid": True, "email": "new@example.com", "token": "new-phase-token"}
mock_is_rate_limit.assert_called_once_with("new@example.com")
mock_add_rate.assert_not_called()
mock_revoke_token.assert_called_once_with("token-456")
mock_generate_token.assert_called_once_with(
"new@example.com",
code="5678",
old_email="old@example.com",
additional_data={
AccountService.CHANGE_EMAIL_TOKEN_PHASE_KEY: AccountService.CHANGE_EMAIL_PHASE_NEW_VERIFIED
},
)
mock_reset_rate.assert_called_once_with("new@example.com")
mock_csrf.assert_called_once()
class TestChangeEmailReset:
@patch("controllers.console.wraps.db")
@ -350,11 +175,7 @@ class TestChangeEmailReset:
mock_current_account.return_value = (current_user, None)
mock_is_freeze.return_value = False
mock_check_unique.return_value = True
mock_get_data.return_value = {
"old_email": "OLD@example.com",
"email": "new@example.com",
AccountService.CHANGE_EMAIL_TOKEN_PHASE_KEY: AccountService.CHANGE_EMAIL_PHASE_NEW_VERIFIED,
}
mock_get_data.return_value = {"old_email": "OLD@example.com"}
mock_account_after_update = _build_account("new@example.com", "acc3-updated")
mock_update_account.return_value = mock_account_after_update
@ -373,106 +194,6 @@ class TestChangeEmailReset:
mock_send_notify.assert_called_once_with(email="new@example.com")
mock_csrf.assert_called_once()
@patch("controllers.console.wraps.db")
@patch("controllers.console.workspace.account.current_account_with_tenant")
@patch("controllers.console.workspace.account.AccountService.send_change_email_completed_notify_email")
@patch("controllers.console.workspace.account.AccountService.update_account_email")
@patch("controllers.console.workspace.account.AccountService.revoke_change_email_token")
@patch("controllers.console.workspace.account.AccountService.get_change_email_data")
@patch("controllers.console.workspace.account.AccountService.check_email_unique")
@patch("controllers.console.workspace.account.AccountService.is_account_in_freeze")
@patch("libs.login.check_csrf_token", return_value=None)
@patch("controllers.console.wraps.FeatureService.get_system_features")
def test_should_reject_old_phase_token_for_reset(
self,
mock_features,
mock_csrf,
mock_is_freeze,
mock_check_unique,
mock_get_data,
mock_revoke_token,
mock_update_account,
mock_send_notify,
mock_current_account,
mock_db,
app,
):
_mock_wraps_db(mock_db)
mock_features.return_value = SimpleNamespace(enable_change_email=True)
current_user = _build_account("old@example.com", "acc3")
mock_current_account.return_value = (current_user, None)
mock_is_freeze.return_value = False
mock_check_unique.return_value = True
mock_get_data.return_value = {
"old_email": "OLD@example.com",
"email": "old@example.com",
AccountService.CHANGE_EMAIL_TOKEN_PHASE_KEY: AccountService.CHANGE_EMAIL_PHASE_OLD,
}
with app.test_request_context(
"/account/change-email/reset",
method="POST",
json={"new_email": "new@example.com", "token": "token-123"},
):
_set_logged_in_user(_build_account("tester@example.com", "tester"))
with pytest.raises(InvalidTokenError):
ChangeEmailResetApi().post()
mock_revoke_token.assert_not_called()
mock_update_account.assert_not_called()
mock_send_notify.assert_not_called()
mock_csrf.assert_called_once()
@patch("controllers.console.wraps.db")
@patch("controllers.console.workspace.account.current_account_with_tenant")
@patch("controllers.console.workspace.account.AccountService.send_change_email_completed_notify_email")
@patch("controllers.console.workspace.account.AccountService.update_account_email")
@patch("controllers.console.workspace.account.AccountService.revoke_change_email_token")
@patch("controllers.console.workspace.account.AccountService.get_change_email_data")
@patch("controllers.console.workspace.account.AccountService.check_email_unique")
@patch("controllers.console.workspace.account.AccountService.is_account_in_freeze")
@patch("libs.login.check_csrf_token", return_value=None)
@patch("controllers.console.wraps.FeatureService.get_system_features")
def test_should_reject_mismatched_new_email_for_verified_token(
self,
mock_features,
mock_csrf,
mock_is_freeze,
mock_check_unique,
mock_get_data,
mock_revoke_token,
mock_update_account,
mock_send_notify,
mock_current_account,
mock_db,
app,
):
_mock_wraps_db(mock_db)
mock_features.return_value = SimpleNamespace(enable_change_email=True)
current_user = _build_account("old@example.com", "acc3")
mock_current_account.return_value = (current_user, None)
mock_is_freeze.return_value = False
mock_check_unique.return_value = True
mock_get_data.return_value = {
"old_email": "OLD@example.com",
"email": "another@example.com",
AccountService.CHANGE_EMAIL_TOKEN_PHASE_KEY: AccountService.CHANGE_EMAIL_PHASE_NEW_VERIFIED,
}
with app.test_request_context(
"/account/change-email/reset",
method="POST",
json={"new_email": "new@example.com", "token": "token-789"},
):
_set_logged_in_user(_build_account("tester@example.com", "tester"))
with pytest.raises(InvalidTokenError):
ChangeEmailResetApi().post()
mock_revoke_token.assert_not_called()
mock_update_account.assert_not_called()
mock_send_notify.assert_not_called()
mock_csrf.assert_called_once()
class TestAccountDeletionFeedback:
@patch("controllers.console.wraps.db")

View File

@ -1,4 +1,3 @@
from datetime import datetime
from io import BytesIO
from unittest.mock import MagicMock, patch
@ -26,6 +25,7 @@ from controllers.console.workspace.workspace import (
WorkspacePermissionApi,
)
from enums.cloud_plan import CloudPlan
from libs.datetime_utils import naive_utc_now
from models.account import TenantStatus
@ -44,13 +44,13 @@ class TestTenantListApi:
id="t1",
name="Tenant 1",
status="active",
created_at=datetime.utcnow(),
created_at=naive_utc_now(),
)
tenant2 = MagicMock(
id="t2",
name="Tenant 2",
status="active",
created_at=datetime.utcnow(),
created_at=naive_utc_now(),
)
with (
@ -97,13 +97,13 @@ class TestTenantListApi:
id="t1",
name="Tenant 1",
status="active",
created_at=datetime.utcnow(),
created_at=naive_utc_now(),
)
tenant2 = MagicMock(
id="t2",
name="Tenant 2",
status="active",
created_at=datetime.utcnow(),
created_at=naive_utc_now(),
)
features_t2 = MagicMock()
@ -152,13 +152,13 @@ class TestTenantListApi:
id="t1",
name="Tenant 1",
status="active",
created_at=datetime.utcnow(),
created_at=naive_utc_now(),
)
tenant2 = MagicMock(
id="t2",
name="Tenant 2",
status="active",
created_at=datetime.utcnow(),
created_at=naive_utc_now(),
)
features = MagicMock()
@ -204,7 +204,7 @@ class TestTenantListApi:
id="t1",
name="Tenant",
status="active",
created_at=datetime.utcnow(),
created_at=naive_utc_now(),
)
features = MagicMock()
@ -243,13 +243,13 @@ class TestTenantListApi:
id="t1",
name="Tenant 1",
status="active",
created_at=datetime.utcnow(),
created_at=naive_utc_now(),
)
tenant2 = MagicMock(
id="t2",
name="Tenant 2",
status="active",
created_at=datetime.utcnow(),
created_at=naive_utc_now(),
)
with (
@ -305,7 +305,7 @@ class TestWorkspaceListApi:
api = WorkspaceListApi()
method = unwrap(api.get)
tenant = MagicMock(id="t1", name="T", status="active", created_at=datetime.utcnow())
tenant = MagicMock(id="t1", name="T", status="active", created_at=naive_utc_now())
paginate_result = MagicMock(
items=[tenant],
@ -331,7 +331,7 @@ class TestWorkspaceListApi:
id="t1",
name="T",
status="active",
created_at=datetime.utcnow(),
created_at=naive_utc_now(),
)
paginate_result = MagicMock(

View File

@ -1,7 +1,6 @@
from __future__ import annotations
from contextlib import contextmanager
from datetime import datetime
from types import SimpleNamespace
import pytest
@ -45,6 +44,7 @@ from core.base.tts.app_generator_tts_publisher import AudioTrunk
from core.workflow.system_variables import build_system_variables
from graphon.enums import BuiltinNodeTypes
from graphon.runtime import GraphRuntimeState, VariablePool
from libs.datetime_utils import naive_utc_now
from models.enums import MessageStatus
from models.model import AppMode, EndUser
from tests.workflow_test_utils import build_test_variable_pool
@ -76,7 +76,7 @@ def _make_pipeline():
message = SimpleNamespace(
id="message-id",
query="hello",
created_at=datetime.utcnow(),
created_at=naive_utc_now(),
status=MessageStatus.NORMAL,
answer="",
)
@ -257,7 +257,7 @@ class TestAdvancedChatGenerateTaskPipeline:
node_id="node",
node_type=BuiltinNodeTypes.LLM,
node_title="LLM",
start_at=datetime.utcnow(),
start_at=naive_utc_now(),
node_run_index=1,
)
iter_next = QueueIterationNextEvent(
@ -273,7 +273,7 @@ class TestAdvancedChatGenerateTaskPipeline:
node_id="node",
node_type=BuiltinNodeTypes.LLM,
node_title="LLM",
start_at=datetime.utcnow(),
start_at=naive_utc_now(),
node_run_index=1,
)
loop_start = QueueLoopStartEvent(
@ -281,7 +281,7 @@ class TestAdvancedChatGenerateTaskPipeline:
node_id="node",
node_type=BuiltinNodeTypes.LLM,
node_title="LLM",
start_at=datetime.utcnow(),
start_at=naive_utc_now(),
node_run_index=1,
)
loop_next = QueueLoopNextEvent(
@ -297,7 +297,7 @@ class TestAdvancedChatGenerateTaskPipeline:
node_id="node",
node_type=BuiltinNodeTypes.LLM,
node_title="LLM",
start_at=datetime.utcnow(),
start_at=naive_utc_now(),
node_run_index=1,
)
@ -360,7 +360,7 @@ class TestAdvancedChatGenerateTaskPipeline:
node_execution_id="exec",
node_id="node",
node_type=BuiltinNodeTypes.LLM,
start_at=datetime.utcnow(),
start_at=naive_utc_now(),
inputs={},
outputs={},
process_data={},
@ -370,7 +370,7 @@ class TestAdvancedChatGenerateTaskPipeline:
node_execution_id="exec",
node_id="node",
node_type=BuiltinNodeTypes.LLM,
start_at=datetime.utcnow(),
start_at=naive_utc_now(),
inputs={},
outputs={},
process_data={},
@ -473,7 +473,7 @@ class TestAdvancedChatGenerateTaskPipeline:
node_id="node",
node_type=BuiltinNodeTypes.LLM,
node_title="title",
expiration_time=datetime.utcnow(),
expiration_time=naive_utc_now(),
)
assert list(pipeline._handle_human_input_form_filled_event(filled_event)) == ["filled"]
@ -591,7 +591,7 @@ class TestAdvancedChatGenerateTaskPipeline:
node_execution_id="exec",
node_id="node",
node_type=BuiltinNodeTypes.LLM,
start_at=datetime.utcnow(),
start_at=naive_utc_now(),
inputs={},
outputs={},
process_data={},

View File

@ -1,7 +1,6 @@
from __future__ import annotations
from contextlib import contextmanager
from datetime import datetime
from types import SimpleNamespace
import pytest
@ -47,6 +46,7 @@ from core.base.tts.app_generator_tts_publisher import AudioTrunk
from core.workflow.system_variables import build_system_variables, system_variables_to_mapping
from graphon.enums import BuiltinNodeTypes, WorkflowExecutionStatus
from graphon.runtime import GraphRuntimeState, VariablePool
from libs.datetime_utils import naive_utc_now
from models.enums import CreatorUserRole
from models.model import AppMode, EndUser
from tests.workflow_test_utils import build_test_variable_pool
@ -192,7 +192,7 @@ class TestWorkflowGenerateTaskPipeline:
node_execution_id="exec",
node_id="node",
node_type=BuiltinNodeTypes.START,
start_at=datetime.utcnow(),
start_at=naive_utc_now(),
inputs={},
outputs={},
process_data={},
@ -245,7 +245,7 @@ class TestWorkflowGenerateTaskPipeline:
node_execution_id="exec",
node_id="node",
node_type=BuiltinNodeTypes.START,
start_at=datetime.utcnow(),
start_at=naive_utc_now(),
inputs={},
outputs={},
process_data={},
@ -303,7 +303,7 @@ class TestWorkflowGenerateTaskPipeline:
node_id="node",
node_type=BuiltinNodeTypes.LLM,
node_title="LLM",
start_at=datetime.utcnow(),
start_at=naive_utc_now(),
node_run_index=1,
)
iter_next = QueueIterationNextEvent(
@ -319,7 +319,7 @@ class TestWorkflowGenerateTaskPipeline:
node_id="node",
node_type=BuiltinNodeTypes.LLM,
node_title="LLM",
start_at=datetime.utcnow(),
start_at=naive_utc_now(),
node_run_index=1,
)
loop_start = QueueLoopStartEvent(
@ -327,7 +327,7 @@ class TestWorkflowGenerateTaskPipeline:
node_id="node",
node_type=BuiltinNodeTypes.LLM,
node_title="LLM",
start_at=datetime.utcnow(),
start_at=naive_utc_now(),
node_run_index=1,
)
loop_next = QueueLoopNextEvent(
@ -343,7 +343,7 @@ class TestWorkflowGenerateTaskPipeline:
node_id="node",
node_type=BuiltinNodeTypes.LLM,
node_title="LLM",
start_at=datetime.utcnow(),
start_at=naive_utc_now(),
node_run_index=1,
)
filled_event = QueueHumanInputFormFilledEvent(
@ -359,7 +359,7 @@ class TestWorkflowGenerateTaskPipeline:
node_id="node",
node_type=BuiltinNodeTypes.LLM,
node_title="title",
expiration_time=datetime.utcnow(),
expiration_time=naive_utc_now(),
)
agent_event = QueueAgentLogEvent(
id="log",
@ -648,7 +648,7 @@ class TestWorkflowGenerateTaskPipeline:
node_title="title",
node_type=BuiltinNodeTypes.LLM,
node_run_index=1,
start_at=datetime.utcnow(),
start_at=naive_utc_now(),
provider_type="provider",
provider_id="provider-id",
error="error",
@ -660,7 +660,7 @@ class TestWorkflowGenerateTaskPipeline:
node_title="title",
node_type=BuiltinNodeTypes.LLM,
node_run_index=1,
start_at=datetime.utcnow(),
start_at=naive_utc_now(),
provider_type="provider",
provider_id="provider-id",
)
@ -685,7 +685,7 @@ class TestWorkflowGenerateTaskPipeline:
node_execution_id="exec-id",
node_id="node",
node_type=BuiltinNodeTypes.START,
start_at=datetime.utcnow(),
start_at=naive_utc_now(),
inputs={},
outputs={},
process_data={},
@ -836,7 +836,7 @@ class TestWorkflowGenerateTaskPipeline:
node_id="node-id",
node_type=BuiltinNodeTypes.START,
in_loop_id="loop-id",
start_at=datetime.utcnow(),
start_at=naive_utc_now(),
process_data={"k": "v"},
outputs={"out": 1},
)

View File

@ -1,5 +1,4 @@
from collections.abc import Sequence
from datetime import datetime
from unittest.mock import Mock
from core.app.layers.conversation_variable_persist_layer import ConversationVariablePersistenceLayer
@ -12,6 +11,7 @@ from graphon.node_events import NodeRunResult
from graphon.runtime.graph_runtime_state_protocol import ReadOnlyGraphRuntimeState
from graphon.variables import StringVariable
from graphon.variables.segments import Segment, StringSegment
from libs.datetime_utils import naive_utc_now
class MockReadOnlyVariablePool:
@ -48,7 +48,7 @@ def _build_node_run_succeeded_event() -> NodeRunSucceededEvent:
id="node-exec-id",
node_id="assigner",
node_type=BuiltinNodeTypes.LLM,
start_at=datetime.utcnow(),
start_at=naive_utc_now(),
node_run_result=NodeRunResult(
status=WorkflowNodeExecutionStatus.SUCCEEDED,
outputs={},

View File

@ -274,7 +274,7 @@ def _make_form_definition() -> str:
inputs=[],
user_actions=[UserAction(id="submit", title="Submit")],
rendered_content="<p>hello</p>",
expiration_time=datetime.utcnow(),
expiration_time=naive_utc_now(),
).model_dump_json()

View File

@ -1,10 +1,10 @@
import queue
from datetime import datetime
from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionStatus
from graphon.graph_engine.orchestration.dispatcher import Dispatcher
from graphon.graph_events import NodeRunSucceededEvent
from graphon.node_events import NodeRunResult
from libs.datetime_utils import naive_utc_now
class StubExecutionCoordinator:
@ -52,7 +52,7 @@ def test_dispatcher_drains_events_when_paused() -> None:
id="exec-1",
node_id="node-1",
node_type=BuiltinNodeTypes.START,
start_at=datetime.utcnow(),
start_at=naive_utc_now(),
node_run_result=NodeRunResult(status=WorkflowNodeExecutionStatus.SUCCEEDED),
)
event_queue.put(event)

View File

@ -6,6 +6,7 @@ from typing import Any
from graphon.nodes.human_input.entities import FormInput
from graphon.nodes.human_input.enums import TimeoutUnit
from libs.datetime_utils import naive_utc_now
# Exceptions
@ -49,7 +50,7 @@ class HumanInputForm:
timeout: int
timeout_unit: TimeoutUnit
form_token: str | None = None
created_at: datetime = field(default_factory=datetime.utcnow)
created_at: datetime = field(default_factory=naive_utc_now)
expires_at: datetime | None = None
submitted_at: datetime | None = None
submitted_data: dict[str, Any] | None = None
@ -61,7 +62,7 @@ class HumanInputForm:
@property
def is_expired(self) -> bool:
return self.expires_at is not None and datetime.utcnow() > self.expires_at
return self.expires_at is not None and naive_utc_now() > self.expires_at
@property
def is_submitted(self) -> bool:
@ -70,7 +71,7 @@ class HumanInputForm:
def mark_submitted(self, inputs: dict[str, Any], action: str) -> None:
self.submitted_data = inputs
self.submitted_action = action
self.submitted_at = datetime.utcnow()
self.submitted_at = naive_utc_now()
def submit(self, inputs: dict[str, Any], action: str) -> None:
self.mark_submitted(inputs, action)
@ -107,7 +108,7 @@ class FormSubmissionData:
form_id: str
inputs: dict[str, Any]
action: str
submitted_at: datetime = field(default_factory=datetime.utcnow)
submitted_at: datetime = field(default_factory=naive_utc_now)
@classmethod
def from_request(cls, form_id: str, request: FormSubmissionRequest) -> FormSubmissionData: # type: ignore

View File

@ -2,7 +2,7 @@
Unit tests for FormService.
"""
from datetime import datetime, timedelta
from datetime import timedelta
import pytest
@ -142,7 +142,7 @@ class TestFormService:
# Manually expire the form by modifying expiry time
form = form_service.get_form_by_id("form-123")
form.expires_at = datetime.utcnow() - timedelta(hours=1)
form.expires_at = naive_utc_now() - timedelta(hours=1)
form_service.repository.save(form)
# Should raise FormExpiredError
@ -227,7 +227,7 @@ class TestFormService:
# Manually expire the form
form = form_service.get_form_by_id("form-123")
form.expires_at = datetime.utcnow() - timedelta(hours=1)
form.expires_at = naive_utc_now() - timedelta(hours=1)
form_service.repository.save(form)
# Try to submit expired form

View File

@ -14,6 +14,7 @@ from graphon.nodes.human_input.enums import (
FormInputType,
TimeoutUnit,
)
from libs.datetime_utils import naive_utc_now
from .support import FormSubmissionData, FormSubmissionRequest, HumanInputForm
@ -83,7 +84,7 @@ class TestHumanInputForm:
def test_form_expiry_property_expired(self, sample_form_data):
"""Test is_expired property for expired form."""
# Create form with past expiry
past_time = datetime.utcnow() - timedelta(hours=1)
past_time = naive_utc_now() - timedelta(hours=1)
sample_form_data["created_at"] = past_time
form = HumanInputForm(**sample_form_data)
@ -111,9 +112,9 @@ class TestHumanInputForm:
"""Test form submit method."""
form = HumanInputForm(**sample_form_data)
submission_time_before = datetime.utcnow()
submission_time_before = naive_utc_now()
form.submit({"input": "test value"}, "submit")
submission_time_after = datetime.utcnow()
submission_time_after = naive_utc_now()
assert form.is_submitted
assert form.submitted_data == {"input": "test value"}
@ -213,11 +214,11 @@ class TestFormSubmissionData:
def test_submission_data_timestamps(self):
"""Test submission data timestamp handling."""
before_time = datetime.utcnow()
before_time = naive_utc_now()
submission_data = FormSubmissionData(form_id="form-123", inputs={"test": "value"}, action="submit")
after_time = datetime.utcnow()
after_time = naive_utc_now()
assert before_time <= submission_data.submitted_at <= after_time

View File

@ -6,13 +6,14 @@ Tests are organized by functionality and include edge cases, error handling,
and both positive and negative test scenarios.
"""
from datetime import datetime, timedelta
from datetime import timedelta
from unittest.mock import MagicMock, Mock, create_autospec, patch
import pytest
from sqlalchemy import asc, desc
from core.app.entities.app_invoke_entities import InvokeFrom
from libs.datetime_utils import naive_utc_now
from libs.infinite_scroll_pagination import InfiniteScrollPagination
from models import Account, ConversationVariable
from models.enums import ConversationFromSource
@ -122,8 +123,8 @@ class ConversationServiceTestDataFactory:
conversation.is_deleted = kwargs.get("is_deleted", False)
conversation.name = kwargs.get("name", "Test Conversation")
conversation.status = kwargs.get("status", "normal")
conversation.created_at = kwargs.get("created_at", datetime.utcnow())
conversation.updated_at = kwargs.get("updated_at", datetime.utcnow())
conversation.created_at = kwargs.get("created_at", naive_utc_now())
conversation.updated_at = kwargs.get("updated_at", naive_utc_now())
for key, value in kwargs.items():
setattr(conversation, key, value)
return conversation
@ -152,7 +153,7 @@ class ConversationServiceTestDataFactory:
message.conversation_id = conversation_id
message.app_id = app_id
message.query = kwargs.get("query", "Test message content")
message.created_at = kwargs.get("created_at", datetime.utcnow())
message.created_at = kwargs.get("created_at", naive_utc_now())
for key, value in kwargs.items():
setattr(message, key, value)
return message
@ -181,8 +182,8 @@ class ConversationServiceTestDataFactory:
variable.conversation_id = conversation_id
variable.app_id = app_id
variable.data = {"name": kwargs.get("name", "test_var"), "value": kwargs.get("value", "test_value")}
variable.created_at = kwargs.get("created_at", datetime.utcnow())
variable.updated_at = kwargs.get("updated_at", datetime.utcnow())
variable.created_at = kwargs.get("created_at", naive_utc_now())
variable.updated_at = kwargs.get("updated_at", naive_utc_now())
# Mock to_variable method
mock_variable = Mock()
@ -302,7 +303,7 @@ class TestConversationServiceHelpers:
"""
# Arrange
mock_conversation = ConversationServiceTestDataFactory.create_conversation_mock()
mock_conversation.updated_at = datetime.utcnow()
mock_conversation.updated_at = naive_utc_now()
# Act
condition = ConversationService._build_filter_condition(
@ -323,7 +324,7 @@ class TestConversationServiceHelpers:
"""
# Arrange
mock_conversation = ConversationServiceTestDataFactory.create_conversation_mock()
mock_conversation.created_at = datetime.utcnow()
mock_conversation.created_at = naive_utc_now()
# Act
condition = ConversationService._build_filter_condition(
@ -668,9 +669,9 @@ class TestConversationServiceConversationalVariable:
mock_session_factory.create_session.return_value.__enter__.return_value = mock_session
last_variable = ConversationServiceTestDataFactory.create_conversation_variable_mock(
created_at=datetime.utcnow() - timedelta(hours=1)
created_at=naive_utc_now() - timedelta(hours=1)
)
variable = ConversationServiceTestDataFactory.create_conversation_variable_mock(created_at=datetime.utcnow())
variable = ConversationServiceTestDataFactory.create_conversation_variable_mock(created_at=naive_utc_now())
mock_session.scalar.return_value = last_variable
mock_session.scalars.return_value.all.return_value = [variable]

View File

@ -15,6 +15,7 @@ from graphon.nodes.human_input.entities import (
UserAction,
)
from graphon.nodes.human_input.enums import FormInputType, HumanInputFormKind, HumanInputFormStatus
from libs.datetime_utils import naive_utc_now
from models.human_input import RecipientType
from services.human_input_service import (
Form,
@ -51,11 +52,11 @@ def sample_form_record():
inputs=[],
user_actions=[UserAction(id="submit", title="Submit")],
rendered_content="<p>hello</p>",
expiration_time=datetime.utcnow() + timedelta(hours=1),
expiration_time=naive_utc_now() + timedelta(hours=1),
),
rendered_content="<p>hello</p>",
created_at=datetime.utcnow(),
expiration_time=datetime.utcnow() + timedelta(hours=1),
created_at=naive_utc_now(),
expiration_time=naive_utc_now() + timedelta(hours=1),
status=HumanInputFormStatus.WAITING,
selected_action_id=None,
submitted_data=None,
@ -101,8 +102,8 @@ def test_ensure_form_active_respects_global_timeout(monkeypatch, sample_form_rec
service = HumanInputService(session_factory)
expired_record = dataclasses.replace(
sample_form_record,
created_at=datetime.utcnow() - timedelta(hours=2),
expiration_time=datetime.utcnow() + timedelta(hours=2),
created_at=naive_utc_now() - timedelta(hours=2),
expiration_time=naive_utc_now() + timedelta(hours=2),
)
monkeypatch.setattr(human_input_service_module.dify_config, "HUMAN_INPUT_GLOBAL_TIMEOUT_SECONDS", 3600)
@ -391,7 +392,7 @@ def test_ensure_form_active_errors(sample_form_record, mock_session_factory):
service = HumanInputService(session_factory)
# Submitted
submitted_record = dataclasses.replace(sample_form_record, submitted_at=datetime.utcnow())
submitted_record = dataclasses.replace(sample_form_record, submitted_at=naive_utc_now())
with pytest.raises(human_input_service_module.FormSubmittedError):
service.ensure_form_active(Form(submitted_record))
@ -402,7 +403,7 @@ def test_ensure_form_active_errors(sample_form_record, mock_session_factory):
# Expired time
expired_time_record = dataclasses.replace(
sample_form_record, expiration_time=datetime.utcnow() - timedelta(minutes=1)
sample_form_record, expiration_time=naive_utc_now() - timedelta(minutes=1)
)
with pytest.raises(FormExpiredError):
service.ensure_form_active(Form(expired_time_record))
@ -411,7 +412,7 @@ def test_ensure_form_active_errors(sample_form_record, mock_session_factory):
def test_ensure_not_submitted_raises(sample_form_record, mock_session_factory):
session_factory, _ = mock_session_factory
service = HumanInputService(session_factory)
submitted_record = dataclasses.replace(sample_form_record, submitted_at=datetime.utcnow())
submitted_record = dataclasses.replace(sample_form_record, submitted_at=naive_utc_now())
with pytest.raises(human_input_service_module.FormSubmittedError):
service._ensure_not_submitted(Form(submitted_record))

View File

@ -58,10 +58,11 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
}, 1000)
}
const sendEmail = async (email: string, token?: string) => {
const sendEmail = async (email: string, isOrigin: boolean, token?: string) => {
try {
const res = await sendVerifyCode({
email,
phase: isOrigin ? 'old_email' : 'new_email',
token,
})
startCount()
@ -105,6 +106,7 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
const sendCodeToOriginEmail = async () => {
await sendEmail(
email,
true,
)
setStep(STEP.verifyOrigin)
}
@ -160,6 +162,7 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
}
await sendEmail(
mail,
false,
stepToken,
)
setStep(STEP.verifyNew)

View File

@ -372,7 +372,7 @@ export const submitDeleteAccountFeedback = (body: { feedback: string, email: str
export const getDocDownloadUrl = (doc_name: string): Promise<{ url: string }> =>
get<{ url: string }>('/compliance/download', { params: { doc_name } }, { silent: true })
export const sendVerifyCode = (body: { email: string, token?: string }): Promise<CommonResponse & { data: string }> =>
export const sendVerifyCode = (body: { email: string, phase: string, token?: string }): Promise<CommonResponse & { data: string }> =>
post<CommonResponse & { data: string }>('/account/change-email', { body })
export const verifyEmail = (body: { email: string, code: string, token: string }): Promise<CommonResponse & { is_valid: boolean, email: string, token: string }> =>