Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.3.0"
__version__ = "0.3.1"
1 change: 1 addition & 0 deletions src/api_ptt.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ def login() -> Dict[str, Any]:
- 'NO_SUCH_POST': 在看板中找不到文章 AID 或 Index。
- 'NO_PERMISSION': 沒有權限。
- 'LOGIN_FAILED': 登入失敗。
- 'TWO_FACTOR_AUTH_REQUIRED': 需要二階段驗證,請先用一般 BBS 客戶端從這台機器手動登入完成驗證。
- 'WRONG_CREDENTIALS': 帳號或密碼錯誤。
- 'CANT_RESPONSE': 已結案並標記, 不得回應。
- 'NO_FAST_COMMENT': 推文間隔太短。
Expand Down
45 changes: 45 additions & 0 deletions src/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,51 @@ def test_parameter_error_maps_to_parameter_error_code():
sys.modules[m] = mod


def test_two_factor_auth_required_maps_to_dedicated_code():
src_dir = os.path.dirname(os.path.abspath(__file__))
if src_dir not in sys.path:
sys.path.insert(0, src_dir)
snapshot = {m: sys.modules.get(m) for m in _SHARED}
try:
if not getattr(sys.modules.get("PyPtt"), "__file__", None):
for m in _SHARED:
sys.modules.pop(m, None)
import PyPtt

if not hasattr(PyPtt, "TwoFactorAuthRequired"):
pytest.skip("real PyPtt (>=2.3.6) not installed")
import utils

# PyPtt 例外的 message 來自 i18n,建構前得先 init,否則
# 存取 .message 會噴 AttributeError(PyPtt.API 平常會幫你做這步)。
import PyPtt.data_type as pyptt_data_type
import PyPtt.i18n as pyptt_i18n

pyptt_i18n.init(pyptt_data_type.Language.MANDARIN)

# TwoFactorAuthRequired 繼承 LoginError,須確認沒被父類搶先攔截,
# 且保留 PyPtt 的原始訊息(不是被換成「登入失敗」)。
exc = PyPtt.TwoFactorAuthRequired()
result = utils._handle_ptt_exception(exc, {})
assert result["success"] is False
assert result["code"] == "TWO_FACTOR_AUTH_REQUIRED"
assert result["message"] == str(exc)
assert result["message"] != "登入失敗"

# 回歸保護:LoginError 本身仍要維持 LOGIN_FAILED,不能被上面的分支搶走。
login_error_result = utils._handle_ptt_exception(PyPtt.LoginError(), {})
assert login_error_result["code"] == "LOGIN_FAILED"
assert login_error_result["message"] == "登入失敗"
finally:
for m in _SHARED:
mod = snapshot[m]
if mod is None:
sys.modules.pop(m, None)
else:
sys.modules[m] = mod


if __name__ == "__main__":
test_parameter_error_maps_to_parameter_error_code()
test_two_factor_auth_required_maps_to_dedicated_code()
print("OK")
14 changes: 14 additions & 0 deletions src/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,20 @@ def _handle_ptt_exception(e: Exception, kwargs: Dict[str, Any]) -> Dict[str, Any
),
}

# TwoFactorAuthRequired 繼承 LoginError,若放進上面的 dict 迴圈會被
# LoginError 先攔截、蓋成泛用的「登入失敗」。獨立分支、放在迴圈之前,
# 保留 PyPtt 的原始訊息(含如何完成 2FA 驗證的指引)。getattr 是為了
# 相容裝了舊版 PyPtt(<2.3.6,還沒有這個例外類別)的環境。
two_factor_auth_required = getattr(PyPtt, "TwoFactorAuthRequired", None)
if two_factor_auth_required is not None and isinstance(
e, two_factor_auth_required
):
return {
"success": False,
"message": str(e),
"code": "TWO_FACTOR_AUTH_REQUIRED",
}

for exc_type, (message_format, code) in EXCEPTION_MAPPING.items():
if isinstance(e, exc_type):
message = (
Expand Down
Loading