|
3 | 3 | from __future__ import annotations |
4 | 4 |
|
5 | 5 | import os |
| 6 | +from typing import TYPE_CHECKING, ClassVar, cast |
6 | 7 | from unittest import TestCase, skipUnless |
7 | 8 |
|
8 | 9 | from amazon_paapi.api import AmazonApi |
9 | 10 |
|
| 11 | +if TYPE_CHECKING: |
| 12 | + from amazon_paapi.models import BrowseNode, CountryCode |
| 13 | + from amazon_paapi.models.item_result import Item |
| 14 | + from amazon_paapi.models.search_result import SearchResult |
| 15 | + from amazon_paapi.models.variations_result import VariationsResult |
10 | 16 |
|
11 | | -def get_api_credentials() -> tuple[str | None, str | None, str | None, str | None]: |
12 | | - api_key = os.environ.get("API_KEY") |
13 | | - api_secret = os.environ.get("API_SECRET") |
14 | | - affiliate_tag = os.environ.get("AFFILIATE_TAG") |
15 | | - country_code = os.environ.get("COUNTRY_CODE") |
16 | 17 |
|
17 | | - return api_key, api_secret, affiliate_tag, country_code |
| 18 | +def get_api_credentials() -> tuple[str, str, str, CountryCode]: |
| 19 | + """Get API credentials from environment variables. |
18 | 20 |
|
| 21 | + Raises: |
| 22 | + ValueError: If any required credential is missing. |
| 23 | + """ |
| 24 | + credentials = { |
| 25 | + "API_KEY": os.environ.get("API_KEY"), |
| 26 | + "API_SECRET": os.environ.get("API_SECRET"), |
| 27 | + "AFFILIATE_TAG": os.environ.get("AFFILIATE_TAG"), |
| 28 | + "COUNTRY_CODE": os.environ.get("COUNTRY_CODE"), |
| 29 | + } |
19 | 30 |
|
20 | | -@skipUnless(all(get_api_credentials()), "Needs Amazon API credentials") |
| 31 | + missing = [key for key, value in credentials.items() if value is None] |
| 32 | + if missing: |
| 33 | + msg = f"Missing environment variables: {', '.join(missing)}" |
| 34 | + raise ValueError(msg) |
| 35 | + |
| 36 | + return ( |
| 37 | + cast("str", credentials["API_KEY"]), |
| 38 | + cast("str", credentials["API_SECRET"]), |
| 39 | + cast("str", credentials["AFFILIATE_TAG"]), |
| 40 | + cast("CountryCode", credentials["COUNTRY_CODE"]), |
| 41 | + ) |
| 42 | + |
| 43 | + |
| 44 | +def has_api_credentials() -> bool: |
| 45 | + """Check if all API credentials are available.""" |
| 46 | + try: |
| 47 | + get_api_credentials() |
| 48 | + except ValueError: |
| 49 | + return False |
| 50 | + return True |
| 51 | + |
| 52 | + |
| 53 | +@skipUnless(has_api_credentials(), "Needs Amazon API credentials") |
21 | 54 | class IntegrationTest(TestCase): |
| 55 | + """Integration tests that make real API calls to Amazon. |
| 56 | +
|
| 57 | + All API results are cached at class level to minimize the number of |
| 58 | + requests. This reduces costs and avoids rate limiting. |
| 59 | + """ |
| 60 | + |
| 61 | + api: ClassVar[AmazonApi] |
| 62 | + affiliate_tag: ClassVar[str] |
| 63 | + search_result: ClassVar[SearchResult] |
| 64 | + item_with_offers: ClassVar[Item] |
| 65 | + get_items_result: ClassVar[list[Item]] |
| 66 | + variations_result: ClassVar[VariationsResult] |
| 67 | + browse_nodes_result: ClassVar[list[BrowseNode]] |
| 68 | + |
22 | 69 | @classmethod |
23 | | - def setUpClass(cls): |
| 70 | + def setUpClass(cls) -> None: |
| 71 | + """Set up API client and make shared API calls once for all tests.""" |
24 | 72 | api_key, api_secret, affiliate_tag, country_code = get_api_credentials() |
| 73 | + |
25 | 74 | cls.api = AmazonApi(api_key, api_secret, affiliate_tag, country_code) |
26 | 75 | cls.affiliate_tag = affiliate_tag |
27 | 76 |
|
28 | | - def test_search_items_and_get_information_for_the_first_one(self): |
29 | | - search_result = self.api.search_items(keywords="zapatillas") |
30 | | - searched_item = search_result.items[0] |
| 77 | + cls.search_result = cls.api.search_items(keywords="laptop") |
| 78 | + |
| 79 | + cls.item_with_offers = next( |
| 80 | + (item for item in cls.search_result.items if item.offers_v2 is not None), |
| 81 | + cls.search_result.items[0], |
| 82 | + ) |
| 83 | + cls.get_items_result = cls.api.get_items(cls.item_with_offers.asin) |
| 84 | + |
| 85 | + item_with_variations = next( |
| 86 | + (item for item in cls.search_result.items if item.parent_asin), |
| 87 | + cls.search_result.items[0], |
| 88 | + ) |
| 89 | + cls.variations_result = cls.api.get_variations( |
| 90 | + item_with_variations.parent_asin or item_with_variations.asin |
| 91 | + ) |
| 92 | + |
| 93 | + item_with_browse_nodes = next( |
| 94 | + ( |
| 95 | + item |
| 96 | + for item in cls.search_result.items |
| 97 | + if item.browse_node_info and item.browse_node_info.browse_nodes |
| 98 | + ), |
| 99 | + None, |
| 100 | + ) |
| 101 | + if item_with_browse_nodes: |
| 102 | + browse_node_id = item_with_browse_nodes.browse_node_info.browse_nodes[0].id |
| 103 | + cls.browse_nodes_result = cls.api.get_browse_nodes([browse_node_id]) |
| 104 | + else: |
| 105 | + cls.browse_nodes_result = [] |
31 | 106 |
|
32 | | - self.assertEqual(10, len(search_result.items)) |
| 107 | + def test_search_items_returns_expected_count(self) -> None: |
| 108 | + """Test that search returns the default number of items.""" |
| 109 | + self.assertEqual(10, len(self.search_result.items)) |
| 110 | + |
| 111 | + def test_search_items_includes_affiliate_tag(self) -> None: |
| 112 | + """Test that search results include the affiliate tag in URLs.""" |
| 113 | + searched_item = self.search_result.items[0] |
33 | 114 | self.assertIn(self.affiliate_tag, searched_item.detail_page_url) |
34 | 115 |
|
35 | | - get_results = self.api.get_items(searched_item.asin) |
| 116 | + def test_search_items_returns_offers_v2(self) -> None: |
| 117 | + """Test that search results include OffersV2 data.""" |
| 118 | + self.assertGreater(len(self.search_result.items), 0) |
| 119 | + |
| 120 | + item = self.item_with_offers |
| 121 | + self.assertIsNotNone(item.offers_v2) |
| 122 | + |
| 123 | + if item.offers_v2.listings: |
| 124 | + listing = item.offers_v2.listings[0] |
| 125 | + self.assertIsNotNone(listing) |
| 126 | + |
| 127 | + def test_get_items_returns_single_result(self) -> None: |
| 128 | + """Test that get_items returns exactly one item when given one ASIN.""" |
| 129 | + self.assertEqual(1, len(self.get_items_result)) |
| 130 | + |
| 131 | + def test_get_items_includes_affiliate_tag(self) -> None: |
| 132 | + """Test that get_items results include the affiliate tag in URLs.""" |
| 133 | + self.assertIn(self.affiliate_tag, self.get_items_result[0].detail_page_url) |
| 134 | + |
| 135 | + def test_get_items_returns_offers_v2(self) -> None: |
| 136 | + """Test that get_items returns OffersV2 data.""" |
| 137 | + item = self.get_items_result[0] |
| 138 | + self.assertIsNotNone(item.offers_v2) |
| 139 | + |
| 140 | + if item.offers_v2.listings: |
| 141 | + listing = item.offers_v2.listings[0] |
| 142 | + self.assertIsNotNone(listing) |
| 143 | + |
| 144 | + def test_get_variations_returns_items(self) -> None: |
| 145 | + """Test that get_variations returns a list of variation items.""" |
| 146 | + self.assertIsNotNone(self.variations_result) |
| 147 | + self.assertGreater(len(self.variations_result.items), 0) |
| 148 | + |
| 149 | + def test_get_variations_returns_variation_summary(self) -> None: |
| 150 | + """Test that get_variations returns variation summary.""" |
| 151 | + self.assertIsNotNone(self.variations_result.variation_summary) |
| 152 | + self.assertGreater(self.variations_result.variation_summary.variation_count, 0) |
| 153 | + |
| 154 | + def test_get_variations_items_include_affiliate_tag(self) -> None: |
| 155 | + """Test that variation items include the affiliate tag in URLs.""" |
| 156 | + item = self.variations_result.items[0] |
| 157 | + self.assertIn(self.affiliate_tag, item.detail_page_url) |
| 158 | + |
| 159 | + def test_get_browse_nodes_returns_results(self) -> None: |
| 160 | + """Test that get_browse_nodes returns browse node information.""" |
| 161 | + self.assertGreater(len(self.browse_nodes_result), 0) |
36 | 162 |
|
37 | | - self.assertEqual(1, len(get_results)) |
38 | | - self.assertIn(self.affiliate_tag, get_results[0].detail_page_url) |
| 163 | + def test_get_browse_nodes_returns_node_info(self) -> None: |
| 164 | + """Test that browse nodes contain expected information.""" |
| 165 | + node = self.browse_nodes_result[0] |
| 166 | + self.assertIsNotNone(node.id) |
| 167 | + self.assertIsNotNone(node.display_name) |
0 commit comments