PyPtt 2.1.6__tar.gz → 2.1.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. {pyptt-2.1.6 → pyptt-2.1.7}/PKG-INFO +1 -1
  2. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/__init__.py +1 -1
  3. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/_api_del_post.py +107 -2
  4. pyptt-2.1.7/PyPtt/ssl_config.py +24 -0
  5. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt.egg-info/PKG-INFO +1 -1
  6. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt.egg-info/SOURCES.txt +1 -0
  7. pyptt-2.1.7/tests/test_del_post_unit.py +78 -0
  8. pyptt-2.1.6/PyPtt/ssl_config.py +0 -24
  9. {pyptt-2.1.6 → pyptt-2.1.7}/LICENSE +0 -0
  10. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/PTT.py +0 -0
  11. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/_api_bucket.py +0 -0
  12. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/_api_call_status.py +0 -0
  13. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/_api_change_pw.py +0 -0
  14. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/_api_comment.py +0 -0
  15. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/_api_get_board_info.py +0 -0
  16. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/_api_get_board_list.py +0 -0
  17. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/_api_get_bottom_post_list.py +0 -0
  18. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/_api_get_favourite_board.py +0 -0
  19. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/_api_get_newest_index.py +0 -0
  20. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/_api_get_post.py +0 -0
  21. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/_api_get_post_index.py +0 -0
  22. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/_api_get_post_list.py +0 -0
  23. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/_api_get_time.py +0 -0
  24. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/_api_get_user.py +0 -0
  25. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/_api_get_waterball.py +0 -0
  26. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/_api_give_money.py +0 -0
  27. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/_api_has_new_mail.py +0 -0
  28. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/_api_loginout.py +0 -0
  29. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/_api_mail.py +0 -0
  30. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/_api_mark_post.py +0 -0
  31. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/_api_post.py +0 -0
  32. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/_api_reply_post.py +0 -0
  33. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/_api_search_user.py +0 -0
  34. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/_api_set_board_title.py +0 -0
  35. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/_api_util.py +0 -0
  36. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/api_server.py +0 -0
  37. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/check_value.py +0 -0
  38. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/command.py +0 -0
  39. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/config.py +0 -0
  40. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/connect_core.py +0 -0
  41. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/data_type.py +0 -0
  42. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/exceptions.py +0 -0
  43. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/i18n.py +0 -0
  44. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/lang_en_US.py +0 -0
  45. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/lang_zh_TW.py +0 -0
  46. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/lib_util.py +0 -0
  47. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/log.py +0 -0
  48. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/screens.py +0 -0
  49. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt/service.py +0 -0
  50. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt.egg-info/dependency_links.txt +0 -0
  51. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt.egg-info/requires.txt +0 -0
  52. {pyptt-2.1.6 → pyptt-2.1.7}/PyPtt.egg-info/top_level.txt +0 -0
  53. {pyptt-2.1.6 → pyptt-2.1.7}/README.md +0 -0
  54. {pyptt-2.1.6 → pyptt-2.1.7}/pyproject.toml +0 -0
  55. {pyptt-2.1.6 → pyptt-2.1.7}/setup.cfg +0 -0
  56. {pyptt-2.1.6 → pyptt-2.1.7}/setup.py +0 -0
  57. {pyptt-2.1.6 → pyptt-2.1.7}/tests/test_bucket.py +0 -0
  58. {pyptt-2.1.6 → pyptt-2.1.7}/tests/test_change_pw.py +0 -0
  59. {pyptt-2.1.6 → pyptt-2.1.7}/tests/test_comment.py +0 -0
  60. {pyptt-2.1.6 → pyptt-2.1.7}/tests/test_del_post.py +0 -0
  61. {pyptt-2.1.6 → pyptt-2.1.7}/tests/test_exceptions.py +0 -0
  62. {pyptt-2.1.6 → pyptt-2.1.7}/tests/test_fast_post.py +0 -0
  63. {pyptt-2.1.6 → pyptt-2.1.7}/tests/test_get_board_info.py +0 -0
  64. {pyptt-2.1.6 → pyptt-2.1.7}/tests/test_get_board_list.py +0 -0
  65. {pyptt-2.1.6 → pyptt-2.1.7}/tests/test_get_bottom_post_list.py +0 -0
  66. {pyptt-2.1.6 → pyptt-2.1.7}/tests/test_get_content_formula.py +0 -0
  67. {pyptt-2.1.6 → pyptt-2.1.7}/tests/test_get_favourite_boards.py +0 -0
  68. {pyptt-2.1.6 → pyptt-2.1.7}/tests/test_get_mail.py +0 -0
  69. {pyptt-2.1.6 → pyptt-2.1.7}/tests/test_get_newest_index.py +0 -0
  70. {pyptt-2.1.6 → pyptt-2.1.7}/tests/test_get_post.py +0 -0
  71. {pyptt-2.1.6 → pyptt-2.1.7}/tests/test_get_post_list.py +0 -0
  72. {pyptt-2.1.6 → pyptt-2.1.7}/tests/test_get_post_list_parser.py +0 -0
  73. {pyptt-2.1.6 → pyptt-2.1.7}/tests/test_get_post_parser.py +0 -0
  74. {pyptt-2.1.6 → pyptt-2.1.7}/tests/test_get_time.py +0 -0
  75. {pyptt-2.1.6 → pyptt-2.1.7}/tests/test_get_user.py +0 -0
  76. {pyptt-2.1.6 → pyptt-2.1.7}/tests/test_get_waterball.py +0 -0
  77. {pyptt-2.1.6 → pyptt-2.1.7}/tests/test_give_money.py +0 -0
  78. {pyptt-2.1.6 → pyptt-2.1.7}/tests/test_i18n.py +0 -0
  79. {pyptt-2.1.6 → pyptt-2.1.7}/tests/test_init.py +0 -0
  80. {pyptt-2.1.6 → pyptt-2.1.7}/tests/test_logger.py +0 -0
  81. {pyptt-2.1.6 → pyptt-2.1.7}/tests/test_login_logout.py +0 -0
  82. {pyptt-2.1.6 → pyptt-2.1.7}/tests/test_mail_send_and_del.py +0 -0
  83. {pyptt-2.1.6 → pyptt-2.1.7}/tests/test_mark_post.py +0 -0
  84. {pyptt-2.1.6 → pyptt-2.1.7}/tests/test_parse_query_post.py +0 -0
  85. {pyptt-2.1.6 → pyptt-2.1.7}/tests/test_post.py +0 -0
  86. {pyptt-2.1.6 → pyptt-2.1.7}/tests/test_pyte_parity.py +0 -0
  87. {pyptt-2.1.6 → pyptt-2.1.7}/tests/test_reply_post.py +0 -0
  88. {pyptt-2.1.6 → pyptt-2.1.7}/tests/test_search_user.py +0 -0
  89. {pyptt-2.1.6 → pyptt-2.1.7}/tests/test_service.py +0 -0
  90. {pyptt-2.1.6 → pyptt-2.1.7}/tests/test_set_board_title.py +0 -0
  91. {pyptt-2.1.6 → pyptt-2.1.7}/tests/test_vt100_parser.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPtt
