Skip to content

Commit 1a982cb

Browse files
committed
Sanity checking improvements. All IntegerNumberOption really have to
be positive integers or zero. Replace type with new IntegerNumberGeqZeroOption class definition to enforce this range. Use update_option to test now unused IntegerNumberOption and never used FloatNumberOption. Test save function, make sure it creates .bak file. Test a few more configuration option classes.
1 parent 57fcca6 commit 1a982cb

File tree

2 files changed

+156
-11
lines changed

2 files changed

+156
-11
lines changed

roundup/configuration.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -722,14 +722,14 @@ def str2value(self, value):
722722
"stop-words (eg. A,AND,ARE,AS,AT,BE,BUT,BY, ...)"),
723723
(OctalNumberOption, "umask", "0o002",
724724
"Defines the file creation mode mask."),
725-
(IntegerNumberOption, 'csv_field_size', '131072',
725+
(IntegerNumberGeqZeroOption, 'csv_field_size', '131072',
726726
"Maximum size of a csv-field during import. Roundups export\n"
727727
"format is a csv (comma separated values) variant. The csv\n"
728728
"reader has a limit on the size of individual fields\n"
729729
"starting with python 2.5. Set this to a higher value if you\n"
730730
"get the error 'Error: field larger than field limit' during\n"
731731
"import."),
732-
(IntegerNumberOption, 'password_pbkdf2_default_rounds', '10000',
732+
(IntegerNumberGeqZeroOption, 'password_pbkdf2_default_rounds', '10000',
733733
"Sets the default number of rounds used when encoding passwords\n"
734734
"using the PBKDF2 scheme. Set this to a higher value on faster\n"
735735
"systems which want more security.\n"
@@ -819,7 +819,7 @@ def str2value(self, value):
819819
additional REST-API parameters after the roundup web url configured in
820820
the tracker section. If this variable is set to 'no', the rest path has
821821
no special meaning and will yield an error message."""),
822-
(IntegerNumberOption, 'api_calls_per_interval', "0",
822+
(IntegerNumberGeqZeroOption, 'api_calls_per_interval', "0",
823823
"Limit API calls per api_interval_in_sec seconds to\n"
824824
"this number.\n"
825825
"Determines the burst rate and the rate that new api\n"
@@ -830,7 +830,7 @@ def str2value(self, value):
830830
"another api request. A value of 0 turns off rate\n"
831831
"limiting in the API. Tune this as needed. See rest\n"
832832
"documentation for more info.\n"),
833-
(IntegerNumberOption, 'api_interval_in_sec', "3600",
833+
(IntegerNumberGeqZeroOption, 'api_interval_in_sec', "3600",
834834
"Defines the interval in seconds over which an api client can\n"
835835
"make api_calls_per_interval api calls. Tune this as needed.\n"),
836836
(CsrfSettingOption, 'csrf_enforce_token', "yes",
@@ -845,7 +845,7 @@ def str2value(self, value):
845845
the post.
846846
Set this to 'no' to ignore the field and accept the post.
847847
"""),
848-
(IntegerNumberOption, 'csrf_token_lifetime', "20160",
848+
(IntegerNumberGeqZeroOption, 'csrf_token_lifetime', "20160",
849849
"""csrf_tokens have a limited lifetime. If they are not
850850
used they are purged from the database after this
851851
number of minutes. Default (20160) is 2 weeks."""),
@@ -920,7 +920,7 @@ def str2value(self, value):
920920
log if the header is invalid or missing, but accept
921921
the post.
922922
Set this to 'no' to ignore the header and accept the post."""),
923-
(IntegerNumberOption, 'csrf_header_min_count', "1",
923+
(IntegerNumberGeqZeroOption, 'csrf_header_min_count', "1",
924924
"""Minimum number of header checks that must pass
925925
to accept the request. Set to 0 to accept post
926926
even if no header checks pass. Usually the Host header check
@@ -978,11 +978,11 @@ def str2value(self, value):
978978
(NullableOption, 'read_default_group', 'roundup',
979979
"Name of the group to use in the MySQL defaults file (.my.cnf).\n"
980980
"Only used in MySQL connections."),
981-
(IntegerNumberOption, 'sqlite_timeout', '30',
981+
(IntegerNumberGeqZeroOption, 'sqlite_timeout', '30',
982982
"Number of seconds to wait when the SQLite database is locked\n"
983983
"Default: use a 30 second timeout (extraordinarily generous)\n"
984984
"Only used in SQLite connections."),
985-
(IntegerNumberOption, 'cache_size', '100',
985+
(IntegerNumberGeqZeroOption, 'cache_size', '100',
986986
"Size of the node cache (in elements)"),
987987
(BooleanOption, "allow_create", "yes",
988988
"Setting this option to 'no' protects the database against table creations."),
@@ -1039,7 +1039,7 @@ def str2value(self, value):
10391039
"If username is not empty, password (below) MUST be set!"),
10401040
(Option, "password", NODEFAULT, "SMTP login password.\n"
10411041
"Set this if your mail host requires authenticated access."),
1042-
(IntegerNumberOption, "port", smtplib.SMTP_PORT,
1042+
(IntegerNumberGeqZeroOption, "port", smtplib.SMTP_PORT,
10431043
"Default port to send SMTP on.\n"
10441044
"Set this if your mail server runs on a different port."),
10451045
(NullableOption, "local_hostname", '',
@@ -1236,7 +1236,7 @@ def str2value(self, value):
12361236
"\"multiple\" then a separate email is sent to each\n"
12371237
"recipient. If \"single\" then a single email is sent with\n"
12381238
"each recipient as a CC address."),
1239-
(IntegerNumberOption, "max_attachment_size", sys.maxsize,
1239+
(IntegerNumberGeqZeroOption, "max_attachment_size", sys.maxsize,
12401240
"Attachments larger than the given number of bytes\n"
12411241
"won't be attached to nosy mails. They will be replaced by\n"
12421242
"a link to the tracker's download page for the file.")

test/test_config.py

Lines changed: 146 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import unittest
1919
import logging
2020

21+
import os, shutil, errno
22+
2123
import pytest
2224
from roundup import configuration
2325

@@ -68,7 +70,10 @@ def testTrackerWeb(self):
6870
self.assertRaises(configuration.OptionValueError,
6971
config._get_option('TRACKER_WEB').set, "htt://foo.example/bar")
7072

71-
def testLoginRateLimit(self):
73+
self.assertRaises(configuration.OptionValueError,
74+
config._get_option('TRACKER_WEB').set, "")
75+
76+
def testLoginAttemptsMin(self):
7277
config = configuration.CoreConfig()
7378

7479
self.assertEqual(None,
@@ -82,3 +87,143 @@ def testLoginRateLimit(self):
8287
self.assertRaises(configuration.OptionValueError,
8388
config._get_option('WEB_LOGIN_ATTEMPTS_MIN').set, "-1")
8489

90+
self.assertRaises(configuration.OptionValueError,
91+
config._get_option('WEB_LOGIN_ATTEMPTS_MIN').set, "")
92+
93+
def testTimeZone(self):
94+
config = configuration.CoreConfig()
95+
96+
self.assertEqual(None,
97+
config._get_option('TIMEZONE').set("0"))
98+
99+
# not a valid timezone
100+
self.assertRaises(configuration.OptionValueError,
101+
config._get_option('TIMEZONE').set, "Zot")
102+
103+
# 25 is not a valid UTC offset: -12 - +14 is range
104+
# possibly +/- 1 for DST. But roundup.date doesn't
105+
# constrain to this range.
106+
#self.assertRaises(configuration.OptionValueError,
107+
# config._get_option('TIMEZONE').set, "25")
108+
109+
try:
110+
import pytz
111+
self.assertEqual(None,
112+
config._get_option('TIMEZONE').set("UTC"))
113+
self.assertEqual(None,
114+
config._get_option('TIMEZONE').set("America/New_York"))
115+
116+
except ImportError:
117+
self.assertRaises(configuration.OptionValueError,
118+
config._get_option('TIMEZONE').set, "UTC")
119+
self.assertRaises(configuration.OptionValueError,
120+
config._get_option('TIMEZONE').set, "America/New_York")
121+
122+
def testWebSecretKey(self):
123+
config = configuration.CoreConfig()
124+
125+
self.assertEqual(None,
126+
config._get_option('WEB_SECRET_KEY').set("skskskd"))
127+
128+
self.assertRaises(configuration.OptionValueError,
129+
config._get_option('WEB_SECRET_KEY').set, "")
130+
131+
132+
def testStaticFiles(self):
133+
config = configuration.CoreConfig()
134+
135+
self.assertEqual(None,
136+
config._get_option('STATIC_FILES').set("foo /tmp/bar"))
137+
138+
self.assertEqual(config.STATIC_FILES,
139+
["./foo", "/tmp/bar"])
140+
141+
self.assertEqual(config['STATIC_FILES'],
142+
["./foo", "/tmp/bar"])
143+
144+
def testIsolationLevel(self):
145+
config = configuration.CoreConfig()
146+
147+
self.assertEqual(None,
148+
config._get_option('RDBMS_ISOLATION_LEVEL').set("read uncommitted"))
149+
self.assertEqual(None,
150+
config._get_option('RDBMS_ISOLATION_LEVEL').set("read committed"))
151+
self.assertEqual(None,
152+
config._get_option('RDBMS_ISOLATION_LEVEL').set("repeatable read"))
153+
154+
155+
self.assertRaises(configuration.OptionValueError,
156+
config._get_option('RDBMS_ISOLATION_LEVEL').set, "not a level")
157+
158+
def testConfigSave(self):
159+
160+
config = configuration.CoreConfig()
161+
# make scratch directory to create files in
162+
163+
self.startdir = os.getcwd()
164+
165+
self.dirname = os.getcwd() + '_test_config'
166+
os.mkdir(self.dirname)
167+
168+
try:
169+
os.chdir(self.dirname)
170+
self.assertFalse(os.access("config.ini", os.F_OK))
171+
self.assertFalse(os.access("config.bak", os.F_OK))
172+
config.save()
173+
config.save() # creates .bak file
174+
self.assertTrue(os.access("config.ini", os.F_OK))
175+
self.assertTrue(os.access("config.bak", os.F_OK))
176+
177+
self.assertFalse(os.access("foo.bar", os.F_OK))
178+
self.assertFalse(os.access("foo.bak", os.F_OK))
179+
config.save("foo.bar")
180+
config.save("foo.bar") # creates .bak file
181+
self.assertTrue(os.access("foo.bar", os.F_OK))
182+
self.assertTrue(os.access("foo.bak", os.F_OK))
183+
184+
finally:
185+
# cleanup scratch directory and files
186+
try:
187+
os.chdir(self.startdir)
188+
shutil.rmtree(self.dirname)
189+
except OSError as error:
190+
if error.errno not in (errno.ENOENT, errno.ESRCH): raise
191+
192+
def testFloatAndInt_with_update_option(self):
193+
194+
config = configuration.CoreConfig()
195+
196+
# Update existing IntegerNumberGeqZeroOption to IntegerNumberOption
197+
config.update_option('WEB_LOGIN_ATTEMPTS_MIN',
198+
configuration.IntegerNumberOption,
199+
"0", description="new desc")
200+
201+
# -1 is allowed now that it is an int.
202+
self.assertEqual(None,
203+
config._get_option('WEB_LOGIN_ATTEMPTS_MIN').set("-1"))
204+
205+
# but can't float this
206+
self.assertRaises(configuration.OptionValueError,
207+
config._get_option('WEB_LOGIN_ATTEMPTS_MIN').set, "2.4")
208+
209+
# but fred is still an issue
210+
self.assertRaises(configuration.OptionValueError,
211+
config._get_option('WEB_LOGIN_ATTEMPTS_MIN').set, "fred")
212+
213+
# Update existing IntegerNumberOption to FloatNumberOption
214+
config.update_option('WEB_LOGIN_ATTEMPTS_MIN',
215+
configuration.FloatNumberOption,
216+
"0.0")
217+
218+
self.assertEqual(config['WEB_LOGIN_ATTEMPTS_MIN'], -1)
219+
220+
# can float this
221+
self.assertEqual(None,
222+
config._get_option('WEB_LOGIN_ATTEMPTS_MIN').set("3.1415926"))
223+
224+
# but fred is still an issue
225+
self.assertRaises(configuration.OptionValueError,
226+
config._get_option('WEB_LOGIN_ATTEMPTS_MIN').set, "fred")
227+
228+
self.assertAlmostEqual(config['WEB_LOGIN_ATTEMPTS_MIN'], 3.1415926,
229+
places=6)

0 commit comments

Comments
 (0)