test_forms.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  1. from django import forms
  2. from django.test import TestCase
  3. from dcim.models import Site
  4. from netbox.choices import ImportFormatChoices
  5. from utilities.forms.bulk_import import BulkImportForm
  6. from utilities.forms.fields.csv import CSVSelectWidget
  7. from utilities.forms.forms import BulkRenameForm
  8. from utilities.forms.utils import get_field_value, expand_alphanumeric_pattern, expand_ipaddress_pattern
  9. class ExpandIPAddress(TestCase):
  10. """
  11. Validate the operation of expand_ipaddress_pattern().
  12. """
  13. def test_ipv4_range(self):
  14. input = '1.2.3.[9-10]/32'
  15. output = sorted([
  16. '1.2.3.9/32',
  17. '1.2.3.10/32',
  18. ])
  19. self.assertEqual(sorted(expand_ipaddress_pattern(input, 4)), output)
  20. def test_ipv4_set(self):
  21. input = '1.2.3.[4,44]/32'
  22. output = sorted([
  23. '1.2.3.4/32',
  24. '1.2.3.44/32',
  25. ])
  26. self.assertEqual(sorted(expand_ipaddress_pattern(input, 4)), output)
  27. def test_ipv4_multiple_ranges(self):
  28. input = '1.[9-10].3.[9-11]/32'
  29. output = sorted([
  30. '1.9.3.9/32',
  31. '1.9.3.10/32',
  32. '1.9.3.11/32',
  33. '1.10.3.9/32',
  34. '1.10.3.10/32',
  35. '1.10.3.11/32',
  36. ])
  37. self.assertEqual(sorted(expand_ipaddress_pattern(input, 4)), output)
  38. def test_ipv4_multiple_sets(self):
  39. input = '1.[2,22].3.[4,44]/32'
  40. output = sorted([
  41. '1.2.3.4/32',
  42. '1.2.3.44/32',
  43. '1.22.3.4/32',
  44. '1.22.3.44/32',
  45. ])
  46. self.assertEqual(sorted(expand_ipaddress_pattern(input, 4)), output)
  47. def test_ipv4_set_and_range(self):
  48. input = '1.[2,22].3.[9-11]/32'
  49. output = sorted([
  50. '1.2.3.9/32',
  51. '1.2.3.10/32',
  52. '1.2.3.11/32',
  53. '1.22.3.9/32',
  54. '1.22.3.10/32',
  55. '1.22.3.11/32',
  56. ])
  57. self.assertEqual(sorted(expand_ipaddress_pattern(input, 4)), output)
  58. def test_ipv6_range(self):
  59. input = 'fec::abcd:[9-b]/64'
  60. output = sorted([
  61. 'fec::abcd:9/64',
  62. 'fec::abcd:a/64',
  63. 'fec::abcd:b/64',
  64. ])
  65. self.assertEqual(sorted(expand_ipaddress_pattern(input, 6)), output)
  66. def test_ipv6_range_multichar_field(self):
  67. input = 'fec::abcd:[f-11]/64'
  68. output = sorted([
  69. 'fec::abcd:f/64',
  70. 'fec::abcd:10/64',
  71. 'fec::abcd:11/64',
  72. ])
  73. self.assertEqual(sorted(expand_ipaddress_pattern(input, 6)), output)
  74. def test_ipv6_set(self):
  75. input = 'fec::abcd:[9,ab]/64'
  76. output = sorted([
  77. 'fec::abcd:9/64',
  78. 'fec::abcd:ab/64',
  79. ])
  80. self.assertEqual(sorted(expand_ipaddress_pattern(input, 6)), output)
  81. def test_ipv6_multiple_ranges(self):
  82. input = 'fec::[1-2]bcd:[9-b]/64'
  83. output = sorted([
  84. 'fec::1bcd:9/64',
  85. 'fec::1bcd:a/64',
  86. 'fec::1bcd:b/64',
  87. 'fec::2bcd:9/64',
  88. 'fec::2bcd:a/64',
  89. 'fec::2bcd:b/64',
  90. ])
  91. self.assertEqual(sorted(expand_ipaddress_pattern(input, 6)), output)
  92. def test_ipv6_multiple_sets(self):
  93. input = 'fec::[a,f]bcd:[9,ab]/64'
  94. output = sorted([
  95. 'fec::abcd:9/64',
  96. 'fec::abcd:ab/64',
  97. 'fec::fbcd:9/64',
  98. 'fec::fbcd:ab/64',
  99. ])
  100. self.assertEqual(sorted(expand_ipaddress_pattern(input, 6)), output)
  101. def test_ipv6_set_and_range(self):
  102. input = 'fec::[dead,beaf]:[9-b]/64'
  103. output = sorted([
  104. 'fec::dead:9/64',
  105. 'fec::dead:a/64',
  106. 'fec::dead:b/64',
  107. 'fec::beaf:9/64',
  108. 'fec::beaf:a/64',
  109. 'fec::beaf:b/64',
  110. ])
  111. self.assertEqual(sorted(expand_ipaddress_pattern(input, 6)), output)
  112. def test_invalid_address_family(self):
  113. with self.assertRaisesRegex(Exception, 'Invalid IP address family: 5'):
  114. sorted(expand_ipaddress_pattern(None, 5))
  115. def test_invalid_non_pattern(self):
  116. with self.assertRaises(ValueError):
  117. sorted(expand_ipaddress_pattern('1.2.3.4/32', 4))
  118. def test_invalid_range(self):
  119. with self.assertRaises(ValueError):
  120. sorted(expand_ipaddress_pattern('1.2.3.[4-]/32', 4))
  121. with self.assertRaises(ValueError):
  122. sorted(expand_ipaddress_pattern('1.2.3.[-4]/32', 4))
  123. with self.assertRaises(ValueError):
  124. sorted(expand_ipaddress_pattern('1.2.3.[4--5]/32', 4))
  125. def test_invalid_range_bounds(self):
  126. self.assertEqual(sorted(expand_ipaddress_pattern('1.2.3.[4-3]/32', 6)), [])
  127. def test_invalid_set(self):
  128. with self.assertRaises(ValueError):
  129. sorted(expand_ipaddress_pattern('1.2.3.[4]/32', 4))
  130. with self.assertRaises(ValueError):
  131. sorted(expand_ipaddress_pattern('1.2.3.[4,]/32', 4))
  132. with self.assertRaises(ValueError):
  133. sorted(expand_ipaddress_pattern('1.2.3.[,4]/32', 4))
  134. with self.assertRaises(ValueError):
  135. sorted(expand_ipaddress_pattern('1.2.3.[4,,5]/32', 4))
  136. class ExpandAlphanumeric(TestCase):
  137. """
  138. Validate the operation of expand_alphanumeric_pattern().
  139. """
  140. def test_range_numberic(self):
  141. input = 'r[9-11]a'
  142. output = sorted([
  143. 'r9a',
  144. 'r10a',
  145. 'r11a',
  146. ])
  147. self.assertEqual(sorted(expand_alphanumeric_pattern(input)), output)
  148. def test_range_alpha(self):
  149. input = '[r-t]1a'
  150. output = sorted([
  151. 'r1a',
  152. 's1a',
  153. 't1a',
  154. ])
  155. self.assertEqual(sorted(expand_alphanumeric_pattern(input)), output)
  156. def test_set_numeric(self):
  157. input = 'r[1,2]a'
  158. output = sorted([
  159. 'r1a',
  160. 'r2a',
  161. ])
  162. self.assertEqual(sorted(expand_alphanumeric_pattern(input)), output)
  163. def test_set_alpha(self):
  164. input = '[r,t]1a'
  165. output = sorted([
  166. 'r1a',
  167. 't1a',
  168. ])
  169. self.assertEqual(sorted(expand_alphanumeric_pattern(input)), output)
  170. def test_set_multichar(self):
  171. input = '[ra,tb]1a'
  172. output = sorted([
  173. 'ra1a',
  174. 'tb1a',
  175. ])
  176. self.assertEqual(sorted(expand_alphanumeric_pattern(input)), output)
  177. def test_multiple_ranges(self):
  178. input = '[r-t]1[a-b]'
  179. output = sorted([
  180. 'r1a',
  181. 'r1b',
  182. 's1a',
  183. 's1b',
  184. 't1a',
  185. 't1b',
  186. ])
  187. self.assertEqual(sorted(expand_alphanumeric_pattern(input)), output)
  188. def test_multiple_sets(self):
  189. input = '[ra,tb]1[ax,by]'
  190. output = sorted([
  191. 'ra1ax',
  192. 'ra1by',
  193. 'tb1ax',
  194. 'tb1by',
  195. ])
  196. self.assertEqual(sorted(expand_alphanumeric_pattern(input)), output)
  197. def test_set_and_range(self):
  198. input = '[ra,tb]1[a-c]'
  199. output = sorted([
  200. 'ra1a',
  201. 'ra1b',
  202. 'ra1c',
  203. 'tb1a',
  204. 'tb1b',
  205. 'tb1c',
  206. ])
  207. self.assertEqual(sorted(expand_alphanumeric_pattern(input)), output)
  208. def test_invalid_non_pattern(self):
  209. with self.assertRaises(ValueError):
  210. sorted(expand_alphanumeric_pattern('r9a'))
  211. def test_invalid_range(self):
  212. with self.assertRaises(ValueError):
  213. sorted(expand_alphanumeric_pattern('r[8-]a'))
  214. with self.assertRaises(ValueError):
  215. sorted(expand_alphanumeric_pattern('r[-8]a'))
  216. with self.assertRaises(ValueError):
  217. sorted(expand_alphanumeric_pattern('r[8--9]a'))
  218. def test_invalid_range_alphanumeric(self):
  219. self.assertEqual(sorted(expand_alphanumeric_pattern('r[9-a]a')), [])
  220. self.assertEqual(sorted(expand_alphanumeric_pattern('r[a-9]a')), [])
  221. def test_invalid_range_bounds(self):
  222. with self.assertRaises(forms.ValidationError):
  223. sorted(expand_alphanumeric_pattern('r[9-8]a'))
  224. sorted(expand_alphanumeric_pattern('r[b-a]a'))
  225. def test_invalid_range_len(self):
  226. with self.assertRaises(forms.ValidationError):
  227. sorted(expand_alphanumeric_pattern('r[a-bb]a'))
  228. def test_invalid_set(self):
  229. with self.assertRaises(ValueError):
  230. sorted(expand_alphanumeric_pattern('r[a]a'))
  231. with self.assertRaises(ValueError):
  232. sorted(expand_alphanumeric_pattern('r[a,]a'))
  233. with self.assertRaises(ValueError):
  234. sorted(expand_alphanumeric_pattern('r[,a]a'))
  235. with self.assertRaises(ValueError):
  236. sorted(expand_alphanumeric_pattern('r[a,,b]a'))
  237. class ImportFormTest(TestCase):
  238. def test_format_detection(self):
  239. form = BulkImportForm()
  240. data = (
  241. "a,b,c\n"
  242. "1,2,3\n"
  243. "4,5,6\n"
  244. )
  245. self.assertEqual(form._detect_format(data), ImportFormatChoices.CSV)
  246. data = '{"a": 1, "b": 2, "c": 3"}'
  247. self.assertEqual(form._detect_format(data), ImportFormatChoices.JSON)
  248. data = '[{"a": 1, "b": 2, "c": 3"}, {"a": 4, "b": 5, "c": 6"}]'
  249. self.assertEqual(form._detect_format(data), ImportFormatChoices.JSON)
  250. data = (
  251. "- a: 1\n"
  252. " b: 2\n"
  253. " c: 3\n"
  254. "- a: 4\n"
  255. " b: 5\n"
  256. " c: 6\n"
  257. )
  258. self.assertEqual(form._detect_format(data), ImportFormatChoices.YAML)
  259. data = (
  260. "---\n"
  261. "a: 1\n"
  262. "b: 2\n"
  263. "c: 3\n"
  264. "---\n"
  265. "a: 4\n"
  266. "b: 5\n"
  267. "c: 6\n"
  268. )
  269. self.assertEqual(form._detect_format(data), ImportFormatChoices.YAML)
  270. # Invalid data
  271. with self.assertRaises(forms.ValidationError):
  272. form._detect_format('')
  273. with self.assertRaises(forms.ValidationError):
  274. form._detect_format('?')
  275. def test_csv_delimiters(self):
  276. form = BulkImportForm()
  277. data = (
  278. "a,b,c\n"
  279. "1,2,3\n"
  280. "4,5,6\n"
  281. )
  282. self.assertEqual(form._clean_csv(data, delimiter=','), [
  283. {'a': '1', 'b': '2', 'c': '3'},
  284. {'a': '4', 'b': '5', 'c': '6'},
  285. ])
  286. data = (
  287. "a;b;c\n"
  288. "1;2;3\n"
  289. "4;5;6\n"
  290. )
  291. self.assertEqual(form._clean_csv(data, delimiter=';'), [
  292. {'a': '1', 'b': '2', 'c': '3'},
  293. {'a': '4', 'b': '5', 'c': '6'},
  294. ])
  295. data = (
  296. "a\tb\tc\n"
  297. "1\t2\t3\n"
  298. "4\t5\t6\n"
  299. )
  300. self.assertEqual(form._clean_csv(data, delimiter='\t'), [
  301. {'a': '1', 'b': '2', 'c': '3'},
  302. {'a': '4', 'b': '5', 'c': '6'},
  303. ])
  304. class BulkRenameFormTest(TestCase):
  305. def test_no_strip_whitespace(self):
  306. # Tests to make sure Bulk Rename Form isn't stripping whitespaces
  307. # See: https://github.com/netbox-community/netbox/issues/13791
  308. form = BulkRenameForm(data={
  309. "find": " hello ",
  310. "replace": " world "
  311. })
  312. self.assertTrue(form.is_valid())
  313. self.assertEqual(form.cleaned_data["find"], " hello ")
  314. self.assertEqual(form.cleaned_data["replace"], " world ")
  315. class GetFieldValueTest(TestCase):
  316. @classmethod
  317. def setUpTestData(cls):
  318. class TestForm(forms.Form):
  319. site = forms.ModelChoiceField(
  320. queryset=Site.objects.all(),
  321. required=False
  322. )
  323. cls.form_class = TestForm
  324. cls.sites = (
  325. Site(name='Test Site 1', slug='test-site-1'),
  326. Site(name='Test Site 2', slug='test-site-2'),
  327. )
  328. Site.objects.bulk_create(cls.sites)
  329. def test_unbound_without_initial(self):
  330. form = self.form_class()
  331. self.assertEqual(
  332. get_field_value(form, 'site'),
  333. None
  334. )
  335. def test_unbound_with_initial(self):
  336. form = self.form_class(initial={'site': self.sites[0].pk})
  337. self.assertEqual(
  338. get_field_value(form, 'site'),
  339. self.sites[0].pk
  340. )
  341. def test_bound_value_without_initial(self):
  342. form = self.form_class({'site': self.sites[0].pk})
  343. self.assertEqual(
  344. get_field_value(form, 'site'),
  345. self.sites[0].pk
  346. )
  347. def test_bound_value_with_initial(self):
  348. form = self.form_class({'site': self.sites[0].pk}, initial={'site': self.sites[1].pk})
  349. self.assertEqual(
  350. get_field_value(form, 'site'),
  351. self.sites[0].pk
  352. )
  353. def test_bound_null_without_initial(self):
  354. form = self.form_class({'site': None})
  355. self.assertEqual(
  356. get_field_value(form, 'site'),
  357. None
  358. )
  359. def test_bound_null_with_initial(self):
  360. form = self.form_class({'site': None}, initial={'site': self.sites[1].pk})
  361. self.assertEqual(
  362. get_field_value(form, 'site'),
  363. None
  364. )
  365. class CSVSelectWidgetTest(TestCase):
  366. """
  367. Validate that CSVSelectWidget treats blank values as omitted.
  368. This allows model defaults to be applied when CSV fields are present but empty.
  369. Related to issue #20645.
  370. """
  371. def test_blank_value_treated_as_omitted(self):
  372. """Test that blank string values are treated as omitted"""
  373. widget = CSVSelectWidget()
  374. data = {'test_field': ''}
  375. self.assertTrue(widget.value_omitted_from_data(data, {}, 'test_field'))
  376. def test_none_value_treated_as_omitted(self):
  377. """Test that None values are treated as omitted"""
  378. widget = CSVSelectWidget()
  379. data = {'test_field': None}
  380. self.assertTrue(widget.value_omitted_from_data(data, {}, 'test_field'))
  381. def test_missing_field_treated_as_omitted(self):
  382. """Test that missing fields are treated as omitted"""
  383. widget = CSVSelectWidget()
  384. data = {}
  385. self.assertTrue(widget.value_omitted_from_data(data, {}, 'test_field'))
  386. def test_valid_value_not_omitted(self):
  387. """Test that valid values are not treated as omitted"""
  388. widget = CSVSelectWidget()
  389. data = {'test_field': 'valid_value'}
  390. self.assertFalse(widget.value_omitted_from_data(data, {}, 'test_field'))