3
- Version: 2.1.6
3
+ Version: 2.1.7
4
4
  Summary: PyPtt github: https://github.com/PyPtt/PyPtt
5
5
  Author-email: CodingMan <pttcodingman@gmail.com>
6
6
  License: GNU Lesser General Public License v3 (LGPLv3)
@@ -1,4 +1,4 @@
1
- __version__ = '2.1.6'
1
+ __version__ = '2.1.7'
2
2
  __author__ = 'CodingMan'
3
3
  __email__ = 'pttcodingman@gmail.com'
4
4
 
@@ -17,6 +17,45 @@ from . import screens
17
17
  # character), not in Python characters.
18
18
  _BAD_POST_REASON_MAX_BYTES = 50
19
19
 
20
+ # --- del_post's `reason` field (annotates the title shown after a
21
+ # moderator deletes another user's post) ---
22
+ #
23
+ # pttbbs pre-fills this prompt with "(已被<moderator id>刪除) <author id>"
24
+ # (pttbbs mbbsd/bbs.c, del_post()'s SAFE_ARTICLE_DELETE branch) in a
25
+ # `char reason[PROPER_TITLE_LEN]` buffer; PROPER_TITLE_LEN is 42
26
+ # (pttbbs include/common.h), so the whole title -- prefix *and* PyPtt's
27
+ # reason together -- is capped at 41 Big5 bytes, not python characters.
28
+ # PyPtt answers the prompt with Ctrl-E (jump to the end of the prefilled
29
+ # text) plus a leading space plus `reason`, so the byte budget actually
30
+ # left for `reason` depends on how long the moderator's and the post
31
+ # author's PTT IDs are -- there is no single safe constant independent of
32
+ # who is deleting what, unlike bad_post_reason above.
33
+ #
34
+ # Verified live against a local pttbbs (bbsdocker/imageptt rebuilt with
35
+ # SAFE_ARTICLE_DELETE enabled -- the stock image ships with that feature
36
+ # #define'd out and never offers this prompt at all):
37
+ # - exactly at the budget: reason is accepted in full, title not
38
+ # truncated.
39
+ # - 1 byte over budget, ASCII-only overflow: pttbbs silently truncates
40
+ # to fit (title is just missing the last character).
41
+ # - 1 byte over budget, where the overflow splits a double-byte Big5
42
+ # glyph in half: pttbbs's line editor corrupts and del_post() hangs
43
+ # until connect_core's screen_timeout, returning neither success nor
44
+ # a clean failure -- this is the "刪文失敗" a caller with a too-long
45
+ # reason actually experiences.
46
+ # PyPtt must reject before sending in both cases.
47
+ _REASON_TITLE_BUFFER_BYTES = 41 # PROPER_TITLE_LEN(42) - 1 byte reserved for the NUL terminator
48
+ _REASON_PREFIX_FIXED_BYTES = 13 # Big5 bytes of "(已被刪除) <>", the literal part of "(已被%s刪除) <%s>"
49
+ # A rough, moderator/author-id-independent ceiling: the most bytes `reason`
50
+ # could ever fit under, i.e. the budget if both PTT ids were the shortest
51
+ # possible (1 byte each). Real ids can only be longer than that, which only
52
+ # shrinks the true budget further -- so a reason within this ceiling is not
53
+ # guaranteed to fit any *particular* id pair, but a reason over it can never
54
+ # fit *any* id pair. That asymmetry is what makes it safe to use as an
55
+ # early, id-independent guard: it will never reject a reason that might be
56
+ # legal once we know the real ids (checked later by _check_reason_budget).
57
+ _REASON_MAX_POSSIBLE_BYTES = _REASON_TITLE_BUFFER_BYTES - _REASON_PREFIX_FIXED_BYTES - 1 - 1 - 1
58
+
20
59
 
