diff --git a/CHANGELOG.md b/CHANGELOG.md index 90a8409..a73600f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Removed the trailing semicolon from CSP strings +- Added some missing `int` into function parameter types + ### Deprecated ### Removed diff --git a/ruff.toml b/ruff.toml index 9732550..03263bf 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,4 +1,4 @@ -extend = "/home/app/ruff-strict.toml" +extend = "/home/app/ruff.toml" [lint] ignore = [ diff --git a/tests/test_flask_talisman.py b/tests/test_flask_talisman.py index ad6c7d9..67327bd 100644 --- a/tests/test_flask_talisman.py +++ b/tests/test_flask_talisman.py @@ -50,7 +50,7 @@ def test_default_talisman_app(self): self.assertIn("Content-Security-Policy", rv.headers) self.assertEqual( - "default-src 'self'; object-src 'none'; frame-ancestors 'none'; child-src 'none';", + "default-src 'self'; object-src 'none'; frame-ancestors 'none'; child-src 'none'", rv.headers["Content-Security-Policy"], ) @@ -83,7 +83,7 @@ def test_default_talisman_app_delayed(self): self.assertIn("Content-Security-Policy", rv.headers) self.assertEqual( - "default-src 'self'; object-src 'none'; frame-ancestors 'none'; child-src 'none';", + "default-src 'self'; object-src 'none'; frame-ancestors 'none'; child-src 'none'", rv.headers["Content-Security-Policy"], ) @@ -95,29 +95,29 @@ def test_talisman_google_csp(self): self.assertEqual(rv.status_code, 200) self.assertIn("Content-Security-Policy", rv.headers) - self.assertIn("default-src 'self';", rv.headers["Content-Security-Policy"]) + self.assertIn("default-src 'self'", rv.headers["Content-Security-Policy"]) self.assertIn( - "connect-src 'self' *.google-analytics.com *.googletagmanager.com *.analytics.google.com;", + "connect-src 'self' *.google-analytics.com *.googletagmanager.com *.analytics.google.com", rv.headers["Content-Security-Policy"], ) self.assertIn( - "frame-src 'self' www.google.com www.youtube.com www.youtube-nocookie.com;", + "frame-src 'self' www.google.com www.youtube.com www.youtube-nocookie.com", rv.headers["Content-Security-Policy"], ) self.assertIn( - "font-src 'self' *.gstatic.com data:;", + "font-src 'self' *.gstatic.com data:", rv.headers["Content-Security-Policy"], ) self.assertIn( - "img-src 'self' img.youtube.com i.ytimg.com *.googletagmanager.com ssl.gstatic.com www.gstatic.com *.google-analytics.com;", + "img-src 'self' img.youtube.com i.ytimg.com *.googletagmanager.com ssl.gstatic.com www.gstatic.com *.google-analytics.com", rv.headers["Content-Security-Policy"], ) self.assertIn( - "script-src 'self' ajax.googleapis.com *.googleanalytics.com *.google-analytics.com www.youtube.com *.gstatic.com *.googletagmanager.com tagmanager.google.com;", + "script-src 'self' ajax.googleapis.com *.googleanalytics.com *.google-analytics.com www.youtube.com *.gstatic.com *.googletagmanager.com tagmanager.google.com", rv.headers["Content-Security-Policy"], ) self.assertIn( - "style-src 'self' ajax.googleapis.com fonts.googleapis.com *.gstatic.com googletagmanager.com tagmanager.google.com;", + "style-src 'self' ajax.googleapis.com fonts.googleapis.com *.gstatic.com googletagmanager.com tagmanager.google.com", rv.headers["Content-Security-Policy"], ) @@ -130,10 +130,10 @@ def test_talisman_typekit_csp(self): self.assertIn("Content-Security-Policy", rv.headers) self.assertIn( - "font-src 'self' use.typekit.net;", rv.headers["Content-Security-Policy"] + "font-src 'self' use.typekit.net", rv.headers["Content-Security-Policy"] ) self.assertIn( - "style-src 'self' *.typekit.net;", rv.headers["Content-Security-Policy"] + "style-src 'self' *.typekit.net", rv.headers["Content-Security-Policy"] ) def test_talisman_google_and_typekit_csp(self): @@ -149,15 +149,15 @@ def test_talisman_google_and_typekit_csp(self): self.assertIn("Content-Security-Policy", rv.headers) self.assertIn( - "font-src 'self' *.gstatic.com data: use.typekit.net;", + "font-src 'self' *.gstatic.com data: use.typekit.net", rv.headers["Content-Security-Policy"], ) self.assertIn( - "style-src 'self' ajax.googleapis.com fonts.googleapis.com *.gstatic.com googletagmanager.com tagmanager.google.com *.typekit.net;", + "style-src 'self' ajax.googleapis.com fonts.googleapis.com *.gstatic.com googletagmanager.com tagmanager.google.com *.typekit.net", rv.headers["Content-Security-Policy"], ) self.assertIn( - "img-src 'self' img.youtube.com i.ytimg.com *.googletagmanager.com ssl.gstatic.com www.gstatic.com *.google-analytics.com;", + "img-src 'self' img.youtube.com i.ytimg.com *.googletagmanager.com ssl.gstatic.com www.gstatic.com *.google-analytics.com", rv.headers["Content-Security-Policy"], ) @@ -176,11 +176,11 @@ def test_talisman_custom_csp(self): self.assertIn("Content-Security-Policy", rv.headers) self.assertIn( - "default-src 'self' example.com;", + "default-src 'self' example.com", rv.headers["Content-Security-Policy"], ) self.assertIn( - "img-src 'self' img.example.com;", + "img-src 'self' img.example.com", rv.headers["Content-Security-Policy"], ) @@ -200,11 +200,11 @@ def test_talisman_custom_csp_with_google(self): self.assertIn("Content-Security-Policy", rv.headers) self.assertIn( - "default-src 'self' example.com;", + "default-src 'self' example.com", rv.headers["Content-Security-Policy"], ) self.assertIn( - "img-src 'self' img.example.com img.youtube.com i.ytimg.com *.googletagmanager.com ssl.gstatic.com www.gstatic.com *.google-analytics.com;", + "img-src 'self' img.example.com img.youtube.com i.ytimg.com *.googletagmanager.com ssl.gstatic.com www.gstatic.com *.google-analytics.com", rv.headers["Content-Security-Policy"], ) diff --git a/tests/test_security.py b/tests/test_security.py index 3b22169..a94ddd8 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -13,14 +13,14 @@ def test_init(self): generator = CspGenerator() self.assertEqual( generator.to_string(), - "default-src 'self'; object-src 'none'; frame-ancestors 'none'; child-src 'none';", + "default-src 'self'; object-src 'none'; frame-ancestors 'none'; child-src 'none'", ) def test_init_str_output(self): generator = CspGenerator() self.assertEqual( str(generator), - "default-src 'self'; object-src 'none'; frame-ancestors 'none'; child-src 'none';", + "default-src 'self'; object-src 'none'; frame-ancestors 'none'; child-src 'none'", ) def test_init_dict_output(self): @@ -39,83 +39,83 @@ def test_init_none(self): generator = CspGenerator(CspGenerator.NONE) self.assertEqual( generator.to_string(), - "default-src 'none'; object-src 'none'; frame-ancestors 'none'; child-src 'none';", + "default-src 'none'; object-src 'none'; frame-ancestors 'none'; child-src 'none'", ) def test_init_list(self): generator = CspGenerator([CspGenerator.NONE, self.test_domain]) self.assertEqual( generator.to_string(), - f"default-src 'none' {self.test_domain}; object-src 'none'; frame-ancestors 'none'; child-src 'none';", + f"default-src 'none' {self.test_domain}; object-src 'none'; frame-ancestors 'none'; child-src 'none'", ) def test_init_empty_string(self): generator = CspGenerator("") self.assertEqual( generator.to_string(), - "default-src 'self'; object-src 'none'; frame-ancestors 'none'; child-src 'none';", + "default-src 'self'; object-src 'none'; frame-ancestors 'none'; child-src 'none'", ) def test_init_list_of_empty_strings(self): generator = CspGenerator([""]) self.assertEqual( generator.to_string(), - "default-src 'self'; object-src 'none'; frame-ancestors 'none'; child-src 'none';", + "default-src 'self'; object-src 'none'; frame-ancestors 'none'; child-src 'none'", ) def test_init_allow_objects(self): generator = CspGenerator([""], allow_objects=True) self.assertEqual( generator.to_string(), - "default-src 'self'; frame-ancestors 'none'; child-src 'none';", + "default-src 'self'; frame-ancestors 'none'; child-src 'none'", ) def test_init_allow_iframe_embedding(self): generator = CspGenerator([""], allow_iframe_embedding=True) self.assertEqual( generator.to_string(), - "default-src 'self'; object-src 'none'; child-src 'none';", + "default-src 'self'; object-src 'none'; child-src 'none'", ) def test_init_allow_children(self): generator = CspGenerator([""], allow_children=True) self.assertEqual( generator.to_string(), - "default-src 'self'; object-src 'none'; frame-ancestors 'none';", + "default-src 'self'; object-src 'none'; frame-ancestors 'none'", ) def test_init_allow_all_optional(self): generator = CspGenerator( [""], allow_objects=True, allow_iframe_embedding=True, allow_children=True ) - self.assertEqual(generator.to_string(), "default-src 'self';") + self.assertEqual(generator.to_string(), "default-src 'self'") def test_init_overwrite_default_disallows(self): generator = CspGenerator(CspGenerator.NONE) generator.object_src(self.test_domain, omit_self=True) self.assertEqual( generator.to_string(), - f"default-src 'none'; object-src {self.test_domain}; frame-ancestors 'none'; child-src 'none';", + f"default-src 'none'; object-src {self.test_domain}; frame-ancestors 'none'; child-src 'none'", ) def test_add_directive(self): generator = CspGenerator() generator.script_src(self.test_domain) - self.assertIn("default-src 'self';", generator.to_string()) - self.assertIn(f"script-src 'self' {self.test_domain};", generator.to_string()) + self.assertIn("default-src 'self'", generator.to_string()) + self.assertIn(f"script-src 'self' {self.test_domain}", generator.to_string()) def test_add_directive_none(self): generator = CspGenerator() generator.script_src(CspGenerator.NONE) - self.assertIn("default-src 'self';", generator.to_string()) - self.assertIn("script-src 'none';", generator.to_string()) + self.assertIn("default-src 'self'", generator.to_string()) + self.assertIn("script-src 'none'", generator.to_string()) def test_add_directive_multiple(self): generator = CspGenerator() generator.script_src(self.test_domain, self.test_domain_2) - self.assertIn("default-src 'self';", generator.to_string()) + self.assertIn("default-src 'self'", generator.to_string()) self.assertIn( - f"script-src 'self' {self.test_domain} {self.test_domain_2};", + f"script-src 'self' {self.test_domain} {self.test_domain_2}", generator.to_string(), ) @@ -124,18 +124,18 @@ def test_add_directive_duplicates(self): generator.script_src( self.test_domain, self.test_domain_2, self.test_domain, self.test_domain ) - self.assertIn("default-src 'self';", generator.to_string()) + self.assertIn("default-src 'self'", generator.to_string()) self.assertIn( - f"script-src 'self' {self.test_domain} {self.test_domain_2};", + f"script-src 'self' {self.test_domain} {self.test_domain_2}", generator.to_string(), ) def test_add_directive_list(self): generator = CspGenerator() generator.script_src([self.test_domain, self.test_domain_2]) - self.assertIn("default-src 'self';", generator.to_string()) + self.assertIn("default-src 'self'", generator.to_string()) self.assertIn( - f"script-src 'self' {self.test_domain} {self.test_domain_2};", + f"script-src 'self' {self.test_domain} {self.test_domain_2}", generator.to_string(), ) @@ -150,27 +150,27 @@ def test_add_directive_mixed_types(self): f"{self.test_domain_2} {test_domain_3}", [test_domain_4, f"{test_domain_5} {test_domain_6}"], ) - self.assertIn("default-src 'self';", generator.to_string()) + self.assertIn("default-src 'self'", generator.to_string()) self.assertIn( - f"script-src 'self' {self.test_domain} {self.test_domain_2} {test_domain_3} {test_domain_4} {test_domain_5} {test_domain_6};", + f"script-src 'self' {self.test_domain} {self.test_domain_2} {test_domain_3} {test_domain_4} {test_domain_5} {test_domain_6}", generator.to_string(), ) def test_add_directive_space_separated_list(self): generator = CspGenerator() generator.script_src(f"{self.test_domain} {self.test_domain_2}") - self.assertIn("default-src 'self';", generator.to_string()) + self.assertIn("default-src 'self'", generator.to_string()) self.assertIn( - f"script-src 'self' {self.test_domain} {self.test_domain_2};", + f"script-src 'self' {self.test_domain} {self.test_domain_2}", generator.to_string(), ) def test_add_directive_empty(self): generator = CspGenerator() generator.script_src() - self.assertIn("default-src 'self';", generator.to_string()) + self.assertIn("default-src 'self'", generator.to_string()) self.assertNotIn( - f"script-src 'self' {self.test_domain} {self.test_domain_2};", + f"script-src 'self' {self.test_domain} {self.test_domain_2}", generator.to_string(), ) @@ -178,54 +178,54 @@ def test_no_simplify_duplicated_directives(self): generator = CspGenerator() generator.script_src(CspGenerator.SELF) csp = generator.to_string() - self.assertIn("default-src 'self';", csp) - self.assertIn("script-src 'self';", csp) + self.assertIn("default-src 'self'", csp) + self.assertIn("script-src 'self'", csp) def test_simplify_duplicated_directives(self): generator = CspGenerator() generator.script_src(CspGenerator.SELF) csp = generator.to_string(simplify=True) - self.assertIn("default-src 'self';", csp) + self.assertIn("default-src 'self'", csp) self.assertNotIn("script-src", csp) def test_add_directive_existing_self(self): generator = CspGenerator() generator.script_src([CspGenerator.SELF, self.test_domain]) - self.assertIn("default-src 'self';", generator.to_string()) - self.assertIn(f"script-src 'self' {self.test_domain};", generator.to_string()) + self.assertIn("default-src 'self'", generator.to_string()) + self.assertIn(f"script-src 'self' {self.test_domain}", generator.to_string()) def test_add_directive_existing_none(self): generator = CspGenerator() generator.script_src([CspGenerator.NONE, self.test_domain]) - self.assertIn("default-src 'self';", generator.to_string()) - self.assertIn(f"script-src 'none' {self.test_domain};", generator.to_string()) + self.assertIn("default-src 'self'", generator.to_string()) + self.assertIn(f"script-src 'none' {self.test_domain}", generator.to_string()) def test_add_directive_omit_self(self): generator = CspGenerator() generator.script_src(self.test_domain, omit_self=True) - self.assertIn("default-src 'self';", generator.to_string()) - self.assertIn(f"script-src {self.test_domain};", generator.to_string()) + self.assertIn("default-src 'self'", generator.to_string()) + self.assertIn(f"script-src {self.test_domain}", generator.to_string()) def test_add_directive_list_omit_self(self): generator = CspGenerator() generator.script_src([self.test_domain, self.test_domain_2], omit_self=True) - self.assertIn("default-src 'self';", generator.to_string()) + self.assertIn("default-src 'self'", generator.to_string()) self.assertIn( - f"script-src {self.test_domain} {self.test_domain_2};", + f"script-src {self.test_domain} {self.test_domain_2}", generator.to_string(), ) def test_add_directive_omit_self_existing_none(self): generator = CspGenerator() generator.script_src([CspGenerator.NONE, self.test_domain], omit_self=True) - self.assertIn("default-src 'self';", generator.to_string()) - self.assertIn(f"script-src 'none' {self.test_domain};", generator.to_string()) + self.assertIn("default-src 'self'", generator.to_string()) + self.assertIn(f"script-src 'none' {self.test_domain}", generator.to_string()) def test_add_disallow_directive(self): generator = CspGenerator() generator.disallow("script-src") - self.assertIn("default-src 'self';", generator.to_string()) - self.assertIn("script-src 'none';", generator.to_string()) + self.assertIn("default-src 'self'", generator.to_string()) + self.assertIn("script-src 'none'", generator.to_string()) def test_add_directive_chained(self): generator = CspGenerator() @@ -235,17 +235,17 @@ def test_add_multiple_directives(self): generator = CspGenerator() generator.script_src(self.test_domain) generator.style_src(self.test_domain_2) - self.assertIn("default-src 'self';", generator.to_string()) - self.assertIn(f"script-src 'self' {self.test_domain};", generator.to_string()) - self.assertIn(f"style-src 'self' {self.test_domain_2};", generator.to_string()) + self.assertIn("default-src 'self'", generator.to_string()) + self.assertIn(f"script-src 'self' {self.test_domain}", generator.to_string()) + self.assertIn(f"style-src 'self' {self.test_domain_2}", generator.to_string()) def test_add_multiple_same_directives(self): generator = CspGenerator() generator.script_src(self.test_domain) generator.script_src(self.test_domain_2) - self.assertIn("default-src 'self';", generator.to_string()) + self.assertIn("default-src 'self'", generator.to_string()) self.assertIn( - f"script-src 'self' {self.test_domain} {self.test_domain_2};", + f"script-src 'self' {self.test_domain} {self.test_domain_2}", generator.to_string(), ) @@ -253,15 +253,15 @@ def test_add_multiple_same_directives_replace(self): generator = CspGenerator() generator.script_src(self.test_domain) generator.script_src(self.test_domain_2, replace=True) - self.assertIn("default-src 'self';", generator.to_string()) - self.assertIn(f"script-src 'self' {self.test_domain_2};", generator.to_string()) + self.assertIn("default-src 'self'", generator.to_string()) + self.assertIn(f"script-src 'self' {self.test_domain_2}", generator.to_string()) def test_add_custom_directive(self): generator = CspGenerator() generator.custom_src("custom-directive", self.test_domain) - self.assertIn("default-src 'self';", generator.to_string()) + self.assertIn("default-src 'self'", generator.to_string()) self.assertIn( - f"custom-directive 'self' {self.test_domain};", generator.to_string() + f"custom-directive 'self' {self.test_domain}", generator.to_string() ) def test_add_directive_sources(self): @@ -293,60 +293,60 @@ def test_add_directive_sources(self): for directive, method in directives: generator = CspGenerator() getattr(generator, method)(self.test_domain) - self.assertIn("default-src 'self';", generator.to_string()) + self.assertIn("default-src 'self'", generator.to_string()) self.assertIn( - f"{directive} 'self' {self.test_domain};", generator.to_string() + f"{directive} 'self' {self.test_domain}", generator.to_string() ) def test_add_report_uri(self): generator = CspGenerator() report_uri = "https://report.example.com" generator.report_uri(report_uri) - self.assertIn("default-src 'self';", generator.to_string()) - self.assertIn(f"report-uri {report_uri};", generator.to_string()) + self.assertIn("default-src 'self'", generator.to_string()) + self.assertIn(f"report-uri {report_uri}", generator.to_string()) def test_add_empty_report_uri(self): generator = CspGenerator() generator.report_uri("") - self.assertIn("default-src 'self';", generator.to_string()) + self.assertIn("default-src 'self'", generator.to_string()) self.assertNotIn("report-uri", generator.to_string()) def test_add_report_to(self): generator = CspGenerator() report_endpoint_name = "csp_report_endpoint" generator.report_to(report_endpoint_name) - self.assertIn("default-src 'self';", generator.to_string()) - self.assertIn(f"report-to {report_endpoint_name};", generator.to_string()) + self.assertIn("default-src 'self'", generator.to_string()) + self.assertIn(f"report-to {report_endpoint_name}", generator.to_string()) def test_add_empty_report_to(self): generator = CspGenerator() generator.report_to("") - self.assertIn("default-src 'self';", generator.to_string()) + self.assertIn("default-src 'self'", generator.to_string()) self.assertNotIn("report-to", generator.to_string()) def test_add_require_trusted_types_for(self): generator = CspGenerator() generator.require_trusted_types_for() - self.assertIn("default-src 'self';", generator.to_string()) - self.assertIn("require-trusted-types-for 'script';", generator.to_string()) + self.assertIn("default-src 'self'", generator.to_string()) + self.assertIn("require-trusted-types-for 'script'", generator.to_string()) def test_add_sandbox(self): generator = CspGenerator() generator.sandbox() - self.assertIn("default-src 'self';", generator.to_string()) - self.assertIn("sandbox;", generator.to_string()) + self.assertIn("default-src 'self'", generator.to_string()) + self.assertIn("sandbox", generator.to_string()) def test_add_sandbox_value(self): generator = CspGenerator() generator.sandbox("allow-scripts") - self.assertIn("default-src 'self';", generator.to_string()) - self.assertIn("sandbox allow-scripts;", generator.to_string()) + self.assertIn("default-src 'self'", generator.to_string()) + self.assertIn("sandbox allow-scripts", generator.to_string()) def test_add_sandbox_invalid_value(self): generator = CspGenerator() generator.sandbox("pizza") - self.assertIn("default-src 'self';", generator.to_string()) - self.assertIn("sandbox;", generator.to_string()) + self.assertIn("default-src 'self'", generator.to_string()) + self.assertIn("sandbox", generator.to_string()) class TestCommonSecurityHeaders(unittest.TestCase): diff --git a/tna_utilities/currency.py b/tna_utilities/currency.py index 0e10b8d..1a73191 100644 --- a/tna_utilities/currency.py +++ b/tna_utilities/currency.py @@ -1,9 +1,10 @@ -def currency(value: float | str | int, simplify: bool = True) -> str: +def currency(value: float | str, simplify: bool = True) -> str: """ Formats a number as a currency without the currency symbol. If simplify is True, removes unnecessary decimal places for whole numbers. """ + if not value: return "0" if simplify else "0.00" @@ -17,7 +18,7 @@ def currency(value: float | str | int, simplify: bool = True) -> str: def pretty_price( - value: float | str | int, + value: float | str, simplify: bool = True, currency_symbol: str = "£", ) -> str: @@ -27,6 +28,7 @@ def pretty_price( If the value is 0, returns "Free". Otherwise, returns the currency symbol followed by the formatted currency. """ + if value in {0, "0"} or round(float(value) * 100) == 0: return "Free" @@ -34,8 +36,8 @@ def pretty_price( def pretty_price_range( - value_from: float | str | int | None = None, - value_to: float | str | int | None = None, + value_from: float | str | None = None, + value_to: float | str | None = None, simplify: bool = True, currency_symbol: str = "£", ) -> str: diff --git a/tna_utilities/number.py b/tna_utilities/number.py index 9451ea7..923795c 100644 --- a/tna_utilities/number.py +++ b/tna_utilities/number.py @@ -3,7 +3,7 @@ def numberish( - value: float | int, + value: float, simple_units: bool = False, prefix_text: str | tuple[str, str] = "About ", ) -> str: diff --git a/tna_utilities/security.py b/tna_utilities/security.py index 51344d4..00fa8f8 100644 --- a/tna_utilities/security.py +++ b/tna_utilities/security.py @@ -480,7 +480,7 @@ def to_string(self, simplify=False) -> str: if sources: directive_str += " " + " ".join(sources) parts.append(directive_str) - return "; ".join(parts) + ";" + return "; ".join(parts) def __str__(self) -> str: return self.to_string()