mirror of https://github.com/OCA/web.git
Adding extra functionality for svg (rotate, color, ... )
parent
be5d3f3c26
commit
12e11b00c6
|
@ -42,8 +42,34 @@ Usage
|
|||
=====
|
||||
|
||||
This module works in conjunction with web_iconify. Once installed, icons
|
||||
will be served through the proxy and cached locally. No specific usage
|
||||
instructions are required.
|
||||
will be served through the proxy and cached locally.
|
||||
|
||||
SVG Icon Parameters
|
||||
-------------------
|
||||
|
||||
You can customize SVG icons by adding query parameters to the URL. The
|
||||
format is:
|
||||
|
||||
``/web_iconify_proxy/<prefix>/<icon>.svg?param1=value1¶m2=value2...``
|
||||
|
||||
Available parameters:
|
||||
|
||||
- **color**: Icon color (e.g., ``color=red``, ``color=%23ff0000``).
|
||||
- **width**: Icon width (e.g., ``width=50``, ``width=50px``).
|
||||
- **height**: Icon height (e.g., ``height=50``, ``height=50px``). If
|
||||
only one dimension is specified, the other will be calculated
|
||||
automatically to maintain aspect ratio.
|
||||
- **flip**: Flip the icon. Possible values: ``horizontal``,
|
||||
``vertical``, or both (e.g., ``flip=horizontal``, ``flip=vertical``,
|
||||
``flip=horizontal,vertical``).
|
||||
- **rotate**: Rotate the icon by 90, 180, or 270 degrees (e.g.,
|
||||
``rotate=90``, ``rotate=180``).
|
||||
- **box**: Set to ``true`` to add an empty rectangle to the SVG that
|
||||
matches the viewBox (e.g., ``box=true``).
|
||||
|
||||
Example:
|
||||
|
||||
``/web_iconify_proxy/mdi/home.svg?color=blue&width=64&flip=horizontal``
|
||||
|
||||
Changelog
|
||||
=========
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import ast
|
||||
import base64
|
||||
import datetime
|
||||
import logging
|
||||
|
@ -15,7 +16,13 @@ class IconifyProxyController(http.Controller):
|
|||
"""Controller for proxying Iconify requests."""
|
||||
|
||||
def _fetch_iconify_data(
|
||||
self, upstream_url, content_type, prefix, icons=None, icon=None
|
||||
self,
|
||||
upstream_url,
|
||||
content_type,
|
||||
prefix,
|
||||
icons=None,
|
||||
icon=None,
|
||||
normalized_params_string="",
|
||||
):
|
||||
"""Fetches data from the Iconify API or the local cache.
|
||||
|
||||
|
@ -25,22 +32,28 @@ class IconifyProxyController(http.Controller):
|
|||
prefix (str): The icon prefix.
|
||||
icons (str, optional): Comma-separated list of icons (for CSS and JSON).
|
||||
icon (str, optional): The icon name (for SVG).
|
||||
normalized_params_string (str, optional): Normalized parameters string.
|
||||
|
||||
Returns:
|
||||
Response: The HTTP response.
|
||||
"""
|
||||
|
||||
# Validate prefix
|
||||
prefix = prefix.lower()
|
||||
|
||||
# Validate prefix
|
||||
if not re.match(r"^[a-z0-9-]+$", prefix):
|
||||
raise request.not_found()
|
||||
|
||||
# Validate icon (if provided)
|
||||
if icon and not re.match(r"^[a-z0-9:-]+$", icon):
|
||||
if icon:
|
||||
icon = icon.lower()
|
||||
if not re.match(r"^[a-z0-9:-]+$", icon):
|
||||
raise request.not_found()
|
||||
|
||||
# Validate icons (if provided)
|
||||
if icons:
|
||||
icon_list = icons.split(",")
|
||||
icon_list = [i.lower() for i in icons.split(",")]
|
||||
for single_icon in icon_list:
|
||||
if not re.match(r"^[a-z0-9:-]+$", single_icon):
|
||||
raise request.not_found()
|
||||
|
@ -48,13 +61,13 @@ class IconifyProxyController(http.Controller):
|
|||
|
||||
Attachment = request.env["ir.attachment"].sudo()
|
||||
if content_type == "image/svg+xml":
|
||||
name = f"{prefix}-{icon}"
|
||||
name = f"{prefix}-{icon}-{normalized_params_string.lower()}"
|
||||
res_model = "iconify.svg"
|
||||
elif content_type == "text/css":
|
||||
name = f"{prefix}-{icons}"
|
||||
name = f"{prefix}-{icons}-{normalized_params_string.lower()}"
|
||||
res_model = "iconify.css"
|
||||
elif content_type == "application/json":
|
||||
name = f"{prefix}-{icons}"
|
||||
name = f"{prefix}-{icons}-{normalized_params_string.lower()}"
|
||||
res_model = "iconify.json"
|
||||
else:
|
||||
raise request.not_found()
|
||||
|
@ -98,6 +111,36 @@ class IconifyProxyController(http.Controller):
|
|||
]
|
||||
return request.make_response(data, headers)
|
||||
|
||||
def _normalize_params_common(self, params):
|
||||
"""Normalizes common parameters for Iconify requests."""
|
||||
normalized = []
|
||||
for key in sorted(params.keys()): # Sort keys alphabetically
|
||||
normalized.append(f"{key.lower()}={params[key]}")
|
||||
return ";".join(normalized)
|
||||
|
||||
def _normalize_params_svg(self, params):
|
||||
"""Normalizes parameters specifically for SVG requests."""
|
||||
allowed_params = ["color", "width", "height", "flip", "rotate", "box"]
|
||||
normalized = []
|
||||
for key in sorted(params.keys()):
|
||||
if key in allowed_params:
|
||||
value = params[key]
|
||||
key = key.lower()
|
||||
# Basic type validation
|
||||
if key in ("width", "height", "rotate") and not (
|
||||
isinstance(value, str) or isinstance(value, int)
|
||||
):
|
||||
continue # Skip invalid values
|
||||
if key == "box":
|
||||
try:
|
||||
value = ast.literal_eval(str(value).lower())
|
||||
if not isinstance(value, bool):
|
||||
continue
|
||||
except (ValueError, SyntaxError):
|
||||
continue
|
||||
normalized.append(f"{key}={value}")
|
||||
return ";".join(normalized)
|
||||
|
||||
@http.route(
|
||||
"/web_iconify_proxy/<string:prefix>/<string:icon>.svg",
|
||||
type="http",
|
||||
|
@ -115,9 +158,21 @@ class IconifyProxyController(http.Controller):
|
|||
Returns:
|
||||
Response: The HTTP response containing the SVG data.
|
||||
"""
|
||||
normalized_params = self._normalize_params_svg(params)
|
||||
if normalized_params:
|
||||
query_string = normalized_params.replace(";", "&")
|
||||
upstream_url = (
|
||||
f"https://api.iconify.design/{prefix}/{icon}.svg?{query_string}"
|
||||
)
|
||||
else:
|
||||
upstream_url = f"https://api.iconify.design/{prefix}/{icon}.svg"
|
||||
|
||||
return self._fetch_iconify_data(
|
||||
upstream_url, "image/svg+xml", prefix, icon=icon
|
||||
upstream_url,
|
||||
"image/svg+xml",
|
||||
prefix,
|
||||
icon=icon,
|
||||
normalized_params_string=normalized_params,
|
||||
)
|
||||
|
||||
@http.route(
|
||||
|
|
|
@ -1,3 +1,21 @@
|
|||
This module works in conjunction with web_iconify. Once installed, icons
|
||||
will be served through the proxy and cached locally. No specific usage
|
||||
instructions are required.
|
||||
will be served through the proxy and cached locally.
|
||||
|
||||
## SVG Icon Parameters
|
||||
|
||||
You can customize SVG icons by adding query parameters to the URL. The format is:
|
||||
|
||||
`/web_iconify_proxy/<prefix>/<icon>.svg?param1=value1¶m2=value2...`
|
||||
|
||||
Available parameters:
|
||||
|
||||
* **color**: Icon color (e.g., `color=red`, `color=%23ff0000`).
|
||||
* **width**: Icon width (e.g., `width=50`, `width=50px`).
|
||||
* **height**: Icon height (e.g., `height=50`, `height=50px`). If only one dimension is specified, the other will be calculated automatically to maintain aspect ratio.
|
||||
* **flip**: Flip the icon. Possible values: `horizontal`, `vertical`, or both (e.g., `flip=horizontal`, `flip=vertical`, `flip=horizontal,vertical`).
|
||||
* **rotate**: Rotate the icon by 90, 180, or 270 degrees (e.g., `rotate=90`, `rotate=180`).
|
||||
* **box**: Set to `true` to add an empty rectangle to the SVG that matches the viewBox (e.g., `box=true`).
|
||||
|
||||
Example:
|
||||
|
||||
`/web_iconify_proxy/mdi/home.svg?color=blue&width=64&flip=horizontal`
|
||||
|
|
|
@ -377,16 +377,19 @@ ir.attachment model.</p>
|
|||
<p><strong>Table of contents</strong></p>
|
||||
<div class="contents local topic" id="contents">
|
||||
<ul class="simple">
|
||||
<li><a class="reference internal" href="#usage" id="toc-entry-1">Usage</a></li>
|
||||
<li><a class="reference internal" href="#changelog" id="toc-entry-2">Changelog</a><ul>
|
||||
<li><a class="reference internal" href="#section-1" id="toc-entry-3">18.0.1.0.0 (2025-02-23)</a></li>
|
||||
<li><a class="reference internal" href="#usage" id="toc-entry-1">Usage</a><ul>
|
||||
<li><a class="reference internal" href="#svg-icon-parameters" id="toc-entry-2">SVG Icon Parameters</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-4">Bug Tracker</a></li>
|
||||
<li><a class="reference internal" href="#credits" id="toc-entry-5">Credits</a><ul>
|
||||
<li><a class="reference internal" href="#authors" id="toc-entry-6">Authors</a></li>
|
||||
<li><a class="reference internal" href="#contributors" id="toc-entry-7">Contributors</a></li>
|
||||
<li><a class="reference internal" href="#maintainers" id="toc-entry-8">Maintainers</a></li>
|
||||
<li><a class="reference internal" href="#changelog" id="toc-entry-3">Changelog</a><ul>
|
||||
<li><a class="reference internal" href="#section-1" id="toc-entry-4">18.0.1.0.0 (2025-02-23)</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-5">Bug Tracker</a></li>
|
||||
<li><a class="reference internal" href="#credits" id="toc-entry-6">Credits</a><ul>
|
||||
<li><a class="reference internal" href="#authors" id="toc-entry-7">Authors</a></li>
|
||||
<li><a class="reference internal" href="#contributors" id="toc-entry-8">Contributors</a></li>
|
||||
<li><a class="reference internal" href="#maintainers" id="toc-entry-9">Maintainers</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -394,20 +397,42 @@ ir.attachment model.</p>
|
|||
<div class="section" id="usage">
|
||||
<h1><a class="toc-backref" href="#toc-entry-1">Usage</a></h1>
|
||||
<p>This module works in conjunction with web_iconify. Once installed, icons
|
||||
will be served through the proxy and cached locally. No specific usage
|
||||
instructions are required.</p>
|
||||
will be served through the proxy and cached locally.</p>
|
||||
<div class="section" id="svg-icon-parameters">
|
||||
<h2><a class="toc-backref" href="#toc-entry-2">SVG Icon Parameters</a></h2>
|
||||
<p>You can customize SVG icons by adding query parameters to the URL. The
|
||||
format is:</p>
|
||||
<p><tt class="docutils literal"><span class="pre">/web_iconify_proxy/<prefix>/<icon>.svg?param1=value1&param2=value2...</span></tt></p>
|
||||
<p>Available parameters:</p>
|
||||
<ul class="simple">
|
||||
<li><strong>color</strong>: Icon color (e.g., <tt class="docutils literal">color=red</tt>, <tt class="docutils literal"><span class="pre">color=%23ff0000</span></tt>).</li>
|
||||
<li><strong>width</strong>: Icon width (e.g., <tt class="docutils literal">width=50</tt>, <tt class="docutils literal">width=50px</tt>).</li>
|
||||
<li><strong>height</strong>: Icon height (e.g., <tt class="docutils literal">height=50</tt>, <tt class="docutils literal">height=50px</tt>). If
|
||||
only one dimension is specified, the other will be calculated
|
||||
automatically to maintain aspect ratio.</li>
|
||||
<li><strong>flip</strong>: Flip the icon. Possible values: <tt class="docutils literal">horizontal</tt>,
|
||||
<tt class="docutils literal">vertical</tt>, or both (e.g., <tt class="docutils literal">flip=horizontal</tt>, <tt class="docutils literal">flip=vertical</tt>,
|
||||
<tt class="docutils literal">flip=horizontal,vertical</tt>).</li>
|
||||
<li><strong>rotate</strong>: Rotate the icon by 90, 180, or 270 degrees (e.g.,
|
||||
<tt class="docutils literal">rotate=90</tt>, <tt class="docutils literal">rotate=180</tt>).</li>
|
||||
<li><strong>box</strong>: Set to <tt class="docutils literal">true</tt> to add an empty rectangle to the SVG that
|
||||
matches the viewBox (e.g., <tt class="docutils literal">box=true</tt>).</li>
|
||||
</ul>
|
||||
<p>Example:</p>
|
||||
<p><tt class="docutils literal"><span class="pre">/web_iconify_proxy/mdi/home.svg?color=blue&width=64&flip=horizontal</span></tt></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="changelog">
|
||||
<h1><a class="toc-backref" href="#toc-entry-2">Changelog</a></h1>
|
||||
<h1><a class="toc-backref" href="#toc-entry-3">Changelog</a></h1>
|
||||
<div class="section" id="section-1">
|
||||
<h2><a class="toc-backref" href="#toc-entry-3">18.0.1.0.0 (2025-02-23)</a></h2>
|
||||
<h2><a class="toc-backref" href="#toc-entry-4">18.0.1.0.0 (2025-02-23)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Initial version.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="bug-tracker">
|
||||
<h1><a class="toc-backref" href="#toc-entry-4">Bug Tracker</a></h1>
|
||||
<h1><a class="toc-backref" href="#toc-entry-5">Bug Tracker</a></h1>
|
||||
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/web/issues">GitHub Issues</a>.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us to smash it by providing a detailed and welcomed
|
||||
|
@ -415,21 +440,21 @@ If you spotted it first, help us to smash it by providing a detailed and welcome
|
|||
<p>Do not contact contributors directly about support or help with technical issues.</p>
|
||||
</div>
|
||||
<div class="section" id="credits">
|
||||
<h1><a class="toc-backref" href="#toc-entry-5">Credits</a></h1>
|
||||
<h1><a class="toc-backref" href="#toc-entry-6">Credits</a></h1>
|
||||
<div class="section" id="authors">
|
||||
<h2><a class="toc-backref" href="#toc-entry-6">Authors</a></h2>
|
||||
<h2><a class="toc-backref" href="#toc-entry-7">Authors</a></h2>
|
||||
<ul class="simple">
|
||||
<li>jaco.tech</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="contributors">
|
||||
<h2><a class="toc-backref" href="#toc-entry-7">Contributors</a></h2>
|
||||
<h2><a class="toc-backref" href="#toc-entry-8">Contributors</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Jaco Waes <<a class="reference external" href="mailto:jaco@jaco.tech">jaco@jaco.tech</a>></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="maintainers">
|
||||
<h2><a class="toc-backref" href="#toc-entry-8">Maintainers</a></h2>
|
||||
<h2><a class="toc-backref" href="#toc-entry-9">Maintainers</a></h2>
|
||||
<p>This module is maintained by the OCA.</p>
|
||||
<a class="reference external image-reference" href="https://odoo-community.org">
|
||||
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
|
||||
|
|
|
@ -3,8 +3,7 @@ from unittest.mock import patch
|
|||
|
||||
import requests
|
||||
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import HttpCase
|
||||
from odoo.tests.common import HttpCase, tagged
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
|
@ -129,6 +128,140 @@ class TestIconifyProxyController(HttpCase):
|
|||
# Check that content is the same
|
||||
self.assertEqual(response1.content, response2.content)
|
||||
|
||||
@patch("odoo.addons.web_iconify_proxy.controllers.main.requests.get")
|
||||
def test_caching_with_params(self, mock_get):
|
||||
"""Test caching with different parameters."""
|
||||
mock_response = requests.Response()
|
||||
mock_response.status_code = 200
|
||||
mock_response._content = b"<svg>dummy content</svg>"
|
||||
mock_response.headers["Content-Type"] = "image/svg+xml"
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
# First request with specific parameters
|
||||
response1 = self.url_open(
|
||||
"/web_iconify_proxy/mdi/home.svg?color=red&width=50&flip=horizontal"
|
||||
)
|
||||
self.assertEqual(response1.status_code, 200)
|
||||
self.assertFalse("X-Cached-At" in response1.headers)
|
||||
|
||||
# Second request with the same parameters, should be cached
|
||||
response2 = self.url_open(
|
||||
"/web_iconify_proxy/mdi/home.svg?color=red&width=50&flip=horizontal"
|
||||
)
|
||||
self.assertEqual(response2.status_code, 200)
|
||||
self.assertTrue("X-Cached-At" in response2.headers)
|
||||
|
||||
# Third request with different parameters, should not be cached
|
||||
response3 = self.url_open(
|
||||
"/web_iconify_proxy/mdi/home.svg?color=blue&width=100"
|
||||
)
|
||||
self.assertEqual(response3.status_code, 200)
|
||||
self.assertFalse("X-Cached-At" in response3.headers)
|
||||
|
||||
@patch("odoo.addons.web_iconify_proxy.controllers.main.requests.get")
|
||||
def test_caching_parameter_order(self, mock_get):
|
||||
"""Test that parameter order doesn't affect caching."""
|
||||
mock_response = requests.Response()
|
||||
mock_response.status_code = 200
|
||||
mock_response._content = b"<svg>dummy content</svg>"
|
||||
mock_response.headers["Content-Type"] = "image/svg+xml"
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
# First request with specific parameter order
|
||||
response1 = self.url_open(
|
||||
"/web_iconify_proxy/mdi/home.svg?color=red&width=50&flip=horizontal"
|
||||
)
|
||||
self.assertEqual(response1.status_code, 200)
|
||||
self.assertFalse("X-Cached-At" in response1.headers)
|
||||
|
||||
# Second request with different parameter order, should be cached
|
||||
response2 = self.url_open(
|
||||
"/web_iconify_proxy/mdi/home.svg?flip=horizontal&width=50&color=red"
|
||||
)
|
||||
self.assertEqual(response2.status_code, 200)
|
||||
self.assertTrue("X-Cached-At" in response2.headers)
|
||||
|
||||
# Check that content is the same
|
||||
self.assertEqual(response1.content, response2.content)
|
||||
|
||||
@patch("odoo.addons.web_iconify_proxy.controllers.main.requests.get")
|
||||
def test_caching_boolean_values(self, mock_get):
|
||||
"""Test that boolean values are case-insensitive for caching."""
|
||||
mock_response = requests.Response()
|
||||
mock_response.status_code = 200
|
||||
mock_response._content = b"<svg>dummy content</svg>"
|
||||
mock_response.headers["Content-Type"] = "image/svg+xml"
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
# First request with "true"
|
||||
response1 = self.url_open("/web_iconify_proxy/mdi/home.svg?box=true")
|
||||
self.assertEqual(response1.status_code, 200)
|
||||
self.assertFalse("X-Cached-At" in response1.headers)
|
||||
|
||||
# Second request with "True", should be cached
|
||||
response2 = self.url_open("/web_iconify_proxy/mdi/home.svg?box=True")
|
||||
self.assertEqual(response2.status_code, 200)
|
||||
self.assertTrue("X-Cached-At" in response2.headers)
|
||||
|
||||
# Third request with "TRUE", should be cached
|
||||
response3 = self.url_open("/web_iconify_proxy/mdi/home.svg?box=TRUE")
|
||||
self.assertEqual(response3.status_code, 200)
|
||||
self.assertTrue("X-Cached-At" in response3.headers)
|
||||
|
||||
# Check that content is the same for all
|
||||
self.assertEqual(response1.content, response2.content)
|
||||
self.assertEqual(response1.content, response3.content)
|
||||
|
||||
@patch("odoo.addons.web_iconify_proxy.controllers.main.requests.get")
|
||||
def test_get_svg_with_parameters(self, mock_get):
|
||||
"""Test the get_svg route with various valid parameters."""
|
||||
mock_response = requests.Response()
|
||||
mock_response.status_code = 200
|
||||
mock_response._content = b"<svg>dummy content</svg>"
|
||||
mock_response.headers["Content-Type"] = "image/svg+xml"
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
test_cases = [
|
||||
"/web_iconify_proxy/mdi/home.svg?color=red",
|
||||
"/web_iconify_proxy/mdi/home.svg?width=50",
|
||||
"/web_iconify_proxy/mdi/home.svg?height=50",
|
||||
"/web_iconify_proxy/mdi/home.svg?flip=horizontal",
|
||||
"/web_iconify_proxy/mdi/home.svg?flip=vertical",
|
||||
"/web_iconify_proxy/mdi/home.svg?flip=horizontal,vertical",
|
||||
"/web_iconify_proxy/mdi/home.svg?rotate=90",
|
||||
"/web_iconify_proxy/mdi/home.svg?rotate=180",
|
||||
"/web_iconify_proxy/mdi/home.svg?rotate=270",
|
||||
"/web_iconify_proxy/mdi/home.svg?box=true",
|
||||
"/web_iconify_proxy/mdi/home.svg?color=blue&width=64&flip=horizontal&rotate=180",
|
||||
]
|
||||
for url in test_cases:
|
||||
with self.subTest(url=url):
|
||||
response = self.url_open(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.headers["Content-Type"], "image/svg+xml")
|
||||
|
||||
@patch("odoo.addons.web_iconify_proxy.controllers.main.requests.get")
|
||||
def test_get_svg_with_invalid_parameters(self, mock_get):
|
||||
"""Test the get_svg route with invalid parameters."""
|
||||
mock_response = requests.Response()
|
||||
mock_response.status_code = 200
|
||||
mock_response._content = b"<svg>dummy content</svg>"
|
||||
mock_response.headers["Content-Type"] = "image/svg+xml"
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
test_cases = [
|
||||
"/web_iconify_proxy/mdi/home.svg?width=invalid", # Invalid width
|
||||
"/web_iconify_proxy/mdi/home.svg?height=invalid", # Invalid height
|
||||
"/web_iconify_proxy/mdi/home.svg?rotate=invalid", # Invalid rotate
|
||||
"/web_iconify_proxy/mdi/home.svg?box=invalid", # Invalid box
|
||||
"/web_iconify_proxy/mdi/home.svg?unknown=param", # Unknown parameter
|
||||
]
|
||||
for url in test_cases:
|
||||
with self.subTest(url=url):
|
||||
response = self.url_open(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.headers["Content-Type"], "image/svg+xml")
|
||||
|
||||
@patch("odoo.addons.web_iconify_proxy.controllers.main.requests.get")
|
||||
def test_api_error(self, mock_get):
|
||||
# Mock requests.get to simulate an API error
|
||||
|
|
Loading…
Reference in New Issue