21
60
  def _check_no_control_chars(name: str, value: str) -> None:
22
61
  """Terminal command injection guard: none of PyPtt's del_post text
@@ -40,6 +79,55 @@ def _check_bad_post_reason(bad_post_reason: str) -> None:
40
79
  f'bad_post_reason must not exceed {_BAD_POST_REASON_MAX_BYTES} bytes')
41
80
 
42
81
 
82
+ def _check_reason(reason: str) -> None:
83
+ """Early, post_info-independent guard for `reason`: control chars,
84
+ Big5-encodability, and a rough length ceiling that can never fit any
85
+ moderator/author id pair (_REASON_MAX_POSSIBLE_BYTES). This alone does
86
+ NOT guarantee `reason` fits the *actual* ids involved -- that exact,
87
+ id-dependent budget is enforced later by _check_reason_budget, once
88
+ post_info is available. Keeping this one id-independent lets del_post()
89
+ reject an obviously-broken reason (control chars, non-Big5, absurdly
90
+ long) the same way regardless of whether the target post still exists."""
91
+ _check_no_control_chars('reason', reason)
92
+ try:
93
+ encoded = reason.encode('big5uao')
94
+ except UnicodeEncodeError as e:
95
+ raise exceptions.ParameterError(
96
+ f'reason contains a character that cannot be encoded in Big5: {e}') from e
97
+ if len(encoded) > _REASON_MAX_POSSIBLE_BYTES:
98
+ raise exceptions.ParameterError(
99
+ f'reason must not exceed {_REASON_MAX_POSSIBLE_BYTES} bytes '
100
+ f'(pttbbs caps the deleted-post title at {_REASON_TITLE_BUFFER_BYTES} bytes total; '
101
+ f'{_REASON_MAX_POSSIBLE_BYTES} bytes is the most any reason could ever fit, for the '
102
+ f'shortest possible moderator/author PTT ids -- the actual limit for this delete may '
103
+ f'be smaller)')
104
+
105
+
106
+ def _check_reason_budget(reason: str, moderator_id: str, author_id: str) -> None:
107
+ """Exact, post_info-dependent guard for `reason`: pttbbs pre-fills the
108
+ delete-title prompt with "(已被<moderator_id>刪除) <author_id>" inside the
109
+ fixed-size buffer described above, so the byte budget actually left for
110
+ `reason` depends on the two real PTT IDs involved in this delete --
111
+ unlike _check_bad_post_reason, this can't be a plain module-level
112
+ constant. Call this once post_info is available (see del_post());
113
+ _check_reason above already covers the id-independent checks (control
114
+ chars, Big5-encodability) so this only needs to re-derive the byte
115
+ count for the length comparison."""
116
+ try:
117
+ encoded = reason.encode('big5uao')
118
+ except UnicodeEncodeError as e:
119
+ raise exceptions.ParameterError(
120
+ f'reason contains a character that cannot be encoded in Big5: {e}') from e
121
+ prefix_bytes = _REASON_PREFIX_FIXED_BYTES + len(moderator_id) + len(author_id)
122
+ # -1 for the leading space PyPtt sends before reason (see del_post()).
123
+ max_reason_bytes = max(0, _REASON_TITLE_BUFFER_BYTES - prefix_bytes - 1)
124
+ if len(encoded) > max_reason_bytes:
125
+ raise exceptions.ParameterError(
126
+ f'reason must not exceed {max_reason_bytes} bytes for this moderator/author pair '
127
+ f'(pttbbs caps the deleted-post title at {_REASON_TITLE_BUFFER_BYTES} bytes total, '
128
+ f'and the prefix "(已被{moderator_id}刪除) <{author_id}>" already uses {prefix_bytes} of them)')
129
+
130
+
43
131
  def del_post(api, board: str, post_aid: Optional[str] = None, post_index: int = 0, reason: Optional[str] = None,
44
132
  bad_post_type: Optional[data_type.BadPostType] = None,
45
133
  bad_post_reason: Optional[str] = None) -> None:
@@ -74,9 +162,17 @@ def del_post(api, board: str, post_aid: Optional[str] = None, post_index: int =
74
162
  # --- Pure parameter validation (doesn't need post_info) happens here,
75
163
  # before the "already deleted" early-return below -- an invalid
76
164
  # combination of parameters must always fail the same way, regardless of
77
- # whether the post happens to already be gone. ---
165
+ # whether the post happens to already be gone. `reason` is checked in
166
+ # two stages for this reason: _check_reason here covers everything that
167
+ # doesn't depend on state (control chars, Big5-encodability, and a
168
+ # rough ceiling no id pair could ever exceed), so those failures are
169
+ # always reported the same way; the *exact* budget depends on the
170
+ # moderator's and the post author's real PTT IDs, so that part
171
+ # (_check_reason_budget) has to wait until post_info is available below
172
+ # -- alongside the other post_info-dependent reason check ("reason is
173
+ # only valid when...") a few lines down. ---
78
174
  if reason is not None:
79
- _check_no_control_chars('reason', reason)
175
+ _check_reason(reason)
80
176
 
81
177
  if bad_post_type == data_type.BadPostType.OTHER:
82
178
  if not bad_post_reason:
@@ -132,6 +228,15 @@ def del_post(api, board: str, post_aid: Optional[str] = None, post_index: int =
132
228
  log.logger.info(i18n.delete_post, '...', i18n.fail)
133
229
  raise exceptions.NoPermission(i18n.no_permission)
134
230
 
231
+ # Reaching here with reason still set means: not self-authored (checked
232
+ # above) and permitted (the check_author gate above didn't raise) --
233
+ # i.e. exactly the "moderator deleting someone else's post" case where
234
+ # PTT will actually offer the R加註理由 prompt. Validate reason's exact
235
+ # byte budget now, using the real moderator/author id pair, before any
236
+ # of the interactive delete flow below is sent.
237
+ if reason:
238
+ _check_reason_budget(reason, api.ptt_id, post_info[data_type.PostField.author])
239
+
135
240
  _api_util.goto_board(api, board)
136
241
 
137
242
  cmd_list = []
@@ -0,0 +1,24 @@
1
+ key = """-----BEGIN EC PARAMETERS-----
2
+ BggqhkjOPQMBBw==
3
+ -----END EC PARAMETERS-----
4
+ -----BEGIN EC PRIVATE KEY-----
5
+ MHcCAQEEIBl6n84gGhyoI6rh8jsDM6+MvbtX++1SctY8mkfAU/PqoAoGCCqGSM49
6
+ AwEHoUQDQgAEKrKMWokWd4Y6dFHFFr4pg9h340NKkekdCrYbxv1l2SUBUDkXUx28
7
+ fGqtXxYCuOsEQTeQrm0OmCq6U+aRXMvwaQ==
8
+ -----END EC PRIVATE KEY-----
9
+ """
10
+
11
+ cert = """-----BEGIN CERTIFICATE-----
12
+ MIIB6DCCAY8CFEXMlgDToWo6s00YgTOitW0BgNCdMAoGCCqGSM49BAMCMHcxCzAJ
13
+ BgNVBAYTAlRXMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0eTEVMBMGA1UE
14
+ CgwMT3JnYW5pemF0aW9uMRwwGgYDVQQLDBNPcmdhbml6YXRpb25hbCBVbml0MRQw
15
+ EgYDVQQDDAtDb21tb24gTmFtZTAeFw0yNjA3MTgwNDE0NTZaFw0zNjA3MTUwNDE0
16
+ NTZaMHcxCzAJBgNVBAYTAlRXMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0
17
+ eTEVMBMGA1UECgwMT3JnYW5pemF0aW9uMRwwGgYDVQQLDBNPcmdhbml6YXRpb25h
18
+ bCBVbml0MRQwEgYDVQQDDAtDb21tb24gTmFtZTBZMBMGByqGSM49AgEGCCqGSM49
19
+ AwEHA0IABCqyjFqJFneGOnRRxRa+KYPYd+NDSpHpHQq2G8b9ZdklAVA5F1MdvHxq
20
+ rV8WArjrBEE3kK5tDpgqulPmkVzL8GkwCgYIKoZIzj0EAwIDRwAwRAIgcfQdHGfL
21
+ m1j+zvm23ToVfbfGdlvcR4hXBQkWMQslAkICIEccC2jqieHYgaLRUgzoUVI/twm3
22
+ 5uAjX0ZWPxTPuNvA
23
+ -----END CERTIFICATE-----
24
+ """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPtt
3
- Version: 2.1.6
3
+ Version: 2.1.7
4
4
  Summary: PyPtt github: https://github.com/PyPtt/PyPtt
5
5
  Author-email: CodingMan <pttcodingman@gmail.com>
6
6
  License: GNU Lesser General Public License v3 (LGPLv3)
@@ -54,6 +54,7 @@ tests/test_bucket.py
54
54
  tests/test_change_pw.py
55
55
  tests/test_comment.py
56
56
  tests/test_del_post.py
57
+ tests/test_del_post_unit.py
57
58
  tests/test_exceptions.py
58
59
  tests/test_fast_post.py
59
60
  tests/test_get_board_info.py
@@ -0,0 +1,78 @@
1
+ """
2
+ No-network unit tests for the pure validation helpers in _api_del_post.py.
3
+ These call the module functions directly and don't need a live PTT session.
4
+ """
5
+ import pytest
6
+
7
+ import PyPtt
8
+ from PyPtt import _api_del_post
9
+
10
+
11
+ class TestCheckBadPostReason:
12
+ def test_within_limit_ok(self):
13
+ _api_del_post._check_bad_post_reason('測' * 25) # 50 bytes, exactly at the limit
14
+
15
+ def test_over_limit_raises(self):
16
+ with pytest.raises(PyPtt.exceptions.ParameterError):
17
+ _api_del_post._check_bad_post_reason('測' * 25 + 'x') # 51 bytes
18
+
19
+ def test_control_char_raises(self):
20
+ with pytest.raises(PyPtt.exceptions.ParameterError):
21
+ _api_del_post._check_bad_post_reason('bad\x07reason')
22
+
23
+
24
+ class TestCheckReasonStatic:
25
+ """`_check_reason` is the early, post_info-independent guard: control
26
+ chars, Big5-encodability, and a rough ceiling (_REASON_MAX_POSSIBLE_BYTES
27
+ == 41 - 13 - 1 - 1 - 1 == 25 bytes) that assumes the shortest possible
28
+ (1-byte) moderator/author PTT ids -- the most bytes a reason could ever
29
+ fit under any real id pair. It therefore never rejects a reason that
30
+ might be legal once the real ids are known; that exact, smaller,
31
+ id-dependent budget is _check_reason_budget's job (see below)."""
32
+
33
+ def test_within_rough_ceiling_ok(self):
34
+ _api_del_post._check_reason('測' * 12 + 'x') # 25 bytes, exactly the rough ceiling
35
+
36
+ def test_over_rough_ceiling_raises(self):
37
+ with pytest.raises(PyPtt.exceptions.ParameterError):
38
+ _api_del_post._check_reason('測' * 12 + 'xx') # 26 bytes
39
+
40
+ def test_control_char_raises(self):
41
+ with pytest.raises(PyPtt.exceptions.ParameterError):
42
+ _api_del_post._check_reason('bad\x07reason')
43
+
44
+ def test_non_big5_raises(self):
45
+ with pytest.raises(PyPtt.exceptions.ParameterError):
46
+ _api_del_post._check_reason('emoji not in big5uao \U0001F600')
47
+
48
+
49
+ class TestCheckReasonBudget:
50
+ """`_check_reason_budget` is the exact, post_info-dependent guard: reason
51
+ gets appended, after a leading space, to pttbbs's prefilled
52
+ "(已被<moderator_id>刪除) <author_id>" title inside a 41-Big5-byte buffer
53
+ (_REASON_TITLE_BUFFER_BYTES). Budget for a 9-char/9-char id pair (e.g.
54
+ pypttbot1 deleting pypttbot2's post, as used in the local imageptt
55
+ integration tests) is 41 - 13 - 9 - 9 - 1 == 9 bytes -- confirmed live
56
+ against a local pttbbs rebuilt with SAFE_ARTICLE_DELETE enabled."""
57
+
58
+ MOD_ID = 'pypttbot1'
59
+ AUTHOR_ID = 'pypttbot2'
60
+
61
+ def test_within_limit_ok(self):
62
+ _api_del_post._check_reason_budget('測測測測x', self.MOD_ID, self.AUTHOR_ID) # 9 bytes, exactly at the limit
63
+
64
+ def test_over_limit_by_one_byte_raises(self):
65
+ with pytest.raises(PyPtt.exceptions.ParameterError):
66
+ _api_del_post._check_reason_budget('測測測測測', self.MOD_ID, self.AUTHOR_ID) # 10 bytes
67
+
68
+ def test_over_limit_ascii_raises(self):
69
+ with pytest.raises(PyPtt.exceptions.ParameterError):
70
+ _api_del_post._check_reason_budget('x' * 10, self.MOD_ID, self.AUTHOR_ID)
71
+
72
+ def test_budget_shrinks_with_longer_ids(self):
73
+ """A 12-char/12-char id pair (PTT's real max, IDLEN==12) leaves only
74
+ 3 bytes of budget -- a reason that fits the 9/9 pair above no
75
+ longer fits."""
76
+ with pytest.raises(PyPtt.exceptions.ParameterError):
77
+ _api_del_post._check_reason_budget('測測測測x', 'a' * 12, 'b' * 12)
78
+ _api_del_post._check_reason_budget('x', 'a' * 12, 'b' * 12) # 1 byte still fits the 3-byte budget
@@ -1,24 +0,0 @@
1
- key = """-----BEGIN EC PARAMETERS-----
2
- BggqhkjOPQMBBw==
3
- -----END EC PARAMETERS-----
4
- -----BEGIN EC PRIVATE KEY-----
5
- MHcCAQEEIFGaW2sWvlg2R4UP5e4CNg4hpmJ2+AH1p+ZDaE2L4ZfzoAoGCCqGSM49
6
- AwEHoUQDQgAEMNlx7QJJjC6RbB9mPHUMmGK0uwUaTJ53DMzaFb4/DD1dhYpOvKcu
7
- qZaNpQbvNCaBN5Zz+uJm78zyJXxXl6uTgw==
8
- -----END EC PRIVATE KEY-----
9
- """
10
-
11
- cert = """-----BEGIN CERTIFICATE-----
12
- MIIB6TCCAY8CFAKFPO04kcFNRJPUCM1H8KteAaEwMAoGCCqGSM49BAMCMHcxCzAJ
13
- BgNVBAYTAlRXMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0eTEVMBMGA1UE
14
- CgwMT3JnYW5pemF0aW9uMRwwGgYDVQQLDBNPcmdhbml6YXRpb25hbCBVbml0MRQw
15
- EgYDVQQDDAtDb21tb24gTmFtZTAeFw0yNjA3MTgwMjMyMjdaFw0zNjA3MTUwMjMy
16
- MjdaMHcxCzAJBgNVBAYTAlRXMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0
17
- eTEVMBMGA1UECgwMT3JnYW5pemF0aW9uMRwwGgYDVQQLDBNPcmdhbml6YXRpb25h
18
- bCBVbml0MRQwEgYDVQQDDAtDb21tb24gTmFtZTBZMBMGByqGSM49AgEGCCqGSM49
19
- AwEHA0IABDDZce0CSYwukWwfZjx1DJhitLsFGkyedwzM2hW+Pww9XYWKTrynLqmW
20
- jaUG7zQmgTeWc/riZu/M8iV8V5erk4MwCgYIKoZIzj0EAwIDSAAwRQIgMOY/7UHn
21
- waVAljYZrhuUwCQxcibRzy006NsenvmTLLwCIQDuQAAsbdtAfswclkrDHBtoCeCl
22
- VphR8OhIzsjJE7SKjA==
23
- -----END CERTIFICATE-----
24
- """
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes