test_views.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. #! /usr/bin/env python3
  2. #
  3. # BitBake Toaster Implementation
  4. #
  5. # Copyright (C) 2013-2015 Intel Corporation
  6. #
  7. # SPDX-License-Identifier: GPL-2.0-only
  8. #
  9. """Test cases for Toaster GUI and ReST."""
  10. from django.test import TestCase
  11. from django.test.client import RequestFactory
  12. from django.urls import reverse
  13. from django.db.models import Q
  14. from orm.models import Project, Package
  15. from orm.models import Layer_Version, Recipe
  16. from orm.models import CustomImageRecipe
  17. from orm.models import CustomImagePackage
  18. import inspect
  19. import toastergui
  20. from toastergui.tables import SoftwareRecipesTable
  21. import json
  22. from bs4 import BeautifulSoup
  23. import string
  24. PROJECT_NAME = "test project"
  25. PROJECT_NAME2 = "test project 2"
  26. CLI_BUILDS_PROJECT_NAME = 'Command line builds'
  27. class ViewTests(TestCase):
  28. """Tests to verify view APIs."""
  29. fixtures = ['toastergui-unittest-data']
  30. def setUp(self):
  31. self.project = Project.objects.first()
  32. self.recipe1 = Recipe.objects.get(pk=2)
  33. self.customr = CustomImageRecipe.objects.first()
  34. self.cust_package = CustomImagePackage.objects.first()
  35. self.package = Package.objects.first()
  36. self.lver = Layer_Version.objects.first()
  37. def test_get_base_call_returns_html(self):
  38. """Basic test for all-projects view"""
  39. response = self.client.get(reverse('all-projects'), follow=True)
  40. self.assertEqual(response.status_code, 200)
  41. self.assertTrue(response['Content-Type'].startswith('text/html'))
  42. self.assertTemplateUsed(response, "projects-toastertable.html")
  43. def test_get_json_call_returns_json(self):
  44. """Test for all projects output in json format"""
  45. url = reverse('all-projects')
  46. response = self.client.get(url, {"format": "json"}, follow=True)
  47. self.assertEqual(response.status_code, 200)
  48. self.assertTrue(response['Content-Type'].startswith(
  49. 'application/json'))
  50. data = json.loads(response.content.decode('utf-8'))
  51. self.assertTrue("error" in data)
  52. self.assertEqual(data["error"], "ok")
  53. self.assertTrue("rows" in data)
  54. name_found = False
  55. for row in data["rows"]:
  56. name_found = row['name'].find(self.project.name)
  57. self.assertTrue(name_found,
  58. "project name not found in projects table")
  59. def test_typeaheads(self):
  60. """Test typeahead ReST API"""
  61. layers_url = reverse('xhr_layerstypeahead', args=(self.project.id,))
  62. prj_url = reverse('xhr_projectstypeahead')
  63. urls = [layers_url,
  64. prj_url,
  65. reverse('xhr_recipestypeahead', args=(self.project.id,)),
  66. reverse('xhr_machinestypeahead', args=(self.project.id,))]
  67. def basic_reponse_check(response, url):
  68. """Check data structure of http response."""
  69. self.assertEqual(response.status_code, 200)
  70. self.assertTrue(response['Content-Type'].startswith(
  71. 'application/json'))
  72. data = json.loads(response.content.decode('utf-8'))
  73. self.assertTrue("error" in data)
  74. self.assertEqual(data["error"], "ok")
  75. self.assertTrue("results" in data)
  76. # We got a result so now check the fields
  77. if len(data['results']) > 0:
  78. result = data['results'][0]
  79. self.assertTrue(len(result['name']) > 0)
  80. self.assertTrue("detail" in result)
  81. self.assertTrue(result['id'] > 0)
  82. # Special check for the layers typeahead's extra fields
  83. if url == layers_url:
  84. self.assertTrue(len(result['layerdetailurl']) > 0)
  85. self.assertTrue(len(result['vcs_url']) > 0)
  86. self.assertTrue(len(result['vcs_reference']) > 0)
  87. # Special check for project typeahead extra fields
  88. elif url == prj_url:
  89. self.assertTrue(len(result['projectPageUrl']) > 0)
  90. return True
  91. return False
  92. for url in urls:
  93. results = False
  94. for typeing in list(string.ascii_letters):
  95. response = self.client.get(url, {'search': typeing})
  96. results = basic_reponse_check(response, url)
  97. if results:
  98. break
  99. # After "typeing" the alpabet we should have result true
  100. # from each of the urls
  101. self.assertTrue(results)
  102. def test_xhr_add_layer(self):
  103. """Test xhr_add API"""
  104. # Test for importing an already existing layer
  105. api_url = reverse('xhr_layer', args=(self.project.id,))
  106. layer_data = {'vcs_url': "git://git.example.com/test",
  107. 'name': "base-layer",
  108. 'git_ref': "c12b9596afd236116b25ce26dbe0d793de9dc7ce",
  109. 'project_id': self.project.id,
  110. 'local_source_dir': "",
  111. 'add_to_project': True,
  112. 'dir_path': "/path/in/repository"}
  113. layer_data_json = json.dumps(layer_data)
  114. response = self.client.put(api_url, layer_data_json)
  115. data = json.loads(response.content.decode('utf-8'))
  116. self.assertEqual(response.status_code, 200)
  117. self.assertEqual(data["error"], "ok")
  118. self.assertTrue(
  119. layer_data['name'] in
  120. self.project.get_all_compatible_layer_versions().values_list(
  121. 'layer__name',
  122. flat=True),
  123. "Could not find imported layer in project's all layers list"
  124. )
  125. # Empty data passed
  126. response = self.client.put(api_url, "{}")
  127. data = json.loads(response.content.decode('utf-8'))
  128. self.assertNotEqual(data["error"], "ok")
  129. def test_custom_ok(self):
  130. """Test successful return from ReST API xhr_customrecipe"""
  131. url = reverse('xhr_customrecipe')
  132. params = {'name': 'custom', 'project': self.project.id,
  133. 'base': self.recipe1.id}
  134. response = self.client.post(url, params)
  135. self.assertEqual(response.status_code, 200)
  136. data = json.loads(response.content.decode('utf-8'))
  137. self.assertEqual(data['error'], 'ok')
  138. self.assertTrue('url' in data)
  139. # get recipe from the database
  140. recipe = CustomImageRecipe.objects.get(project=self.project,
  141. name=params['name'])
  142. args = (self.project.id, recipe.id,)
  143. self.assertEqual(reverse('customrecipe', args=args), data['url'])
  144. def test_custom_incomplete_params(self):
  145. """Test not passing all required parameters to xhr_customrecipe"""
  146. url = reverse('xhr_customrecipe')
  147. for params in [{}, {'name': 'custom'},
  148. {'name': 'custom', 'project': self.project.id}]:
  149. response = self.client.post(url, params)
  150. self.assertEqual(response.status_code, 200)
  151. data = json.loads(response.content.decode('utf-8'))
  152. self.assertNotEqual(data["error"], "ok")
  153. def test_xhr_custom_wrong_project(self):
  154. """Test passing wrong project id to xhr_customrecipe"""
  155. url = reverse('xhr_customrecipe')
  156. params = {'name': 'custom', 'project': 0, "base": self.recipe1.id}
  157. response = self.client.post(url, params)
  158. self.assertEqual(response.status_code, 200)
  159. data = json.loads(response.content.decode('utf-8'))
  160. self.assertNotEqual(data["error"], "ok")
  161. def test_xhr_custom_wrong_base(self):
  162. """Test passing wrong base recipe id to xhr_customrecipe"""
  163. url = reverse('xhr_customrecipe')
  164. params = {'name': 'custom', 'project': self.project.id, "base": 0}
  165. response = self.client.post(url, params)
  166. self.assertEqual(response.status_code, 200)
  167. data = json.loads(response.content.decode('utf-8'))
  168. self.assertNotEqual(data["error"], "ok")
  169. def test_xhr_custom_details(self):
  170. """Test getting custom recipe details"""
  171. url = reverse('xhr_customrecipe_id', args=(self.customr.id,))
  172. response = self.client.get(url)
  173. self.assertEqual(response.status_code, 200)
  174. expected = {"error": "ok",
  175. "info": {'id': self.customr.id,
  176. 'name': self.customr.name,
  177. 'base_recipe_id': self.recipe1.id,
  178. 'project_id': self.project.id}}
  179. self.assertEqual(json.loads(response.content.decode('utf-8')),
  180. expected)
  181. def test_xhr_custom_del(self):
  182. """Test deleting custom recipe"""
  183. name = "to be deleted"
  184. recipe = CustomImageRecipe.objects.create(
  185. name=name, project=self.project,
  186. base_recipe=self.recipe1,
  187. file_path="/tmp/testing",
  188. layer_version=self.customr.layer_version)
  189. url = reverse('xhr_customrecipe_id', args=(recipe.id,))
  190. response = self.client.delete(url)
  191. self.assertEqual(response.status_code, 200)
  192. gotoUrl = reverse('projectcustomimages', args=(self.project.pk,))
  193. self.assertEqual(json.loads(response.content.decode('utf-8')),
  194. {"error": "ok",
  195. "gotoUrl": gotoUrl})
  196. # try to delete not-existent recipe
  197. url = reverse('xhr_customrecipe_id', args=(recipe.id,))
  198. response = self.client.delete(url)
  199. self.assertEqual(response.status_code, 200)
  200. self.assertNotEqual(json.loads(
  201. response.content.decode('utf-8'))["error"], "ok")
  202. def test_xhr_custom_packages(self):
  203. """Test adding and deleting package to a custom recipe"""
  204. # add self.package to recipe
  205. response = self.client.put(reverse('xhr_customrecipe_packages',
  206. args=(self.customr.id,
  207. self.cust_package.id)))
  208. self.assertEqual(response.status_code, 200)
  209. self.assertEqual(json.loads(response.content.decode('utf-8')),
  210. {"error": "ok"})
  211. self.assertEqual(self.customr.appends_set.first().name,
  212. self.cust_package.name)
  213. # delete it
  214. to_delete = self.customr.appends_set.first().pk
  215. del_url = reverse('xhr_customrecipe_packages',
  216. args=(self.customr.id, to_delete))
  217. response = self.client.delete(del_url)
  218. self.assertEqual(response.status_code, 200)
  219. self.assertEqual(json.loads(response.content.decode('utf-8')),
  220. {"error": "ok"})
  221. all_packages = self.customr.get_all_packages().values_list('pk',
  222. flat=True)
  223. self.assertFalse(to_delete in all_packages)
  224. # delete invalid package to test error condition
  225. del_url = reverse('xhr_customrecipe_packages',
  226. args=(self.customr.id,
  227. 99999))
  228. response = self.client.delete(del_url)
  229. self.assertEqual(response.status_code, 200)
  230. self.assertNotEqual(json.loads(
  231. response.content.decode('utf-8'))["error"], "ok")
  232. def test_xhr_custom_packages_err(self):
  233. """Test error conditions of xhr_customrecipe_packages"""
  234. # test calls with wrong recipe id and wrong package id
  235. for args in [(0, self.package.id), (self.customr.id, 0)]:
  236. url = reverse('xhr_customrecipe_packages', args=args)
  237. # test put and delete methods
  238. for method in (self.client.put, self.client.delete):
  239. response = method(url)
  240. self.assertEqual(response.status_code, 200)
  241. self.assertNotEqual(json.loads(
  242. response.content.decode('utf-8')),
  243. {"error": "ok"})
  244. def test_download_custom_recipe(self):
  245. """Download the recipe file generated for the custom image"""
  246. # Create a dummy recipe file for the custom image generation to read
  247. open("/tmp/a_recipe.bb", 'a').close()
  248. response = self.client.get(reverse('customrecipedownload',
  249. args=(self.project.id,
  250. self.customr.id)))
  251. self.assertEqual(response.status_code, 200)
  252. def test_software_recipes_table(self):
  253. """Test structure returned for Software RecipesTable"""
  254. table = SoftwareRecipesTable()
  255. request = RequestFactory().get('/foo/', {'format': 'json'})
  256. response = table.get(request, pid=self.project.id)
  257. data = json.loads(response.content.decode('utf-8'))
  258. recipes = Recipe.objects.filter(Q(is_image=False))
  259. self.assertTrue(len(recipes) > 1,
  260. "Need more than one software recipe to test "
  261. "SoftwareRecipesTable")
  262. recipe1 = recipes[0]
  263. recipe2 = recipes[1]
  264. rows = data['rows']
  265. row1 = next(x for x in rows if x['name'] == recipe1.name)
  266. row2 = next(x for x in rows if x['name'] == recipe2.name)
  267. self.assertEqual(response.status_code, 200, 'should be 200 OK status')
  268. # check other columns have been populated correctly
  269. self.assertTrue(recipe1.name in row1['name'])
  270. self.assertTrue(recipe1.version in row1['version'])
  271. self.assertTrue(recipe1.description in
  272. row1['get_description_or_summary'])
  273. self.assertTrue(recipe1.layer_version.layer.name in
  274. row1['layer_version__layer__name'])
  275. self.assertTrue(recipe2.name in row2['name'])
  276. self.assertTrue(recipe2.version in row2['version'])
  277. self.assertTrue(recipe2.description in
  278. row2['get_description_or_summary'])
  279. self.assertTrue(recipe2.layer_version.layer.name in
  280. row2['layer_version__layer__name'])
  281. def test_toaster_tables(self):
  282. """Test all ToasterTables instances"""
  283. def get_data(table, options={}):
  284. """Send a request and parse the json response"""
  285. options['format'] = "json"
  286. options['nocache'] = "true"
  287. request = RequestFactory().get('/', options)
  288. # This is the image recipe needed for a package list for
  289. # PackagesTable do this here to throw a non exist exception
  290. image_recipe = Recipe.objects.get(pk=4)
  291. # Add any kwargs that are needed by any of the possible tables
  292. args = {'pid': self.project.id,
  293. 'layerid': self.lver.pk,
  294. 'recipeid': self.recipe1.pk,
  295. 'recipe_id': image_recipe.pk,
  296. 'custrecipeid': self.customr.pk,
  297. 'build_id': 1,
  298. 'target_id': 1}
  299. response = table.get(request, **args)
  300. return json.loads(response.content.decode('utf-8'))
  301. def get_text_from_td(td):
  302. """If we have html in the td then extract the text portion"""
  303. # just so we don't waste time parsing non html
  304. if "<" not in td:
  305. ret = td
  306. else:
  307. ret = BeautifulSoup(td, "html.parser").text
  308. if len(ret):
  309. return "0"
  310. else:
  311. return ret
  312. # Get a list of classes in tables module
  313. tables = inspect.getmembers(toastergui.tables, inspect.isclass)
  314. tables.extend(inspect.getmembers(toastergui.buildtables,
  315. inspect.isclass))
  316. for name, table_cls in tables:
  317. # Filter out the non ToasterTables from the tables module
  318. if not issubclass(table_cls, toastergui.widgets.ToasterTable) or \
  319. table_cls == toastergui.widgets.ToasterTable or \
  320. 'Mixin' in name:
  321. continue
  322. # Get the table data without any options, this also does the
  323. # initialisation of the table i.e. setup_columns,
  324. # setup_filters and setup_queryset that we can use later
  325. table = table_cls()
  326. all_data = get_data(table)
  327. self.assertTrue(len(all_data['rows']) > 1,
  328. "Cannot test on a %s table with < 1 row" % name)
  329. if table.default_orderby:
  330. row_one = get_text_from_td(
  331. all_data['rows'][0][table.default_orderby.strip("-")])
  332. row_two = get_text_from_td(
  333. all_data['rows'][1][table.default_orderby.strip("-")])
  334. if '-' in table.default_orderby:
  335. self.assertTrue(row_one >= row_two,
  336. "Default ordering not working on %s"
  337. " '%s' should be >= '%s'" %
  338. (name, row_one, row_two))
  339. else:
  340. self.assertTrue(row_one <= row_two,
  341. "Default ordering not working on %s"
  342. " '%s' should be <= '%s'" %
  343. (name, row_one, row_two))
  344. # Test the column ordering and filtering functionality
  345. for column in table.columns:
  346. if column['orderable']:
  347. # If a column is orderable test it in both order
  348. # directions ordering on the columns field_name
  349. ascending = get_data(table_cls(),
  350. {"orderby": column['field_name']})
  351. row_one = get_text_from_td(
  352. ascending['rows'][0][column['field_name']])
  353. row_two = get_text_from_td(
  354. ascending['rows'][1][column['field_name']])
  355. self.assertTrue(row_one <= row_two,
  356. "Ascending sort applied but row 0: \"%s\""
  357. " is less than row 1: \"%s\" "
  358. "%s %s " %
  359. (row_one, row_two,
  360. column['field_name'], name))
  361. descending = get_data(table_cls(),
  362. {"orderby":
  363. '-'+column['field_name']})
  364. row_one = get_text_from_td(
  365. descending['rows'][0][column['field_name']])
  366. row_two = get_text_from_td(
  367. descending['rows'][1][column['field_name']])
  368. self.assertTrue(row_one >= row_two,
  369. "Descending sort applied but row 0: %s"
  370. "is greater than row 1: %s"
  371. "field %s table %s" %
  372. (row_one,
  373. row_two,
  374. column['field_name'], name))
  375. # If the two start rows are the same we haven't actually
  376. # changed the order
  377. self.assertNotEqual(ascending['rows'][0],
  378. descending['rows'][0],
  379. "An orderby %s has not changed the "
  380. "order of the data in table %s" %
  381. (column['field_name'], name))
  382. if column['filter_name']:
  383. # If a filter is available for the column get the filter
  384. # info. This contains what filter actions are defined.
  385. filter_info = get_data(table_cls(),
  386. {"cmd": "filterinfo",
  387. "name": column['filter_name']})
  388. self.assertTrue(len(filter_info['filter_actions']) > 0,
  389. "Filter %s was defined but no actions "
  390. "added to it" % column['filter_name'])
  391. for filter_action in filter_info['filter_actions']:
  392. # filter string to pass as the option
  393. # This is the name of the filter:action
  394. # e.g. project_filter:not_in_project
  395. filter_string = "%s:%s" % (
  396. column['filter_name'],
  397. filter_action['action_name'])
  398. # Now get the data with the filter applied
  399. filtered_data = get_data(table_cls(),
  400. {"filter": filter_string})
  401. # date range filter actions can't specify the
  402. # number of results they return, so their count is 0
  403. if filter_action['count'] is not None:
  404. self.assertEqual(
  405. len(filtered_data['rows']),
  406. int(filter_action['count']),
  407. "We added a table filter for %s but "
  408. "the number of rows returned was not "
  409. "what the filter info said there "
  410. "would be" % name)
  411. # Test search functionality on the table
  412. something_found = False
  413. for search in list(string.ascii_letters):
  414. search_data = get_data(table_cls(), {'search': search})
  415. if len(search_data['rows']) > 0:
  416. something_found = True
  417. break
  418. self.assertTrue(something_found,
  419. "We went through the whole alphabet and nothing"
  420. " was found for the search of table %s" % name)
  421. # Test the limit functionality on the table
  422. limited_data = get_data(table_cls(), {'limit': "1"})
  423. self.assertEqual(len(limited_data['rows']),
  424. 1,
  425. "Limit 1 set on table %s but not 1 row returned"
  426. % name)
  427. # Test the pagination functionality on the table
  428. page_one_data = get_data(table_cls(), {'limit': "1",
  429. "page": "1"})['rows'][0]
  430. page_two_data = get_data(table_cls(), {'limit': "1",
  431. "page": "2"})['rows'][0]
  432. self.assertNotEqual(page_one_data,
  433. page_two_data,
  434. "Changed page on table %s but first row is"
  435. " the same as the previous page" % name)