cherrypy-foundation 1.0.0__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.
Files changed (136) hide show
  1. cherrypy_foundation/__init__.py +0 -0
  2. cherrypy_foundation/components/ColorModes.jinja +70 -0
  3. cherrypy_foundation/components/Datatable.css +47 -0
  4. cherrypy_foundation/components/Datatable.jinja +63 -0
  5. cherrypy_foundation/components/Datatable.js +358 -0
  6. cherrypy_foundation/components/Field.css +10 -0
  7. cherrypy_foundation/components/Field.jinja +66 -0
  8. cherrypy_foundation/components/Field.js +56 -0
  9. cherrypy_foundation/components/Fields.jinja +4 -0
  10. cherrypy_foundation/components/Flash.jinja +13 -0
  11. cherrypy_foundation/components/Icon.jinja +3 -0
  12. cherrypy_foundation/components/LocaleSelection.jinja +13 -0
  13. cherrypy_foundation/components/LocaleSelection.js +26 -0
  14. cherrypy_foundation/components/SideBySideMultiSelect.css +25 -0
  15. cherrypy_foundation/components/SideBySideMultiSelect.jinja +9 -0
  16. cherrypy_foundation/components/SideBySideMultiSelect.js +9 -0
  17. cherrypy_foundation/components/Typeahead.css +55 -0
  18. cherrypy_foundation/components/Typeahead.jinja +106 -0
  19. cherrypy_foundation/components/Typeahead.js +8 -0
  20. cherrypy_foundation/components/__init__.py +51 -0
  21. cherrypy_foundation/components/tests/__init__.py +0 -0
  22. cherrypy_foundation/components/tests/test_static.py +90 -0
  23. cherrypy_foundation/components/vendor/bootstrap-icons/bootstrap-icons.css +2106 -0
  24. cherrypy_foundation/components/vendor/bootstrap-icons/bootstrap-icons.min.css +5 -0
  25. cherrypy_foundation/components/vendor/bootstrap-icons/fonts/bootstrap-icons.woff +0 -0
  26. cherrypy_foundation/components/vendor/bootstrap-icons/fonts/bootstrap-icons.woff2 +0 -0
  27. cherrypy_foundation/components/vendor/bootstrap5/css/bootstrap.css +9262 -0
  28. cherrypy_foundation/components/vendor/bootstrap5/css/bootstrap.css.map +95 -0
  29. cherrypy_foundation/components/vendor/bootstrap5/css/bootstrap.min.css +6 -0
  30. cherrypy_foundation/components/vendor/bootstrap5/css/bootstrap.min.css.map +7 -0
  31. cherrypy_foundation/components/vendor/bootstrap5/js/bootstrap.js +4846 -0
  32. cherrypy_foundation/components/vendor/bootstrap5/js/bootstrap.js.map +1 -0
  33. cherrypy_foundation/components/vendor/bootstrap5/js/bootstrap.min.js +7 -0
  34. cherrypy_foundation/components/vendor/bootstrap5/js/bootstrap.min.js.map +7 -0
  35. cherrypy_foundation/components/vendor/bootstrap5/js/color-modes.js +80 -0
  36. cherrypy_foundation/components/vendor/datatables/css/dataTables.dataTables.css +849 -0
  37. cherrypy_foundation/components/vendor/datatables/css/dataTables.dataTables.min.css +1 -0
  38. cherrypy_foundation/components/vendor/datatables/images/sort_asc.png +0 -0
  39. cherrypy_foundation/components/vendor/datatables/images/sort_asc_disabled.png +0 -0
  40. cherrypy_foundation/components/vendor/datatables/images/sort_both.png +0 -0
  41. cherrypy_foundation/components/vendor/datatables/images/sort_desc.png +0 -0
  42. cherrypy_foundation/components/vendor/datatables/images/sort_desc_disabled.png +0 -0
  43. cherrypy_foundation/components/vendor/datatables/js/dataTables.js +14073 -0
  44. cherrypy_foundation/components/vendor/datatables/js/dataTables.min.js +4 -0
  45. cherrypy_foundation/components/vendor/datatables-extensions/Buttons/css/buttons.dataTables.css +556 -0
  46. cherrypy_foundation/components/vendor/datatables-extensions/Buttons/css/buttons.dataTables.min.css +1 -0
  47. cherrypy_foundation/components/vendor/datatables-extensions/Buttons/js/buttons.html5.js +1700 -0
  48. cherrypy_foundation/components/vendor/datatables-extensions/Buttons/js/buttons.html5.min.js +8 -0
  49. cherrypy_foundation/components/vendor/datatables-extensions/Buttons/js/dataTables.buttons.js +2944 -0
  50. cherrypy_foundation/components/vendor/datatables-extensions/Buttons/js/dataTables.buttons.min.js +4 -0
  51. cherrypy_foundation/components/vendor/datatables-extensions/FixedHeader/css/fixedHeader.dataTables.css +13 -0
  52. cherrypy_foundation/components/vendor/datatables-extensions/FixedHeader/css/fixedHeader.dataTables.min.css +1 -0
  53. cherrypy_foundation/components/vendor/datatables-extensions/FixedHeader/js/dataTables.fixedHeader.js +1202 -0
  54. cherrypy_foundation/components/vendor/datatables-extensions/FixedHeader/js/dataTables.fixedHeader.min.js +4 -0
  55. cherrypy_foundation/components/vendor/datatables-extensions/JSZip/jszip.js +11577 -0
  56. cherrypy_foundation/components/vendor/datatables-extensions/JSZip/jszip.min.js +13 -0
  57. cherrypy_foundation/components/vendor/datatables-extensions/Responsive/css/responsive.dataTables.css +194 -0
  58. cherrypy_foundation/components/vendor/datatables-extensions/Responsive/css/responsive.dataTables.min.css +1 -0
  59. cherrypy_foundation/components/vendor/datatables-extensions/Responsive/js/dataTables.responsive.js +1861 -0
  60. cherrypy_foundation/components/vendor/datatables-extensions/Responsive/js/dataTables.responsive.min.js +4 -0
  61. cherrypy_foundation/components/vendor/datatables-extensions/pdfmake/build/pdfmake.js +75023 -0
  62. cherrypy_foundation/components/vendor/datatables-extensions/pdfmake/build/pdfmake.min.js +3 -0
  63. cherrypy_foundation/components/vendor/datatables-extensions/pdfmake/build/vfs_fonts.js +6 -0
  64. cherrypy_foundation/components/vendor/datatables-extensions/rowgroup/css/rowGroup.dataTables.css +53 -0
  65. cherrypy_foundation/components/vendor/datatables-extensions/rowgroup/css/rowGroup.dataTables.min.css +1 -0
  66. cherrypy_foundation/components/vendor/datatables-extensions/rowgroup/js/dataTables.rowGroup.js +485 -0
  67. cherrypy_foundation/components/vendor/datatables-extensions/rowgroup/js/dataTables.rowGroup.min.js +4 -0
  68. cherrypy_foundation/components/vendor/jquery/jquery.min.js +2 -0
  69. cherrypy_foundation/components/vendor/multi/LICENSE +7 -0
  70. cherrypy_foundation/components/vendor/multi/README.md +109 -0
  71. cherrypy_foundation/components/vendor/multi/multi.css +95 -0
  72. cherrypy_foundation/components/vendor/multi/multi.js +328 -0
  73. cherrypy_foundation/components/vendor/popper/popper.js +1825 -0
  74. cherrypy_foundation/components/vendor/popper/popper.min.js +6 -0
  75. cherrypy_foundation/components/vendor/typeahead/jquery.typeahead.min.css +1 -0
  76. cherrypy_foundation/components/vendor/typeahead/jquery.typeahead.min.js +10 -0
  77. cherrypy_foundation/error_page.py +94 -0
  78. cherrypy_foundation/flash.py +50 -0
  79. cherrypy_foundation/form.py +119 -0
  80. cherrypy_foundation/logging.py +103 -0
  81. cherrypy_foundation/passwd.py +65 -0
  82. cherrypy_foundation/plugins/__init__.py +0 -0
  83. cherrypy_foundation/plugins/db.py +286 -0
  84. cherrypy_foundation/plugins/ldap.py +257 -0
  85. cherrypy_foundation/plugins/restapi.py +74 -0
  86. cherrypy_foundation/plugins/scheduler.py +287 -0
  87. cherrypy_foundation/plugins/smtp.py +223 -0
  88. cherrypy_foundation/plugins/tests/__init__.py +0 -0
  89. cherrypy_foundation/plugins/tests/test_db.py +118 -0
  90. cherrypy_foundation/plugins/tests/test_ldap.py +451 -0
  91. cherrypy_foundation/plugins/tests/test_scheduler.py +100 -0
  92. cherrypy_foundation/plugins/tests/test_scheduler_db.py +107 -0
  93. cherrypy_foundation/plugins/tests/test_smtp.py +140 -0
  94. cherrypy_foundation/sessions.py +93 -0
  95. cherrypy_foundation/tests/__init__.py +72 -0
  96. cherrypy_foundation/tests/templates/test_flash.html +9 -0
  97. cherrypy_foundation/tests/templates/test_form.html +16 -0
  98. cherrypy_foundation/tests/templates/test_url.html +15 -0
  99. cherrypy_foundation/tests/test_error_page.py +78 -0
  100. cherrypy_foundation/tests/test_flash.py +61 -0
  101. cherrypy_foundation/tests/test_form.py +148 -0
  102. cherrypy_foundation/tests/test_logging.py +78 -0
  103. cherrypy_foundation/tests/test_passwd.py +51 -0
  104. cherrypy_foundation/tests/test_sessions.py +89 -0
  105. cherrypy_foundation/tests/test_url.py +161 -0
  106. cherrypy_foundation/tools/__init__.py +0 -0
  107. cherrypy_foundation/tools/auth.py +263 -0
  108. cherrypy_foundation/tools/auth_mfa.py +249 -0
  109. cherrypy_foundation/tools/i18n.py +529 -0
  110. cherrypy_foundation/tools/jinja2.py +158 -0
  111. cherrypy_foundation/tools/ratelimit.py +265 -0
  112. cherrypy_foundation/tools/secure_headers.py +119 -0
  113. cherrypy_foundation/tools/sessions_timeout.py +167 -0
  114. cherrypy_foundation/tools/tests/__init__.py +0 -0
  115. cherrypy_foundation/tools/tests/components/Button.jinja +2 -0
  116. cherrypy_foundation/tools/tests/locales/de/LC_MESSAGES/messages.mo +0 -0
  117. cherrypy_foundation/tools/tests/locales/de/LC_MESSAGES/messages.po +15 -0
  118. cherrypy_foundation/tools/tests/locales/fr/LC_MESSAGES/messages.mo +0 -0
  119. cherrypy_foundation/tools/tests/locales/fr/LC_MESSAGES/messages.po +15 -0
  120. cherrypy_foundation/tools/tests/locales/messages.pot +2 -0
  121. cherrypy_foundation/tools/tests/templates/test_jinja2.html +11 -0
  122. cherrypy_foundation/tools/tests/templates/test_jinjax.html +9 -0
  123. cherrypy_foundation/tools/tests/templates/test_jinjax_i18n.html +22 -0
  124. cherrypy_foundation/tools/tests/test_auth.py +110 -0
  125. cherrypy_foundation/tools/tests/test_auth_mfa.py +369 -0
  126. cherrypy_foundation/tools/tests/test_i18n.py +247 -0
  127. cherrypy_foundation/tools/tests/test_jinja2.py +153 -0
  128. cherrypy_foundation/tools/tests/test_ratelimit.py +109 -0
  129. cherrypy_foundation/tools/tests/test_secure_headers.py +200 -0
  130. cherrypy_foundation/url.py +66 -0
  131. cherrypy_foundation/widgets.py +48 -0
  132. cherrypy_foundation-1.0.0.dist-info/METADATA +71 -0
  133. cherrypy_foundation-1.0.0.dist-info/RECORD +136 -0
  134. cherrypy_foundation-1.0.0.dist-info/WHEEL +5 -0
  135. cherrypy_foundation-1.0.0.dist-info/licenses/LICENSE.md +674 -0
  136. cherrypy_foundation-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,247 @@
