mailsocket 0.1.2__tar.gz → 0.1.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mailsocket
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Official Python client for the mailsocket REST API — one line to wait for an OTP.
5
5
  Author: mailsocket
6
6
  License-Expression: MIT
@@ -116,5 +116,11 @@ cd python
116
116
  python -m pytest -q
117
117
  ```
118
118
 
119
- Tests are fully offline — they monkeypatch `urllib.request.urlopen` with a
120
- scripted responder, so nothing touches the live API.
119
+ Tests make no external or live-API requests. Most monkeypatch
120
+ `urllib.request.urlopen` with a scripted responder, and a few start a real local
121
+ `http.server` on `127.0.0.1` to check actual request behaviour, e.g. that an
122
+ invalid id never reaches the network.
123
+
124
+ ## Changelog
125
+
126
+ - **0.1.3** — `_seg` now rejects an empty id or a bare `.`/`..` id with a `MailsocketError` (`code="invalid_id"`) before any request is made, matching the TypeScript SDK's contract.
@@ -90,5 +90,11 @@ cd python
90
90
  python -m pytest -q
91
91
  ```
92
92
 
93
- Tests are fully offline — they monkeypatch `urllib.request.urlopen` with a
94
- scripted responder, so nothing touches the live API.
93
+ Tests make no external or live-API requests. Most monkeypatch
94
+ `urllib.request.urlopen` with a scripted responder, and a few start a real local
95
+ `http.server` on `127.0.0.1` to check actual request behaviour, e.g. that an
96
+ invalid id never reaches the network.
97
+
98
+ ## Changelog
99
+
100
+ - **0.1.3** — `_seg` now rejects an empty id or a bare `.`/`..` id with a `MailsocketError` (`code="invalid_id"`) before any request is made, matching the TypeScript SDK's contract.
@@ -3,7 +3,7 @@
3
3
  from .client import Client, Page, WaitResult
4
4
  from .errors import AuthError, MailsocketError, NotFound, RateLimited, WaitTimeout
5
5
 
6
- __version__ = "0.1.2"
6
+ __version__ = "0.1.3"
7
7
 
