memwal 0.1.8.dev2__tar.gz → 0.1.9__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.
- {memwal-0.1.8.dev2 → memwal-0.1.9}/CHANGELOG.md +9 -0
- {memwal-0.1.8.dev2 → memwal-0.1.9}/PKG-INFO +1 -1
- {memwal-0.1.8.dev2 → memwal-0.1.9}/memwal/__init__.py +1 -1
- {memwal-0.1.8.dev2 → memwal-0.1.9}/memwal/client.py +77 -3
- {memwal-0.1.8.dev2 → memwal-0.1.9}/memwal/types.py +11 -7
- {memwal-0.1.8.dev2 → memwal-0.1.9}/pyproject.toml +1 -1
- memwal-0.1.9/tests/test_auth_rejected_message.py +30 -0
- {memwal-0.1.8.dev2 → memwal-0.1.9}/tests/test_client.py +86 -0
- memwal-0.1.9/tests/test_normalize_server_url.py +96 -0
- memwal-0.1.8.dev2/tests/test_auth_rejected_message.py +0 -9
- {memwal-0.1.8.dev2 → memwal-0.1.9}/.gitignore +0 -0
- {memwal-0.1.8.dev2 → memwal-0.1.9}/README.md +0 -0
- {memwal-0.1.8.dev2 → memwal-0.1.9}/examples/.env.example +0 -0
- {memwal-0.1.8.dev2 → memwal-0.1.9}/examples/.gitignore +0 -0
- {memwal-0.1.8.dev2 → memwal-0.1.9}/examples/async_remember_demo.py +0 -0
- {memwal-0.1.8.dev2 → memwal-0.1.9}/examples/interactive_demo.py +0 -0
- {memwal-0.1.8.dev2 → memwal-0.1.9}/examples/verify_credentials.py +0 -0
- {memwal-0.1.8.dev2 → memwal-0.1.9}/memwal/compatibility.py +0 -0
- {memwal-0.1.8.dev2 → memwal-0.1.9}/memwal/middleware.py +0 -0
- {memwal-0.1.8.dev2 → memwal-0.1.9}/memwal/mock.py +0 -0
- {memwal-0.1.8.dev2 → memwal-0.1.9}/memwal/utils.py +0 -0
- {memwal-0.1.8.dev2 → memwal-0.1.9}/notebooks/walrus_memory_python_sdk.ipynb +0 -0
- {memwal-0.1.8.dev2 → memwal-0.1.9}/run_tests.py +0 -0
- {memwal-0.1.8.dev2 → memwal-0.1.9}/tests/__init__.py +0 -0
- {memwal-0.1.8.dev2 → memwal-0.1.9}/tests/test_env_presets.py +0 -0
- {memwal-0.1.8.dev2 → memwal-0.1.9}/tests/test_integration.py +0 -0
- {memwal-0.1.8.dev2 → memwal-0.1.9}/tests/test_middleware.py +0 -0
- {memwal-0.1.8.dev2 → memwal-0.1.9}/tests/test_mock.py +0 -0
- {memwal-0.1.8.dev2 → memwal-0.1.9}/tests/test_signing.py +0 -0
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# memwal
|
|
2
2
|
|
|
3
|
+
## 0.1.9
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- HTTP 503 with `x-auth-error: AUTH_UPSTREAM_UNAVAILABLE` is reported as a retryable credential-verification outage, not a sign-in failure. Other 503s keep the generic sanitized body.
|
|
8
|
+
- `remember_bulk_async` rejects an empty `items` list before the request and raises when the relayer returns a `job_ids` length that does not match the batch.
|
|
9
|
+
- restore `truncated` docs now match WALM-431 retryable semantics.
|
|
10
|
+
- Warn when `server_url` uses plaintext `http://` against a non-localhost host, matching the TypeScript SDK `normalizeServerUrl` guard. Localhost, `127.0.0.1`, `::1`, and `*.localhost` are exempt; invalid URLs are left for the HTTP client to surface. The warning logs only scheme, host, and port so URL userinfo is not written to logs.
|
|
11
|
+
|
|
3
12
|
## 0.1.8
|
|
4
13
|
|
|
5
14
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: memwal
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.9
|
|
4
4
|
Summary: Python SDK for Walrus Memory — Privacy-first AI memory with Ed25519 signing
|
|
5
5
|
Project-URL: Homepage, https://memory.walrus.xyz
|
|
6
6
|
Project-URL: Documentation, https://memory.walrus.xyz
|
|
@@ -35,6 +35,7 @@ import time
|
|
|
35
35
|
import uuid
|
|
36
36
|
from datetime import datetime, timezone
|
|
37
37
|
from typing import Any, Dict, List, Optional, Sequence, Tuple, TypeVar, Union
|
|
38
|
+
from urllib.parse import ParseResult, urlparse
|
|
38
39
|
|
|
39
40
|
import httpx
|
|
40
41
|
import nacl.signing
|
|
@@ -97,11 +98,62 @@ AUTH_REJECTED_MESSAGE = (
|
|
|
97
98
|
"and dashboard credentials. Full troubleshooting: "
|
|
98
99
|
"https://docs.wal.app/walrus-memory/troubleshooting/overview#401-auth_rejected-errors"
|
|
99
100
|
)
|
|
101
|
+
AUTH_UPSTREAM_UNAVAILABLE = "AUTH_UPSTREAM_UNAVAILABLE"
|
|
102
|
+
UPSTREAM_UNAVAILABLE_MESSAGE = (
|
|
103
|
+
"Walrus Memory temporarily cannot verify credentials (upstream unavailable). "
|
|
104
|
+
"Retry; this is not a sign-in failure."
|
|
105
|
+
)
|
|
100
106
|
|
|
101
107
|
|
|
102
108
|
logger = logging.getLogger("memwal")
|
|
103
109
|
|
|
104
110
|
|
|
111
|
+
def _server_url_for_log(parsed: ParseResult) -> str:
|
|
112
|
+
"""Scheme/host/port only — never userinfo, path, query, or fragment."""
|
|
113
|
+
|
|
114
|
+
host = parsed.hostname or ""
|
|
115
|
+
if ":" in host:
|
|
116
|
+
host = f"[{host}]"
|
|
117
|
+
if parsed.port is not None:
|
|
118
|
+
return f"{parsed.scheme}://{host}:{parsed.port}"
|
|
119
|
+
return f"{parsed.scheme}://{host}"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def normalize_server_url(url: str) -> str:
|
|
123
|
+
"""Strip a trailing slash and warn on plaintext HTTP to a remote host.
|
|
124
|
+
|
|
125
|
+
Ports the TypeScript ``normalizeServerUrl`` helper: localhost,
|
|
126
|
+
``127.0.0.1``, ``::1``, and ``*.localhost`` are exempt. Invalid URLs
|
|
127
|
+
are returned trimmed so the HTTP client can surface the error later.
|
|
128
|
+
|
|
129
|
+
The warning logs only scheme/host/port so URL userinfo (HTTPX
|
|
130
|
+
credentials) and other sensitive components are not written to logs.
|
|
131
|
+
The returned URL is otherwise unchanged and still used for transport.
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
trimmed = url.rstrip("/")
|
|
135
|
+
try:
|
|
136
|
+
parsed = urlparse(trimmed)
|
|
137
|
+
host = (parsed.hostname or "").lower()
|
|
138
|
+
is_local = (
|
|
139
|
+
host == "localhost"
|
|
140
|
+
or host == "127.0.0.1"
|
|
141
|
+
or host == "::1"
|
|
142
|
+
or host.endswith(".localhost")
|
|
143
|
+
)
|
|
144
|
+
if parsed.scheme == "http" and host and not is_local:
|
|
145
|
+
logger.warning(
|
|
146
|
+
'[memwal] serverUrl "%s" uses plaintext HTTP on a non-localhost host. '
|
|
147
|
+
"Signed requests and any bearer material will be visible to the network. "
|
|
148
|
+
"Use https:// in production.",
|
|
149
|
+
_server_url_for_log(parsed),
|
|
150
|
+
)
|
|
151
|
+
except ValueError:
|
|
152
|
+
# invalid URL — let the HTTP call surface the error at request time
|
|
153
|
+
pass
|
|
154
|
+
return trimmed
|
|
155
|
+
|
|
156
|
+
|
|
105
157
|
# ============================================================
|
|
106
158
|
# Polling helpers (PR #121 parity with TS SDK)
|
|
107
159
|
# ============================================================
|
|
@@ -229,7 +281,7 @@ class MemWal:
|
|
|
229
281
|
self._private_key_hex = normalize_private_key(config.key)
|
|
230
282
|
self._signing_key = build_signing_key(self._private_key_hex)
|
|
231
283
|
self._account_id = config.account_id
|
|
232
|
-
self._server_url = config.server_url
|
|
284
|
+
self._server_url = normalize_server_url(config.server_url)
|
|
233
285
|
self._namespace = config.namespace
|
|
234
286
|
self._client: Optional[httpx.AsyncClient] = None
|
|
235
287
|
self._server_config: Optional[Dict[str, str]] = None
|
|
@@ -463,6 +515,9 @@ class MemWal:
|
|
|
463
515
|
capped at ``MAX_REMEMBER_TEXT_BYTES`` (= 64 KiB).
|
|
464
516
|
"""
|
|
465
517
|
|
|
518
|
+
if not items:
|
|
519
|
+
raise ValueError("remember_bulk_async: items must be a non-empty array")
|
|
520
|
+
|
|
466
521
|
payload_items: List[Dict[str, Any]] = [
|
|
467
522
|
{
|
|
468
523
|
"text": item.text,
|
|
@@ -476,8 +531,15 @@ class MemWal:
|
|
|
476
531
|
{"items": payload_items},
|
|
477
532
|
accepted_statuses=(200, 202),
|
|
478
533
|
)
|
|
534
|
+
job_ids = data.get("job_ids")
|
|
535
|
+
returned = len(job_ids) if job_ids else 0
|
|
536
|
+
if not job_ids or returned != len(payload_items):
|
|
537
|
+
raise ValueError(
|
|
538
|
+
f"remember_bulk_async: server returned {returned} job_ids for "
|
|
539
|
+
f"{len(payload_items)} items"
|
|
540
|
+
)
|
|
479
541
|
return RememberBulkAcceptedResult(
|
|
480
|
-
job_ids=list(
|
|
542
|
+
job_ids=list(job_ids),
|
|
481
543
|
total=int(data.get("total", len(payload_items))),
|
|
482
544
|
status=data.get("status", "pending"),
|
|
483
545
|
)
|
|
@@ -1275,6 +1337,8 @@ class MemWal:
|
|
|
1275
1337
|
raise _HttpStatusError(
|
|
1276
1338
|
status=response.status_code,
|
|
1277
1339
|
body=err_text,
|
|
1340
|
+
auth_error=response.headers.get("x-auth-error"),
|
|
1341
|
+
retry_after=response.headers.get("retry-after"),
|
|
1278
1342
|
)
|
|
1279
1343
|
|
|
1280
1344
|
return response.json()
|
|
@@ -1318,15 +1382,25 @@ class _HttpStatusError(MemWalError):
|
|
|
1318
1382
|
explicitly accepted).
|
|
1319
1383
|
"""
|
|
1320
1384
|
|
|
1321
|
-
def __init__(
|
|
1385
|
+
def __init__(
|
|
1386
|
+
self,
|
|
1387
|
+
status: int,
|
|
1388
|
+
body: str,
|
|
1389
|
+
auth_error: str | None = None,
|
|
1390
|
+
retry_after: str | None = None,
|
|
1391
|
+
) -> None:
|
|
1322
1392
|
if status == 401:
|
|
1323
1393
|
super().__init__(AUTH_REJECTED_MESSAGE)
|
|
1394
|
+
elif status == 503 and auth_error == AUTH_UPSTREAM_UNAVAILABLE:
|
|
1395
|
+
super().__init__(UPSTREAM_UNAVAILABLE_MESSAGE)
|
|
1324
1396
|
else:
|
|
1325
1397
|
super().__init__(
|
|
1326
1398
|
f"Walrus Memory API error ({status}): {_redact_internal_urls(body)}"
|
|
1327
1399
|
)
|
|
1328
1400
|
self.status = status
|
|
1329
1401
|
self.body = body
|
|
1402
|
+
self.auth_error = auth_error
|
|
1403
|
+
self.retry_after = retry_after
|
|
1330
1404
|
|
|
1331
1405
|
|
|
1332
1406
|
class MemWalRememberJobNotFound(MemWalError):
|
|
@@ -206,13 +206,17 @@ class RestoreResult:
|
|
|
206
206
|
total: int
|
|
207
207
|
namespace: str
|
|
208
208
|
owner: str
|
|
209
|
-
#: True when this restore is known-incomplete:
|
|
210
|
-
#:
|
|
211
|
-
#:
|
|
212
|
-
#:
|
|
213
|
-
#:
|
|
214
|
-
#:
|
|
215
|
-
#:
|
|
209
|
+
#: True when this restore is known-incomplete: more on-chain blobs were
|
|
210
|
+
#: missing locally than ``limit`` allowed this call to restore, or the
|
|
211
|
+
#: sidecar's owner-wide candidate fetch hit its cap *and* raising
|
|
212
|
+
#: ``limit`` can still expand that fetch (``limit < 20``). Once the cap
|
|
213
|
+
#: is saturated, truncation follows this call's missing-blob page, not
|
|
214
|
+
#: on-chain ``total``, so a fully restored namespace does not loop
|
|
215
|
+
#: (WALM-431 / GH #762).
|
|
216
|
+
#:
|
|
217
|
+
#: ``truncated=False`` is not proof the sidecar saw every on-chain blob.
|
|
218
|
+
#: Blobs beyond the owner-wide sidecar candidate cap can still be
|
|
219
|
+
#: missing. Follow-up field: WALM-451 (``sourceCapped``).
|
|
216
220
|
#:
|
|
217
221
|
#: Relayers older than WALM-319 don't send this field at all; the SDK
|
|
218
222
|
#: defaults it to ``False`` in that case rather than requiring it.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Tests for the AUTH_REJECTED_MESSAGE shown on a 401 from the relayer."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from memwal.client import (
|
|
6
|
+
AUTH_REJECTED_MESSAGE,
|
|
7
|
+
AUTH_UPSTREAM_UNAVAILABLE,
|
|
8
|
+
UPSTREAM_UNAVAILABLE_MESSAGE,
|
|
9
|
+
_HttpStatusError,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_auth_rejected_message_points_to_troubleshooting_guide() -> None:
|
|
14
|
+
assert "docs.wal.app/walrus-memory/troubleshooting/overview" in AUTH_REJECTED_MESSAGE
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_auth_503_is_retryable_not_a_credential_failure() -> None:
|
|
18
|
+
err = _HttpStatusError(
|
|
19
|
+
503, "upstream unavailable", auth_error=AUTH_UPSTREAM_UNAVAILABLE, retry_after="5"
|
|
20
|
+
)
|
|
21
|
+
assert str(err) == UPSTREAM_UNAVAILABLE_MESSAGE
|
|
22
|
+
assert "sign-in" in str(err)
|
|
23
|
+
assert "401" not in str(err)
|
|
24
|
+
assert err.retry_after == "5"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_non_auth_503_keeps_generic_body() -> None:
|
|
28
|
+
err = _HttpStatusError(503, "Rate limiter temporarily unavailable")
|
|
29
|
+
assert "Rate limiter temporarily unavailable" in str(err)
|
|
30
|
+
assert "cannot verify credentials" not in str(err)
|
|
@@ -27,6 +27,8 @@ from memwal.client import (
|
|
|
27
27
|
from memwal.types import (
|
|
28
28
|
RecallManualOptions,
|
|
29
29
|
RecallParams,
|
|
30
|
+
RememberBulkAcceptedResult,
|
|
31
|
+
RememberBulkItem,
|
|
30
32
|
RememberManualOptions,
|
|
31
33
|
ScoringWeights,
|
|
32
34
|
)
|
|
@@ -343,6 +345,90 @@ class TestRemember:
|
|
|
343
345
|
assert result.status == "pending"
|
|
344
346
|
|
|
345
347
|
|
|
348
|
+
# ============================================================
|
|
349
|
+
# remember_bulk_async() tests
|
|
350
|
+
# ============================================================
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class TestRememberBulkAsync:
|
|
354
|
+
@respx.mock
|
|
355
|
+
async def test_empty_items_raises_without_http(self, memwal_client: MemWal) -> None:
|
|
356
|
+
mock_seal_session_prereqs()
|
|
357
|
+
route = respx.post(f"{_TEST_SERVER}/api/remember/bulk").mock(
|
|
358
|
+
return_value=httpx.Response(
|
|
359
|
+
202,
|
|
360
|
+
json={"job_ids": [], "total": 0, "status": "pending"},
|
|
361
|
+
)
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
with pytest.raises(
|
|
365
|
+
ValueError,
|
|
366
|
+
match="remember_bulk_async: items must be a non-empty array",
|
|
367
|
+
):
|
|
368
|
+
await memwal_client.remember_bulk_async([])
|
|
369
|
+
|
|
370
|
+
assert not route.called
|
|
371
|
+
|
|
372
|
+
@respx.mock
|
|
373
|
+
async def test_mismatched_job_ids_raises(self, memwal_client: MemWal) -> None:
|
|
374
|
+
mock_seal_session_prereqs()
|
|
375
|
+
respx.post(f"{_TEST_SERVER}/api/remember/bulk").mock(
|
|
376
|
+
return_value=httpx.Response(
|
|
377
|
+
202,
|
|
378
|
+
json={
|
|
379
|
+
"job_ids": ["job-1"],
|
|
380
|
+
"total": 1,
|
|
381
|
+
"status": "pending",
|
|
382
|
+
},
|
|
383
|
+
)
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
with pytest.raises(
|
|
387
|
+
ValueError,
|
|
388
|
+
match="remember_bulk_async: server returned 1 job_ids for 2 items",
|
|
389
|
+
):
|
|
390
|
+
await memwal_client.remember_bulk_async(
|
|
391
|
+
[
|
|
392
|
+
RememberBulkItem(text="I love coffee"),
|
|
393
|
+
RememberBulkItem(text="I live in Tokyo"),
|
|
394
|
+
]
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
@respx.mock
|
|
398
|
+
async def test_matching_job_ids_returns_accepted(
|
|
399
|
+
self, memwal_client: MemWal
|
|
400
|
+
) -> None:
|
|
401
|
+
mock_seal_session_prereqs()
|
|
402
|
+
route = respx.post(f"{_TEST_SERVER}/api/remember/bulk").mock(
|
|
403
|
+
return_value=httpx.Response(
|
|
404
|
+
202,
|
|
405
|
+
json={
|
|
406
|
+
"job_ids": ["job-1", "job-2"],
|
|
407
|
+
"total": 2,
|
|
408
|
+
"status": "pending",
|
|
409
|
+
},
|
|
410
|
+
)
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
result = await memwal_client.remember_bulk_async(
|
|
414
|
+
[
|
|
415
|
+
RememberBulkItem(text="I love coffee"),
|
|
416
|
+
RememberBulkItem(text="I live in Tokyo", namespace="profile"),
|
|
417
|
+
]
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
assert route.called
|
|
421
|
+
body = json.loads(route.calls[0].request.content)
|
|
422
|
+
assert body["items"] == [
|
|
423
|
+
{"text": "I love coffee", "namespace": "default"},
|
|
424
|
+
{"text": "I live in Tokyo", "namespace": "profile"},
|
|
425
|
+
]
|
|
426
|
+
assert isinstance(result, RememberBulkAcceptedResult)
|
|
427
|
+
assert result.job_ids == ["job-1", "job-2"]
|
|
428
|
+
assert result.total == 2
|
|
429
|
+
assert result.status == "pending"
|
|
430
|
+
|
|
431
|
+
|
|
346
432
|
# ============================================================
|
|
347
433
|
# recall() tests
|
|
348
434
|
# ============================================================
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Tests for plaintext HTTP server_url guarding (WALM-452 / #748).
|
|
2
|
+
|
|
3
|
+
No network: ``MemWal.create`` only stores config, and
|
|
4
|
+
``normalize_server_url`` is a pure parse + log helper.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
from memwal.client import MemWal, normalize_server_url
|
|
14
|
+
|
|
15
|
+
_KEY = "ab" * 32
|
|
16
|
+
_ACCOUNT = "0xdummy"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_plaintext_remote_warns_and_strips(caplog: pytest.LogCaptureFixture) -> None:
|
|
20
|
+
caplog.set_level(logging.WARNING, logger="memwal")
|
|
21
|
+
assert (
|
|
22
|
+
normalize_server_url("http://relayer.example.com/")
|
|
23
|
+
== "http://relayer.example.com"
|
|
24
|
+
)
|
|
25
|
+
assert "plaintext" in caplog.text
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_https_remote_does_not_warn(caplog: pytest.LogCaptureFixture) -> None:
|
|
29
|
+
caplog.set_level(logging.WARNING, logger="memwal")
|
|
30
|
+
assert (
|
|
31
|
+
normalize_server_url("https://relayer.memory.walrus.xyz")
|
|
32
|
+
== "https://relayer.memory.walrus.xyz"
|
|
33
|
+
)
|
|
34
|
+
assert caplog.text == ""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.mark.parametrize(
|
|
38
|
+
"url",
|
|
39
|
+
[
|
|
40
|
+
"http://localhost:8000",
|
|
41
|
+
"http://127.0.0.1:8000",
|
|
42
|
+
"http://foo.localhost",
|
|
43
|
+
"http://[::1]:8000",
|
|
44
|
+
],
|
|
45
|
+
)
|
|
46
|
+
def test_plaintext_local_does_not_warn(
|
|
47
|
+
url: str, caplog: pytest.LogCaptureFixture
|
|
48
|
+
) -> None:
|
|
49
|
+
caplog.set_level(logging.WARNING, logger="memwal")
|
|
50
|
+
assert normalize_server_url(url) == url
|
|
51
|
+
assert caplog.text == ""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_create_warns_on_plaintext_remote(caplog: pytest.LogCaptureFixture) -> None:
|
|
55
|
+
caplog.set_level(logging.WARNING, logger="memwal")
|
|
56
|
+
client = MemWal.create(
|
|
57
|
+
key=_KEY,
|
|
58
|
+
account_id=_ACCOUNT,
|
|
59
|
+
server_url="http://relayer.example.com/",
|
|
60
|
+
)
|
|
61
|
+
assert client._server_url == "http://relayer.example.com"
|
|
62
|
+
assert "plaintext" in caplog.text
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_plaintext_remote_warning_omits_url_credentials(
|
|
66
|
+
caplog: pytest.LogCaptureFixture,
|
|
67
|
+
) -> None:
|
|
68
|
+
"""HTTPX userinfo must stay on the transport URL and out of the warning."""
|
|
69
|
+
|
|
70
|
+
caplog.set_level(logging.WARNING, logger="memwal")
|
|
71
|
+
url = "http://alice:example-secret@relayer.example.com/?token=super-secret"
|
|
72
|
+
assert (
|
|
73
|
+
normalize_server_url(url)
|
|
74
|
+
== "http://alice:example-secret@relayer.example.com/?token=super-secret"
|
|
75
|
+
)
|
|
76
|
+
assert "plaintext" in caplog.text
|
|
77
|
+
assert "http://relayer.example.com" in caplog.text
|
|
78
|
+
assert "alice" not in caplog.text
|
|
79
|
+
assert "example-secret" not in caplog.text
|
|
80
|
+
assert "super-secret" not in caplog.text
|
|
81
|
+
for rec in caplog.records:
|
|
82
|
+
assert "example-secret" not in rec.getMessage()
|
|
83
|
+
assert "example-secret" not in str(rec.args)
|
|
84
|
+
assert "super-secret" not in rec.getMessage()
|
|
85
|
+
assert "super-secret" not in str(rec.args)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_create_preserves_url_credentials_and_redacts_warning(
|
|
89
|
+
caplog: pytest.LogCaptureFixture,
|
|
90
|
+
) -> None:
|
|
91
|
+
caplog.set_level(logging.WARNING, logger="memwal")
|
|
92
|
+
url = "http://alice:example-secret@relayer.example.com/"
|
|
93
|
+
client = MemWal.create(key=_KEY, account_id=_ACCOUNT, server_url=url)
|
|
94
|
+
assert client._server_url == "http://alice:example-secret@relayer.example.com"
|
|
95
|
+
assert "plaintext" in caplog.text
|
|
96
|
+
assert "example-secret" not in caplog.text
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
"""Tests for the AUTH_REJECTED_MESSAGE shown on a 401 from the relayer."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from memwal.client import AUTH_REJECTED_MESSAGE
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def test_auth_rejected_message_points_to_troubleshooting_guide() -> None:
|
|
9
|
-
assert "docs.wal.app/walrus-memory/troubleshooting/overview" in AUTH_REJECTED_MESSAGE
|
|
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
|