1
+ # CherryPy
2
+ # Copyright (C) 2026 IKUS Software
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+
17
+ import gettext
18
+ import importlib.resources
19
+ import unittest
20
+ from datetime import datetime, timezone
21
+
22
+ import cherrypy
23
+ from cherrypy import _cpconfig
24
+ from cherrypy.test import helper
25
+
26
+ from .. import i18n
27
+
28
+ TEXT_EN = 'Some text to translate'
29
+ TEXT_FR = 'Du texte à traduire'
30
+
31
+
32
+ class TestI18n(unittest.TestCase):
33
+ def setUp(self):
34
+ self.mo_dir = importlib.resources.files(__package__) / 'locales'
35
+ self.assertTrue(self.mo_dir.is_dir())
36
+ cherrypy.request.config = _cpconfig.Config()
37
+
38
+ def test_search_translation_en(self):
39
+ # Load default translation return translation
40
+ t = i18n._search_translation(self.mo_dir, 'messages', 'en')
41
+ self.assertEqual("en", t.locale.language)
42
+ # Test translation object
43
+ self.assertEqual(TEXT_EN, t.gettext(TEXT_EN))
44
+
45
+ def test_search_translation_fr(self):
46
+ # Custom lang return translation.
47
+ t = i18n._search_translation(self.mo_dir, 'messages', 'fr')
48
+ self.assertIsInstance(t, gettext.GNUTranslations)
49
+ self.assertEqual("fr", t.locale.language)
50
+ # Test translation object
51
+ self.assertEqual(TEXT_FR, t.gettext(TEXT_EN))
52
+
53
+ def test_search_translation_invalid(self):
54
+ # Load invalid translation return None
55
+ t = i18n._search_translation(self.mo_dir, 'messages', 'tr')
56
+ # Return Null translations.
57
+ self.assertIn('NullTranslations', str(t.__class__))
58
+
59
+
60
+ class Root:
61
+
62
+ @cherrypy.expose
63
+ @cherrypy.tools.i18n(on=True)
64
+ def index(self):
65
+ return i18n.gettext(TEXT_EN)
66
+
67
+
68
+ class AbstractI18nTest(helper.CPWebCase):
69
+ default_lang = None
70
+ interactive = False
71
+
72
+ @classmethod
73
+ def setup_server(cls):
74
+ cherrypy.config.update(
75
+ {
76
+ 'tools.i18n.default': cls.default_lang,
77
+ 'tools.i18n.mo_dir': importlib.resources.files(__package__) / 'locales',
78
+ 'tools.i18n.domain': 'messages',
79
+ 'tools.i18n.cookie_name': 'locale',
80
+ }
81
+ )
82
+ cherrypy.tree.mount(Root(), '/')
83
+
84
+
85
+ class TestI18nWebCase(AbstractI18nTest):
86
+
87
+ def test_language_with_invalid(self):
88
+ # Query the page without login-in
89
+ self.getPage("/", headers=[("Accept-Language", "invalid")])
90
+ self.assertStatus('200 OK')
91
+ self.assertHeaderItemValue("Content-Language", "en")
92
+ self.assertInBody(TEXT_EN)
93
+
94
+ def test_language_with_unknown(self):
95
+ # Query the page without login-in
96
+ self.getPage("/", headers=[("Accept-Language", "it")])
97
+ self.assertStatus('200 OK')
98
+ self.assertHeaderItemValue("Content-Language", "en")
99
+ self.assertInBody(TEXT_EN)
100
+
101
+ def test_language_en(self):
102
+ self.getPage("/", headers=[("Accept-Language", "en-US,en;q=0.8")])
103
+ self.assertStatus('200 OK')
104
+ self.assertHeaderItemValue("Content-Language", "en-US")
105
+ self.assertInBody(TEXT_EN)
106
+
107
+ def test_language_en_fr(self):
108
+ self.getPage("/", headers=[("Accept-Language", "en-US,en;q=0.8,fr-CA;q=0.8")])
109
+ self.assertStatus('200 OK')
110
+ self.assertHeaderItemValue("Content-Language", "en-US")
111
+ self.assertInBody(TEXT_EN)
112
+
113
+ def test_language_fr(self):
114
+ self.getPage("/")
115
+ self.assertInBody(TEXT_EN)
116
+ self.getPage("/", headers=[("Accept-Language", "fr-CA;q=0.8,fr;q=0.6")])
117
+ self.assertStatus('200 OK')
118
+ self.assertHeaderItemValue("Content-Language", "fr-CA")
119
+ self.assertInBody(TEXT_FR)
120
+
121
+ def test_language_en_US_POSIX(self):
122
+ # When calling with locale variant
123
+ self.getPage("/", headers=[("Accept-Language", "en-US-POSIX")])
124
+ self.assertStatus('200 OK')
125
+ # Tehn page return en-US
126
+ self.assertHeaderItemValue("Content-Language", "en-US")
127
+
128
+ def test_cookie_fr(self):
129
+ # When calling with locale variant
130
+ self.getPage("/", headers=[("Accept-Language", "en-US"), ("Cookie", "locale=fr")])
131
+ self.assertStatus('200 OK')
132
+ # Tehn page return en-US
133
+ self.assertHeaderItemValue("Content-Language", "fr")
134
+
135
+ def test_with_preferred_lang(self):
136
+ # Given a default lang 'en'
137
+ date = datetime.fromtimestamp(1680111611, timezone.utc)
138
+ self.assertEqual(TEXT_EN, i18n.ugettext(TEXT_EN))
139
+ self.assertIn('March', i18n.format_datetime(date, format='long'))
140
+ # When using preferred_lang with french
141
+ with i18n.preferred_lang('fr'):
142
+ # Then french translation is used
143
+ self.assertEqual(TEXT_FR, i18n.ugettext(TEXT_EN))
144
+ # Then date time formating used french locale
145
+ self.assertIn('mars', i18n.format_datetime(date, format='long'))
146
+ # Then ouside the block, settings goest back to english
147
+ self.assertEqual(TEXT_EN, i18n.ugettext(TEXT_EN))
148
+ self.assertIn('March', i18n.format_datetime(date, format='long'))
149
+
150
+ def test_format_datetime_locales(self):
151
+ date = datetime.fromtimestamp(1680111611, timezone.utc)
152
+ with i18n.preferred_timezone('utc'):
153
+ with i18n.preferred_lang('fr'):
154
+ self.assertEqual('29 mars 2023, 17:40:11 TU', i18n.format_datetime(date, format='long'))
155
+ with i18n.preferred_lang('en'):
156
+ self.assertEqual('March 29, 2023, 5:40:11\u202fPM UTC', i18n.format_datetime(date, format='long'))
157
+ with i18n.preferred_lang('en_US'):
158
+ self.assertEqual('March 29, 2023, 5:40:11\u202fPM UTC', i18n.format_datetime(date, format='long'))
159
+ with i18n.preferred_lang('en_GB'):
160
+ self.assertEqual('29 March 2023, 17:40:11 UTC', i18n.format_datetime(date, format='long'))
161
+ with i18n.preferred_lang('en_CH'):
162
+ self.assertEqual('29 March 2023, 17:40:11 UTC', i18n.format_datetime(date, format='long'))
163
+ with i18n.preferred_lang('de'):
164
+ self.assertEqual('29. März 2023, 17:40:11 UTC', i18n.format_datetime(date, format='long'))
165
+ with i18n.preferred_lang('de_CH'):
166
+ self.assertEqual('29. März 2023, 17:40:11 UTC', i18n.format_datetime(date, format='long'))
167
+
168
+ with i18n.preferred_timezone('Europe/Paris'):
169
+ with i18n.preferred_lang('fr'):
170
+ self.assertEqual('29 mars 2023, 19:40:11 +0200', i18n.format_datetime(date, format='long'))
171
+ with i18n.preferred_lang('fr_CH'):
172
+ self.assertEqual('29 mars 2023, 19:40:11 +0200', i18n.format_datetime(date, format='long'))
173
+ with i18n.preferred_lang('en'):
174
+ self.assertEqual('March 29, 2023, 7:40:11\u202fPM +0200', i18n.format_datetime(date, format='long'))
175
+ with i18n.preferred_lang('en_US'):
176
+ self.assertEqual('March 29, 2023, 7:40:11\u202fPM +0200', i18n.format_datetime(date, format='long'))
177
+ with i18n.preferred_lang('en_GB'):
178
+ self.assertEqual('29 March 2023, 19:40:11 CEST', i18n.format_datetime(date, format='long'))
179
+ with i18n.preferred_lang('en_CH'):
180
+ self.assertEqual('29 March 2023, 19:40:11 CEST', i18n.format_datetime(date, format='long'))
181
+ with i18n.preferred_lang('de'):
182
+ self.assertEqual('29. März 2023, 19:40:11 MESZ', i18n.format_datetime(date, format='long'))
183
+ with i18n.preferred_lang('de_CH'):
184
+ self.assertEqual('29. März 2023, 19:40:11 MESZ', i18n.format_datetime(date, format='long'))
185
+
186
+ def test_list_available_locales(self):
187
+ self.assertEqual(['de', 'en', 'fr'], sorted([str(l) for l in i18n.list_available_locales()]))
188
+
189
+ def test_list_available_timezones(self):
190
+ timezones = i18n.list_available_timezones()
191
+ self.assertIn('America/Toronto', timezones)
192
+
193
+ def test_get_timezone_name(self):
194
+ with i18n.preferred_lang('en'):
195
+ self.assertEqual('Eastern Time', i18n.get_timezone_name('America/Toronto'))
196
+ self.assertEqual('ET', i18n.get_timezone_name('America/Toronto', width='short'))
197
+ self.assertEqual('Eastern Time', i18n.get_timezone_name('America/Toronto', width='long'))
198
+ with i18n.preferred_lang('fr'):
199
+ self.assertEqual('heure de l’Est nord-américain', i18n.get_timezone_name('America/Toronto'))
200
+ self.assertEqual('HE', i18n.get_timezone_name('America/Toronto', width='short'))
201
+ self.assertEqual('heure de l’Est nord-américain', i18n.get_timezone_name('America/Toronto', width='long'))
202
+
203
+
204
+ class TestI18nDefaultLangWebCase(AbstractI18nTest):
205
+ default_lang = 'FR'
206
+
207
+ @classmethod
208
+ def teardown_class(cls):
209
+ # Reset default-lang to avoid issue with other test
210
+ cherrypy.config['tools.i18n.default'] = 'en'
211
+ super().teardown_class()
212
+
213
+ def test_default_lang_without_accept_language(self):
214
+ # Given a default language
215
+ # When user connect to the application without Accept-Language
216
+ self.getPage("/")
217
+ self.assertStatus(200)
218
+ # Then page is displayed with default lang
219
+ self.assertInBody(TEXT_FR)
220
+
221
+ def test_default_lang_with_accept_language(self):
222
+ # Given a default language
223
+ # When user connect to the application with Accept-Language English
224
+ self.getPage("/", headers=[("Accept-Language", "en-US,en;q=0.8")])
225
+ self.assertStatus(200)
226
+ # Then page is displayed as english
227
+ self.assertInBody(TEXT_EN)
228
+
229
+ def test_default_lang_with_unknown_accept_language(self):
230
+ # Given a default language
231
+ # When user connect to the application with Accept-Language English
232
+ self.getPage("/", headers=[("Accept-Language", "it")])
233
+ self.assertStatus(200)
234
+ # Then page is displayed as english
235
+ self.assertInBody(TEXT_FR)
236
+
237
+
238
+ class TestI18nInvalidDefaultLangWebCase(AbstractI18nTest):
239
+ default_lang = 'invalid'
240
+
241
+ def test_default_lang_invalid(self):
242
+ # Given an invalid default language
243
+ # When user connect to the application without Accept-Language
244
+ self.getPage("/")
245
+ self.assertStatus(200)
246
+ # Then page is displayed with fallback to "en"
247
+ self.assertInBody(TEXT_EN)
@@ -0,0 +1,153 @@
1
+ # Cherrypy-foundation
2
+ # Copyright (C) 2026 IKUS Software
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ import importlib
17
+ from datetime import datetime, timezone
18
+ from unittest import skipUnless
19
+
20
+ import cherrypy
21
+ from cherrypy.test import helper
22
+ from parameterized import parameterized
23
+
24
+ from cherrypy_foundation.components import StaticMiddleware
25
+ from cherrypy_foundation.tests import SeleniumUnitTest
26
+ from cherrypy_foundation.url import url_for
27
+
28
+ from .. import i18n # noqa
29
+ from .. import jinja2 # noqa
30
+
31
+ HAS_JINJAX = importlib.util.find_spec("jinjax") is not None
32
+
33
+ env = cherrypy.tools.jinja2.create_env(
34
+ package_name=__package__,
35
+ globals={
36
+ 'const1': 'STATIC VALUE',
37
+ },
38
+ )
39
+
40
+
41
+ def extra_processor():
42
+ return {'var2': 'bar'}
43
+
44
+
45
+ @cherrypy.tools.i18n(on=False)
46
+ @cherrypy.tools.sessions(on=False)
47
+ class Static:
48
+
49
+ components = StaticMiddleware()
50
+
51
+
52
+ @cherrypy.tools.jinja2(on=False, env=env, extra_processor=extra_processor)
53
+ class Root:
54
+
55
+ static = Static()
56
+
57
+ @cherrypy.expose
58
+ @cherrypy.tools.jinja2(template='test_jinja2.html')
59
+ def index(self):
60
+ return {'var1': 'test-jinja2'}
61
+
62
+ @cherrypy.expose
63
+ @cherrypy.tools.jinja2(template='test_jinjax.html')
64
+ def jinjax(self):
65
+ return {'var1': 'test-jinjax'}
66
+
67
+ @cherrypy.expose
68
+ @cherrypy.tools.jinja2(template='test_jinjax_i18n.html')
69
+ @cherrypy.tools.i18n(
70
+ default='fr',
71
+ default_timezone='America/Toronto',
72
+ mo_dir=importlib.resources.files(__package__) / 'locales',
73
+ domain='messages',
74
+ cookie_name='locale', # For LocaleSelection
75
+ )
76
+ def localized(self):
77
+ return {
78
+ 'my_datetime': datetime(year=2025, month=11, day=26, hour=11, minute=16, tzinfo=timezone.utc),
79
+ 'my_date': datetime(year=2025, month=12, day=22, hour=14, minute=8, tzinfo=timezone.utc),
80
+ }
81
+
82
+
83
+ class Jinja2Test(helper.CPWebCase, SeleniumUnitTest):
84
+ default_lang = None
85
+ interactive = False
86
+
87
+ @classmethod
88
+ def setup_server(cls):
89
+ cherrypy.tree.mount(Root(), '/')
90
+
91
+ def test_get_page(self):
92
+ # Given a page render using jinja2
93
+ # When querying the page
94
+ self.getPage("/")
95
+ # Then the page return without error
96
+ self.assertStatus(200)
97
+ # Then the page is render dynamically using page context
98
+ self.assertInBody('test-jinja2')
99
+ self.assertInBody('bar')
100
+ self.assertInBody('STATIC VALUE')
101
+
102
+ @skipUnless(HAS_JINJAX, reason='Required jinjax')
103
+ def test_get_page_jinjax(self):
104
+ # Given a page render using jinjax
105
+ # When querying the page
106
+ self.getPage("/jinjax")
107
+ # Then the page return without error
108
+ self.assertStatus(200)
109
+ # Then the page is render dynamically using page context
110
+ self.assertInBody('<a class="btn btn-primary" href="http://example.com">foo</a>')
111
+
112
+ @skipUnless(HAS_JINJAX, reason='Required jinjax')
113
+ @parameterized.expand(
114
+ [
115
+ ('server_default', {}, 'fr'),
116
+ ('accept_lang_fr', {'Accept-Language': 'fr'}, 'fr'),
117
+ ('accept_lang_en', {'Accept-Language': 'en'}, 'en'),
118
+ ]
119
+ )
120
+ def test_get_page_i18n(self, _name, headers, expected_lang):
121
+ # Given a localized page render with jinja2
122
+ # When querying the page
123
+ self.getPage("/localized", headers=list(headers.items()))
124
+ # Then the page return without error
125
+ self.assertStatus(200)
126
+ # Then the page is render dynamically using page context
127
+ if expected_lang == 'fr':
128
+ self.assertInBody('lang="fr"')
129
+ self.assertInBody('français')
130
+ self.assertInBody('Du texte à traduire')
131
+ self.assertInBody('mercredi 26 novembre 2025, 06:16:00 heure normale de l’Est nord-américain')
132
+ self.assertInBody('lundi, décembre 22, 2025')
133
+ else:
134
+ self.assertInBody('lang="en"')
135
+ self.assertInBody('English')
136
+ self.assertInBody('Some text to translate')
137
+ self.assertInBody('Wednesday, November 26, 2025, 6:16:00\u202fAM Eastern Standard Time')
138
+ self.assertInBody('Monday, December 22, 2025')
139
+
140
+ @skipUnless(HAS_JINJAX, reason='Required jinjax')
141
+ def test_get_page_i18n_selenium(self):
142
+ # Given a localized page render with jinja2
143
+ with self.selenium() as driver:
144
+ # When querying the page
145
+ driver.get(url_for('localized'))
146
+ # Then page load without error in english (enforced chronium lang)
147
+ self.assertFalse(driver.get_log('browser'))
148
+ self.assertEqual('en_US', driver.find_element('css selector', 'html').get_attribute('lang'))
149
+ # When user select a language
150
+ btn = driver.find_element('css selector', 'button[data-locale=fr]')
151
+ btn.click()
152
+ # Then page is reloaded with in French.
153
+ self.assertEqual('fr', driver.find_element('css selector', 'html').get_attribute('lang'))
@@ -0,0 +1,109 @@
1
+ # CherryPy
2
+ # Copyright (C) 2026 IKUS Software
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+
17
+ import tempfile
18
+
19
+ import cherrypy
20
+ from cherrypy.test import helper
21
+
22
+ from .. import ratelimit # noqa
23
+
24
+
25
+ class Root:
26
+
27
+ @cherrypy.expose
28
+ def index(self):
29
+ return "OK"
30
+
31
+ @cherrypy.expose
32
+ @cherrypy.tools.ratelimit(on=True, methods=['POST'])
33
+ def login(self):
34
+ return "login"
35
+
36
+
37
+ class RateLimitTest(helper.CPWebCase):
38
+ interactive = False
39
+ rate_limit = 5
40
+ rate_limit_dir = None
41
+
42
+ def setUp(self):
43
+ cherrypy.tools.ratelimit.reset()
44
+ return super().setUp()
45
+
46
+ def tearDown(self):
47
+ cherrypy.tools.ratelimit.reset()
48
+ return super().tearDown()
49
+
50
+ @classmethod
51
+ def setup_server(cls):
52
+ rate_limit_storage_class = None
53
+ if cls.rate_limit_dir:
54
+ rate_limit_storage_class = ratelimit.FileRateLimit
55
+ cherrypy.config.update(
56
+ {
57
+ 'tools.ratelimit.debug': True,
58
+ 'tools.ratelimit.delay': 3600,
59
+ 'tools.ratelimit.limit': cls.rate_limit,
60
+ 'tools.ratelimit.storage_class': rate_limit_storage_class,
61
+ 'tools.ratelimit.storage_path': cls.rate_limit_dir,
62
+ }
63
+ )
64
+ cherrypy.tree.mount(Root(), '/')
65
+
66
+ def test_ratelimit(self):
67
+ # Given a endpoint with ratelimit enabled
68
+ # When requesting multiple time the page
69
+ for i in range(0, 5):
70
+ self.getPage('/login', method='POST')
71
+ self.assertStatus(200)
72
+ # Then a 429 error (too many request) is return
73
+ self.getPage('/login', method='POST')
74
+ self.assertStatus(429)
75
+
76
+ def test_ratelimit_forwarded_for(self):
77
+ # Given a endpoint with ratelimit enabled
78
+ # When requesting multiple time the login page with different `X-Forwarded-For`
79
+ for i in range(0, 5):
80
+ self.getPage(
81
+ '/login',
82
+ headers=[('X-Forwarded-For', '127.0.0.%s' % i)],
83
+ method='POST',
84
+ )
85
+ self.assertStatus(200)
86
+ # Then original IP get blocked
87
+ self.getPage(
88
+ '/login',
89
+ headers=[('X-Forwarded-For', '127.0.0.%s' % i)],
90
+ method='POST',
91
+ )
92
+ self.assertHeaderItemValue('X-Ratelimit-Limit', '5')
93
+ self.assertHeaderItemValue('X-Ratelimit-Remaining', '0')
94
+ self.assertHeader('X-Ratelimit-Reset')
95
+ self.assertStatus(429)
96
+
97
+
98
+ class FileStorageRateLimitTest(RateLimitTest):
99
+
100
+ @classmethod
101
+ def setup_class(cls):
102
+ cls.tempdir = tempfile.TemporaryDirectory(prefix='cherrypy-foundation-', suffix='-ratelimit-test')
103
+ cls.rate_limit_dir = cls.tempdir.name
104
+ super().setup_class()
105
+
106
+ @classmethod
107
+ def teardown_class(cls):
108
+ cls.tempdir.cleanup()
109
+ super().teardown_class()