8
8
  __all__ = [
9
9
  "Client",
@@ -32,13 +32,19 @@ def _sleep(seconds: float) -> None:
32
32
  def _seg(value) -> str:
33
33
  """Percent-encode an id as ONE path segment (``/``, ``?``, ``#``, ``..`` safe).
34
34
 
35
- ``quote`` leaves ``.`` alone (unreserved), so a bare ``.``/``..`` segment
36
- is additionally encoded as ``%2E`` so no proxy can treat it as traversal.
35
+ An empty id and the bare dot segments ``.`` / ``..`` are rejected with a
36
+ :class:`MailsocketError` before any request is made (same contract as the
37
+ TypeScript SDK's ``seg``): urllib's request machinery would otherwise let
38
+ them collapse into a different path than the caller intended. No real id
39
+ is ever one of these.
37
40
  """
38
- encoded = urllib.parse.quote(str(value), safe="")
39
- if encoded in (".", ".."):
40
- encoded = encoded.replace(".", "%2E")
41
- return encoded
41
+ raw = str(value)
42
+ if raw in ("", ".", ".."):
43
+ raise MailsocketError(
44
+ f"Invalid id {raw!r}: ids must be non-empty and not \".\" or \"..\".",
45
+ code="invalid_id",
46
+ )
47
+ return urllib.parse.quote(raw, safe="")
42
48
 
43
49
 
44
50
  class Page:
@@ -282,7 +288,7 @@ class Client:
282
288
  url += "?" + urllib.parse.urlencode(params, doseq=True)
283
289
  headers = {
284
290
  "Accept": "application/json",
285
- "User-Agent": "mailsocket-python/0.1.2",
291
+ "User-Agent": "mailsocket-python/0.1.3",
286
292
  }
287
293
  headers.update(self._extra_headers)
288
294
  # Set last so nothing in extra_headers can ever replace it.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mailsocket
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Official Python client for the mailsocket REST API — one line to wait for an OTP.
5
5
  Author: mailsocket
6
6
  License-Expression: MIT
@@ -116,5 +116,11 @@ cd python
116
116
  python -m pytest -q
117
117
  ```
118
118
 
119
- Tests are fully offline — they monkeypatch `urllib.request.urlopen` with a
120
- scripted responder, so nothing touches the live API.
119
+ Tests make no external or live-API requests. Most monkeypatch
120
+ `urllib.request.urlopen` with a scripted responder, and a few start a real local
121
+ `http.server` on `127.0.0.1` to check actual request behaviour, e.g. that an
122
+ invalid id never reaches the network.
123
+
124
+ ## Changelog
125
+
126
+ - **0.1.3** — `_seg` now rejects an empty id or a bare `.`/`..` id with a `MailsocketError` (`code="invalid_id"`) before any request is made, matching the TypeScript SDK's contract.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mailsocket"
7
- version = "0.1.2"
7
+ version = "0.1.3"
8
8
  description = "Official Python client for the mailsocket REST API — one line to wait for an OTP."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -203,8 +203,6 @@ HOSTILE_IDS = [
203
203
  ("in?x=1", "in%3Fx%3D1"),
204
204
  ("in#frag", "in%23frag"),
205
205
  ("../../admin", "..%2F..%2Fadmin"),
206
- ("..", "%2E%2E"),
207
- (".", "%2E"),
208
206
  ("a/b", "a%2Fb"),
209
207
  ("sp ce&%", "sp%20ce%26%25"),
210
208
  ]
@@ -437,18 +435,88 @@ def test_error_types_are_mailsocket_subclasses():
437
435
  assert issubclass(WaitTimeout, MailsocketError)
438
436
 
439
437
 
438
+ # -- F2: _seg rejects "" / "." / ".." before any request (parity with TS) -----
439
+
440
+
441
+ def test_seg_rejects_empty_and_dot_segments():
442
+ for raw in ("", ".", ".."):
443
+ with pytest.raises(MailsocketError) as exc_info:
444
+ mailsocket.client._seg(raw)
445
+ assert exc_info.value.code == "invalid_id"
446
+
447
+
448
+ INVALID_IDS = ["", ".", ".."]
449
+
450
+
451
+ @pytest.mark.parametrize("raw", INVALID_IDS)
452
+ def test_invalid_id_rejected_before_any_request_hits_real_server(raw):
453
+ """Nothing reaches the network: a real local http.server never sees a request."""
454
+ import http.server
455
+ import threading
456
+
457
+ seen = []
458
+
459
+ class Handler(http.server.BaseHTTPRequestHandler):
460
+ def _record(self):
461
+ seen.append(self.path)
462
+ self.send_response(200)
463
+ self.send_header("Content-Type", "application/json")
464
+ self.end_headers()
465
+ self.wfile.write(json.dumps({"data": {"id": "x"}}).encode())
466
+
467
+ def do_GET(self):
468
+ self._record()
469
+
470
+ def do_DELETE(self):
471
+ self._record()
472
+
473
+ def do_POST(self):
474
+ self._record()
475
+
476
+ def log_message(self, format, *args):
477
+ pass # keep test output clean
478
+
479
+ server = http.server.HTTPServer(("127.0.0.1", 0), Handler)
480
+ thread = threading.Thread(target=server.serve_forever, daemon=True)
481
+ thread.start()
482
+ try:
483
+ base_url = f"http://127.0.0.1:{server.server_port}/api/v1"
484
+ client = Client("ms_live_test1234567890", base_url=base_url)
485
+
486
+ calls = [
487
+ lambda: client.get_inbox(raw),
488
+ lambda: client.delete_inbox(raw),
489
+ lambda: client.list_messages(raw),
490
+ lambda: client.get_latest(raw),
491
+ lambda: client.get_message(raw),
492
+ lambda: client.wait_for_otp(raw, timeout=5),
493
+ lambda: client.wait_for_link(raw, timeout=5),
494
+ lambda: client.wait(raw, timeout=5),
495
+ ]
496
+ for call in calls:
497
+ with pytest.raises(MailsocketError) as exc_info:
498
+ call()
499
+ assert exc_info.value.code == "invalid_id"
500
+ assert not isinstance(exc_info.value, WaitTimeout)
501
+
502
+ assert seen == [] # nothing ever reached the network
503
+ finally:
504
+ server.shutdown()
505
+ thread.join(timeout=5)
506
+
507
+
440
508
  def test_version_exported():
441
509
  assert hasattr(mailsocket, "__version__")
442
510
 
443
511
 
444
512
  def test_metadata_consistency_across_files():
445
- """0.1.2 must match everywhere: pyproject, __version__, user agent."""
513
+ """0.1.3 must match everywhere: pyproject, __version__, user agent."""
446
514
  import re
447
515
  import tomllib
448
516
  from pathlib import Path
449
517
 
450
518
  root = Path(__file__).resolve().parents[1]
451
- expected = "0.1.2"
519
+ expected = "0.1.3"
452
520
 
453
521
  pyproject = tomllib.loads((root / "pyproject.toml").read_text())
454
522
  assert pyproject["project"]["version"] == expected
File without changes
File without changes