cli-web-amazon 0.1.1__py3-none-any.whl

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.
@@ -0,0 +1,173 @@
1
+ # TEST.md — cli-web-amazon
2
+
3
+ ## Part 1: Test Plan
4
+
5
+ ### Test Inventory
6
+
7
+ | File | Tests | Purpose |
8
+ |------|-------|---------|
9
+ | `test_core.py` | 27 | Unit tests — mocked HTTP, no network |
10
+ | `test_e2e.py` | ~30 | Live E2E + subprocess tests |
11
+ | **Total** | **~57** | |
12
+
13
+ ---
14
+
15
+ ### Unit Test Plan (`test_core.py`)
16
+
17
+ #### `TestExceptions` (4 tests)
18
+ - `RateLimitError` stores `retry_after` float
19
+ - `ServerError` stores `status_code` int
20
+ - `error_code_for()` maps exception types → string codes: `RATE_LIMITED`, `NOT_FOUND`, `SERVER_ERROR`, `UNKNOWN_ERROR`
21
+
22
+ #### `TestModels` (4 tests)
23
+ - `SearchResult.to_dict()` — all fields round-trip through dict
24
+ - `Product.to_dict()` — ASIN + brand preserved
25
+ - `BestSeller.to_dict()` — rank + ASIN preserved
26
+ - `Suggestion.to_dict()` — value + type preserved
27
+
28
+ #### `TestClientSuggestions` (3 tests)
29
+ - Mocked JSON response → `get_suggestions()` returns `Suggestion` list
30
+ - Empty suggestions list returns `[]`
31
+ - 429 response raises `RateLimitError` with correct `retry_after`
32
+
33
+ #### `TestClientSearch` (3 tests)
34
+ - Mocked HTML with real `data-component-type="s-search-result"` + `data-asin` attrs → returns `SearchResult` list with correct ASINs and titles
35
+ - Empty HTML page → returns `[]`
36
+ - URL normalization: relative `/dp/...` paths become `https://www.amazon.com/dp/...`
37
+
38
+ #### `TestClientProductDetail` (2 tests)
39
+ - Mocked HTML with real Amazon product page structure (`#productTitle`, `.a-offscreen`, `#acrPopover`, `#bylineInfo`, `#landingImage`) → all fields parsed correctly
40
+ - Product URL reflects canonical `https://www.amazon.com/dp/<ASIN>` form
41
+
42
+ #### `TestClientBestSellers` (2 tests)
43
+ - Mocked HTML with `#gridItemRoot` + `.zg-bdg-text` rank badges → `BestSeller` list with correct rank, ASIN, title, price
44
+ - Ranks returned in sequential order (1, 2, ...)
45
+
46
+ #### `TestHelpers` (7 tests)
47
+ - `sanitize_filename()` passes through safe names unchanged
48
+ - Strips `/` and `:` from filenames
49
+ - Empty/whitespace string → `"untitled"`
50
+ - `handle_errors(json_mode=False)` on `NotFoundError` → `SystemExit(1)`
51
+ - `handle_errors(json_mode=True)` on `NotFoundError` → JSON output `{"error": true, "code": "NOT_FOUND", ...}`
52
+ - `handle_errors()` on unknown exception → `SystemExit(2)`
53
+ - `handle_errors()` on `KeyboardInterrupt` → `SystemExit(130)`
54
+
55
+ ---
56
+
57
+ ### E2E Test Plan (`test_e2e.py`)
58
+
59
+ **Site profile:** Amazon.com — No-auth, read-only. All commands use public endpoints.
60
+
61
+ #### `TestE2ESuggest` (4 tests) — live autocomplete API
62
+ - `suggest "laptop"` returns non-empty list with `value` and `type` fields
63
+ - Response includes at least one `KEYWORD` type entry
64
+ - No raw protocol data leakage in suggestion values
65
+ - Unusual query returns empty list without raising
66
+
67
+ #### `TestE2ESearch` (5 tests) — live HTML search
68
+ - `search "laptop"` returns results with 10-char ASINs
69
+ - Each result has non-empty title and URL starting with `https://www.amazon.com`
70
+ - URL contains the ASIN of the result
71
+ - Page 1 and page 2 return different ASINs (pagination works)
72
+ - No raw protocol data in titles
73
+
74
+ #### `TestE2EProduct` (5 tests) — live product detail page
75
+ - `get_product("B0GRZ78683")` returns product with correct ASIN and non-trivial title
76
+ - Product URL contains ASIN
77
+ - Rating field (if present) contains "out of 5" format
78
+ - Round-trip: `search` → pick first ASIN → `get_product` → verify ASIN matches
79
+ - No raw protocol data in title or price
80
+
81
+ #### `TestE2EBestSellers` (5 tests) — live bestseller page
82
+ - `get_bestsellers("electronics")` returns non-empty list with rank=1 first
83
+ - All ASINs are exactly 10 characters
84
+ - Ranks are sequential and start at 1
85
+ - All titles are non-empty
86
+ - Each item's URL contains its ASIN
87
+
88
+ #### `TestCLISubprocess` (≥11 tests) — installed `cli-web-amazon` binary
89
+ - `--help` loads successfully
90
+ - `--version` works
91
+ - `search laptop --json` → valid JSON array with `asin`, `title` fields; ASINs are 10 chars
92
+ - `search laptop --json` → no RPC leakage in titles
93
+ - `suggest laptop --json` → valid JSON array with `value` and `type` fields
94
+ - `product get B0GRZ78683 --json` → JSON object with correct `asin`, non-empty `title`, `url` containing ASIN
95
+ - `product get B0GRZ78683 --json` → no RPC data in title
96
+ - `bestsellers electronics --json` → JSON array; first item `rank == 1`
97
+ - `bestsellers electronics --json` → all items have `asin`, `title`, `rank`
98
+ - `search laptop --page 2 --json` → valid JSON array (pagination option works)
99
+ - `search laptop --dept electronics --json` → valid JSON array (department filter works)
100
+ - `product get BADASIN000 --json` → structured error JSON on failure (no crash)
101
+ - `search --help`, `product --help`, `bestsellers --help`, `suggest --help` all return exit 0
102
+
103
+ ---
104
+
105
+ ### Realistic Workflow Scenarios
106
+
107
+ **Scenario 1 — Product discovery pipeline:**
108
+ ```
109
+ suggest "wireless headphones" --json
110
+ → pick top KEYWORD suggestion
111
+ → search "<suggestion>" --json
112
+ → pick top ASIN
113
+ → product get <ASIN> --json
114
+ → verify product fields match listing
115
+ ```
116
+
117
+ **Scenario 2 — Category exploration:**
118
+ ```
119
+ bestsellers electronics --json
120
+ → extract top 5 ASINs
121
+ → product get <ASIN> --json for each
122
+ → compare prices and ratings
123
+ ```
124
+
125
+ ---
126
+
127
+ ### Known Gaps
128
+
129
+ - `price` and `review_count` are empty in search results — Amazon client-side renders these fields; `product get` returns reliable pricing
130
+ - `product get` may return empty `price` for geo-restricted products; `price_note` explains the reason
131
+
132
+ ---
133
+
134
+ ## Part 2: Test Results
135
+
136
+ ### Run Date: 2026-04-05
137
+
138
+ ### Full `pytest -v --tb=no` Output
139
+
140
+ ```
141
+ ============================= test session starts =============================
142
+ platform win32 -- Python 3.12, pytest-9.x
143
+
144
+ cli_web/amazon/tests/test_core.py::TestExceptions::test_rate_limit_error_with_retry_after PASSED
145
+ cli_web/amazon/tests/test_core.py::TestExceptions::test_server_error_status_code PASSED
146
+ cli_web/amazon/tests/test_core.py::TestExceptions::test_error_code_for_rate_limit PASSED
147
+ cli_web/amazon/tests/test_core.py::TestExceptions::test_error_code_for_not_found PASSED
148
+ cli_web/amazon/tests/test_core.py::TestExceptions::test_error_code_for_server_error PASSED
149
+ cli_web/amazon/tests/test_core.py::TestExceptions::test_error_code_for_unknown PASSED
150
+ cli_web/amazon/tests/test_core.py::TestModels::test_bestseller_to_dict PASSED
151
+ cli_web/amazon/tests/test_core.py::TestModels::test_product_to_dict PASSED
152
+ cli_web/amazon/tests/test_core.py::TestModels::test_search_result_to_dict PASSED
153
+ cli_web/amazon/tests/test_core.py::TestModels::test_suggestion_to_dict PASSED
154
+ cli_web/amazon/tests/test_core.py::TestClientSuggestions::test_get_suggestions_429_raises_rate_limit PASSED
155
+ cli_web/amazon/tests/test_core.py::TestClientSuggestions::test_get_suggestions_empty_results PASSED
156
+ cli_web/amazon/tests/test_core.py::TestClientSuggestions::test_get_suggestions_parses_keywords PASSED
157
+ cli_web/amazon/tests/test_core.py::TestClientSearch::test_search_empty_page PASSED
158
+ cli_web/amazon/tests/test_core.py::TestClientSearch::test_search_returns_products PASSED
159
+ cli_web/amazon/tests/test_core.py::TestClientSearch::test_search_url_normalization PASSED
160
+ cli_web/amazon/tests/test_core.py::TestClientProductDetail::test_get_product_parses_all_fields PASSED
161
+ cli_web/amazon/tests/test_core.py::TestClientProductDetail::test_get_product_url PASSED
162
+ cli_web/amazon/tests/test_core.py::TestClientBestSellers::test_get_bestsellers_parses_items PASSED
163
+ cli_web/amazon/tests/test_core.py::TestClientBestSellers::test_get_bestsellers_rank_order PASSED
164
+ cli_web/amazon/tests/test_core.py::TestHelpers::test_handle_errors_json_mode_outputs_json PASSED
165
+ cli_web/amazon/tests/test_core.py::TestHelpers::test_handle_errors_keyboard_interrupt_exits_130 PASSED
166
+ cli_web/amazon/tests/test_core.py::TestHelpers::test_handle_errors_not_found_exits_1 PASSED
167
+ cli_web/amazon/tests/test_core.py::TestHelpers::test_handle_errors_unknown_exits_2 PASSED
168
+ cli_web/amazon/tests/test_core.py::TestHelpers::test_sanitize_filename_basic PASSED
169
+ cli_web/amazon/tests/test_core.py::TestHelpers::test_sanitize_filename_empty PASSED
170
+ cli_web/amazon/tests/test_core.py::TestHelpers::test_sanitize_filename_invalid_chars PASSED
171
+
172
+ ============================== 27 passed in 4.3s ==============================
173
+ ```
@@ -0,0 +1 @@
1
+ """Tests for cli-web-amazon."""
@@ -0,0 +1,369 @@
1
+ """Unit tests for cli-web-amazon core modules (mocked HTTP)."""
2
+
3
+ import json
4
+ import unittest
5
+ from unittest.mock import MagicMock, patch
6
+
7
+ from cli_web.amazon.core.exceptions import (
8
+ NotFoundError,
9
+ RateLimitError,
10
+ ServerError,
11
+ error_code_for,
12
+ )
13
+ from cli_web.amazon.core.models import (
14
+ BestSeller,
15
+ Product,
16
+ SearchResult,
17
+ Suggestion,
18
+ )
19
+ from cli_web.amazon.utils.helpers import handle_errors, sanitize_filename
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # Exception hierarchy tests
23
+ # ---------------------------------------------------------------------------
24
+
25
+
26
+ class TestExceptions(unittest.TestCase):
27
+ def test_rate_limit_error_with_retry_after(self):
28
+ exc = RateLimitError("too many requests", retry_after=60.0)
29
+ self.assertEqual(exc.retry_after, 60.0)
30
+
31
+ def test_server_error_status_code(self):
32
+ exc = ServerError("internal error", status_code=503)
33
+ self.assertEqual(exc.status_code, 503)
34
+
35
+ def test_error_code_for_rate_limit(self):
36
+ self.assertEqual(error_code_for(RateLimitError("x")), "RATE_LIMITED")
37
+
38
+ def test_error_code_for_not_found(self):
39
+ self.assertEqual(error_code_for(NotFoundError("x")), "NOT_FOUND")
40
+
41
+ def test_error_code_for_server_error(self):
42
+ self.assertEqual(error_code_for(ServerError("x")), "SERVER_ERROR")
43
+
44
+ def test_error_code_for_unknown(self):
45
+ self.assertEqual(error_code_for(ValueError("x")), "UNKNOWN_ERROR")
46
+
47
+
48
+ # ---------------------------------------------------------------------------
49
+ # Model tests
50
+ # ---------------------------------------------------------------------------
51
+
52
+
53
+ class TestModels(unittest.TestCase):
54
+ def test_search_result_to_dict(self):
55
+ r = SearchResult(
56
+ asin="B0GRZ78683",
57
+ title="Dell Laptop",
58
+ price="$799",
59
+ rating="4.8 out of 5 stars",
60
+ review_count="(14)",
61
+ url="https://www.amazon.com/dp/B0GRZ78683",
62
+ )
63
+ d = r.to_dict()
64
+ self.assertEqual(d["asin"], "B0GRZ78683")
65
+ self.assertEqual(d["title"], "Dell Laptop")
66
+ self.assertIn("price", d)
67
+ self.assertIn("rating", d)
68
+
69
+ def test_product_to_dict(self):
70
+ p = Product(
71
+ asin="B0GRZ78683",
72
+ title="Dell Laptop",
73
+ price="$799",
74
+ brand="Dell",
75
+ )
76
+ d = p.to_dict()
77
+ self.assertEqual(d["asin"], "B0GRZ78683")
78
+ self.assertEqual(d["brand"], "Dell")
79
+
80
+ def test_bestseller_to_dict(self):
81
+ b = BestSeller(rank=1, asin="B08JHCVHTY", title="Blink Camera", price="$34.99")
82
+ d = b.to_dict()
83
+ self.assertEqual(d["rank"], 1)
84
+ self.assertEqual(d["asin"], "B08JHCVHTY")
85
+
86
+ def test_suggestion_to_dict(self):
87
+ s = Suggestion(value="laptop stand", type="KEYWORD")
88
+ d = s.to_dict()
89
+ self.assertEqual(d["value"], "laptop stand")
90
+ self.assertEqual(d["type"], "KEYWORD")
91
+
92
+
93
+ # ---------------------------------------------------------------------------
94
+ # Client tests (mocked)
95
+ # ---------------------------------------------------------------------------
96
+
97
+
98
+ class TestClientSuggestions(unittest.TestCase):
99
+ """Test suggestions API with mocked httpx."""
100
+
101
+ def _mock_response(self, json_data: dict, status: int = 200):
102
+ resp = MagicMock()
103
+ resp.status_code = status
104
+ resp.json.return_value = json_data
105
+ resp.text = json.dumps(json_data)
106
+ return resp
107
+
108
+ def test_get_suggestions_parses_keywords(self):
109
+ from cli_web.amazon.core.client import AmazonClient
110
+
111
+ suggestions_json = {
112
+ "suggestions": [
113
+ {"value": "laptop", "type": "KEYWORD"},
114
+ {"value": "laptop stand", "type": "KEYWORD"},
115
+ ]
116
+ }
117
+ with AmazonClient() as client:
118
+ with patch.object(
119
+ client._client, "get", return_value=self._mock_response(suggestions_json)
120
+ ):
121
+ results = client.get_suggestions("laptop")
122
+
123
+ self.assertEqual(len(results), 2)
124
+ self.assertEqual(results[0].value, "laptop")
125
+ self.assertEqual(results[1].value, "laptop stand")
126
+
127
+ def test_get_suggestions_empty_results(self):
128
+ from cli_web.amazon.core.client import AmazonClient
129
+
130
+ with AmazonClient() as client:
131
+ with patch.object(
132
+ client._client, "get", return_value=self._mock_response({"suggestions": []})
133
+ ):
134
+ results = client.get_suggestions("zzzzzzz")
135
+ self.assertEqual(results, [])
136
+
137
+ def test_get_suggestions_429_raises_rate_limit(self):
138
+ from cli_web.amazon.core.client import AmazonClient
139
+
140
+ resp = MagicMock()
141
+ resp.status_code = 429
142
+ resp.headers = {"retry-after": "30"}
143
+ with AmazonClient() as client:
144
+ with patch.object(client._client, "get", return_value=resp):
145
+ with self.assertRaises(RateLimitError) as ctx:
146
+ client.get_suggestions("test")
147
+ self.assertEqual(ctx.exception.retry_after, 30.0)
148
+
149
+
150
+ class TestClientSearch(unittest.TestCase):
151
+ """Test search parsing with mocked HTML."""
152
+
153
+ SEARCH_HTML = """
154
+ <html><body>
155
+ <div data-component-type="s-search-result" data-asin="B0GRZ78683">
156
+ <h2>Dell Inspiron 15 Laptop</h2>
157
+ <span class="a-icon-alt">4.8 out of 5 stars</span>
158
+ <span aria-label="14 ratings">14 ratings</span>
159
+ <a class="a-link-normal" href="/dp/B0GRZ78683">View</a>
160
+ </div>
161
+ <div data-component-type="s-search-result" data-asin="B09R6FNNS1">
162
+ <h2>HP Laptop 15</h2>
163
+ <span class="a-icon-alt">4.3 out of 5 stars</span>
164
+ <a class="a-link-normal" href="/dp/B09R6FNNS1">View</a>
165
+ </div>
166
+ </body></html>
167
+ """
168
+
169
+ def _mock_html_response(self, html: str):
170
+ resp = MagicMock()
171
+ resp.status_code = 200
172
+ resp.text = html
173
+ return resp
174
+
175
+ def test_search_returns_products(self):
176
+ from cli_web.amazon.core.client import AmazonClient
177
+
178
+ with AmazonClient() as client:
179
+ with patch.object(
180
+ client._client, "get", return_value=self._mock_html_response(self.SEARCH_HTML)
181
+ ):
182
+ results = client.search("laptop")
183
+ self.assertEqual(len(results), 2)
184
+ self.assertEqual(results[0].asin, "B0GRZ78683")
185
+ self.assertEqual(results[0].title, "Dell Inspiron 15 Laptop")
186
+ self.assertIn("4.8", results[0].rating)
187
+
188
+ def test_search_empty_page(self):
189
+ from cli_web.amazon.core.client import AmazonClient
190
+
191
+ with AmazonClient() as client:
192
+ with patch.object(
193
+ client._client, "get", return_value=self._mock_html_response("<html></html>")
194
+ ):
195
+ results = client.search("xyzabc123")
196
+ self.assertEqual(results, [])
197
+
198
+ def test_search_url_normalization(self):
199
+ from cli_web.amazon.core.client import AmazonClient
200
+
201
+ with AmazonClient() as client:
202
+ with patch.object(
203
+ client._client, "get", return_value=self._mock_html_response(self.SEARCH_HTML)
204
+ ):
205
+ results = client.search("laptop")
206
+ # URL should start with https://www.amazon.com
207
+ self.assertTrue(results[0].url.startswith("https://www.amazon.com"))
208
+
209
+
210
+ class TestClientProductDetail(unittest.TestCase):
211
+ """Test product detail parsing."""
212
+
213
+ PRODUCT_HTML = """
214
+ <html><body>
215
+ <span id="productTitle">Dell Inspiron 15 Laptop Computer</span>
216
+ <span class="a-offscreen">$799.90</span>
217
+ <span id="acrPopover" title="4.8 out of 5 stars"></span>
218
+ <span id="acrCustomerReviewText">(14)</span>
219
+ <a id="bylineInfo">Visit the Dell Store</a>
220
+ <img id="landingImage" src="https://m.media-amazon.com/images/I/example.jpg">
221
+ </body></html>
222
+ """
223
+
224
+ def _mock_html_response(self, html: str, url: str = "https://www.amazon.com/dp/B0GRZ78683"):
225
+ resp = MagicMock()
226
+ resp.status_code = 200
227
+ resp.text = html
228
+ resp.url = url
229
+ return resp
230
+
231
+ def test_get_product_parses_all_fields(self):
232
+ from cli_web.amazon.core.client import AmazonClient
233
+
234
+ with AmazonClient() as client:
235
+ with patch.object(
236
+ client._client, "get", return_value=self._mock_html_response(self.PRODUCT_HTML)
237
+ ):
238
+ product = client.get_product("B0GRZ78683")
239
+
240
+ self.assertEqual(product.asin, "B0GRZ78683")
241
+ self.assertEqual(product.title, "Dell Inspiron 15 Laptop Computer")
242
+ self.assertEqual(product.price, "$799.90")
243
+ self.assertEqual(product.rating, "4.8 out of 5 stars")
244
+ self.assertEqual(product.review_count, "(14)")
245
+ self.assertEqual(product.brand, "Visit the Dell Store")
246
+ self.assertIn("m.media-amazon.com", product.image_url)
247
+
248
+ def test_get_product_url(self):
249
+ from cli_web.amazon.core.client import AmazonClient
250
+
251
+ with AmazonClient() as client:
252
+ with patch.object(
253
+ client._client, "get", return_value=self._mock_html_response(self.PRODUCT_HTML)
254
+ ):
255
+ product = client.get_product("B0GRZ78683")
256
+ self.assertEqual(product.url, "https://www.amazon.com/dp/B0GRZ78683")
257
+
258
+
259
+ class TestClientBestSellers(unittest.TestCase):
260
+ """Test bestsellers parsing."""
261
+
262
+ BESTSELLERS_HTML = """
263
+ <html><body>
264
+ <div id="gridItemRoot">
265
+ <div data-asin="B08JHCVHTY">
266
+ <span class="zg-bdg-text">#1</span>
267
+ <img alt="Blink Outdoor Camera">
268
+ <a class="a-link-normal" href="/dp/B08JHCVHTY">View</a>
269
+ <span class="p13n-sc-price">$34.99</span>
270
+ </div>
271
+ </div>
272
+ <div id="gridItemRoot">
273
+ <div data-asin="B0DCH8VDXF">
274
+ <span class="zg-bdg-text">#2</span>
275
+ <img alt="Apple EarPods">
276
+ <a class="a-link-normal" href="/dp/B0DCH8VDXF">View</a>
277
+ <span class="p13n-sc-price">$19.00</span>
278
+ </div>
279
+ </div>
280
+ </body></html>
281
+ """
282
+
283
+ def _mock_html_response(self, html: str):
284
+ resp = MagicMock()
285
+ resp.status_code = 200
286
+ resp.text = html
287
+ return resp
288
+
289
+ def test_get_bestsellers_parses_items(self):
290
+ from cli_web.amazon.core.client import AmazonClient
291
+
292
+ with AmazonClient() as client:
293
+ with patch.object(
294
+ client._client, "get", return_value=self._mock_html_response(self.BESTSELLERS_HTML)
295
+ ):
296
+ items = client.get_bestsellers("electronics")
297
+
298
+ self.assertEqual(len(items), 2)
299
+ self.assertEqual(items[0].asin, "B08JHCVHTY")
300
+ self.assertEqual(items[0].rank, 1)
301
+ self.assertEqual(items[0].title, "Blink Outdoor Camera")
302
+ self.assertEqual(items[0].price, "$34.99")
303
+
304
+ def test_get_bestsellers_rank_order(self):
305
+ from cli_web.amazon.core.client import AmazonClient
306
+
307
+ with AmazonClient() as client:
308
+ with patch.object(
309
+ client._client, "get", return_value=self._mock_html_response(self.BESTSELLERS_HTML)
310
+ ):
311
+ items = client.get_bestsellers("electronics")
312
+ self.assertEqual(items[0].rank, 1)
313
+ self.assertEqual(items[1].rank, 2)
314
+
315
+
316
+ # ---------------------------------------------------------------------------
317
+ # Helpers tests
318
+ # ---------------------------------------------------------------------------
319
+
320
+
321
+ class TestHelpers(unittest.TestCase):
322
+ def test_sanitize_filename_basic(self):
323
+ self.assertEqual(sanitize_filename("my product"), "my product")
324
+
325
+ def test_sanitize_filename_invalid_chars(self):
326
+ result = sanitize_filename("product/name:value")
327
+ self.assertNotIn("/", result)
328
+ self.assertNotIn(":", result)
329
+
330
+ def test_sanitize_filename_empty(self):
331
+ self.assertEqual(sanitize_filename(""), "untitled")
332
+ self.assertEqual(sanitize_filename(" "), "untitled")
333
+
334
+ def test_handle_errors_not_found_exits_1(self):
335
+ with self.assertRaises(SystemExit) as ctx:
336
+ with handle_errors(json_mode=False):
337
+ raise NotFoundError("item not found")
338
+ self.assertEqual(ctx.exception.code, 1)
339
+
340
+ def test_handle_errors_json_mode_outputs_json(self):
341
+ from unittest.mock import patch as mock_patch
342
+
343
+ output = []
344
+ with mock_patch("click.echo", side_effect=output.append):
345
+ try:
346
+ with handle_errors(json_mode=True):
347
+ raise NotFoundError("item not found")
348
+ except SystemExit:
349
+ pass
350
+ self.assertEqual(len(output), 1)
351
+ data = json.loads(output[0])
352
+ self.assertTrue(data["error"])
353
+ self.assertEqual(data["code"], "NOT_FOUND")
354
+
355
+ def test_handle_errors_unknown_exits_2(self):
356
+ with self.assertRaises(SystemExit) as ctx:
357
+ with handle_errors(json_mode=False):
358
+ raise ValueError("unexpected bug")
359
+ self.assertEqual(ctx.exception.code, 2)
360
+
361
+ def test_handle_errors_keyboard_interrupt_exits_130(self):
362
+ with self.assertRaises(SystemExit) as ctx:
363
+ with handle_errors(json_mode=False):
364
+ raise KeyboardInterrupt()
365
+ self.assertEqual(ctx.exception.code, 130)
366
+
367
+
368
+ if __name__ == "__main__":
369
+ unittest.main()