Coverage for CIResults/tests/test_rest_views.py: 100%

614 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-13 07:21 +0000

1import json 

2import datetime 

3 

4from unittest.mock import patch, MagicMock 

5from rest_framework.test import APIRequestFactory, APITestCase 

6from django.conf import settings 

7from django.db import transaction 

8from django.test import TestCase 

9from django.urls import reverse 

10from model_bakery import baker 

11 

12from CIResults.models import ( 

13 Bug, 

14 BugTracker, 

15 Build, 

16 Component, 

17 Issue, 

18 IssueFilter, 

19 Machine, 

20 MachineTag, 

21 RunConfig, 

22 RunConfigTag, 

23 Test, 

24 TestResult, 

25 TestSuite, 

26 TextStatus, 

27 UnknownFailure, 

28) 

29from CIResults.rest_views import CustomPagination, IssueFilterViewSet, RunConfigViewSet, get_obj_by_id_or_name 

30from CIResults.rest_views import BuildViewSet 

31from CIResults.tests.test_views import create_user_and_log_in 

32 

33from shortener.models import Shortener 

34 

35# HACK: Massively speed up the login primitive. We don't care about security in tests 

36settings.PASSWORD_HASHERS = ('django.contrib.auth.hashers.MD5PasswordHasher', ) 

37 

38 

39class UtilsTests(TestCase): 

40 def test_get_obj_by_id_or_name__id(self): 

41 rc = RunConfig.objects.create(name='valid', temporary=True) 

42 self.assertEqual(get_obj_by_id_or_name(RunConfig, 1), rc) 

43 

44 def test_get_obj_by_id_or_name__name(self): 

45 rc = RunConfig.objects.create(name='valid', temporary=True) 

46 self.assertEqual(get_obj_by_id_or_name(RunConfig, "valid"), rc) 

47 

48 def test_get_obj_by_id_or_name__not_exist(self): 

49 self.assertRaises(RunConfig.DoesNotExist, get_obj_by_id_or_name, RunConfig, 1) 

50 

51 

52class CustomPaginationTests(TestCase): 

53 def get_page_size(self, query_params, page_size_query_param='page_size', max_page_size=None): 

54 request = MagicMock(query_params=query_params) 

55 

56 pagination = CustomPagination() 

57 pagination.page_size_query_param = page_size_query_param 

58 pagination.max_page_size = max_page_size 

59 

60 return pagination.get_page_size(request) 

61 

62 def test_default_page_size(self): 

63 self.assertEqual(self.get_page_size({}), CustomPagination.page_size) 

64 

65 def test_default_page_size_without_page_size_field(self): 

66 self.assertEqual(self.get_page_size({}, page_size_query_param=None), CustomPagination.page_size) 

67 

68 def test_invalid_page_size(self): 

69 self.assertEqual(self.get_page_size({'page_size': 'toto'}), CustomPagination.page_size) 

70 

71 def test_negative_page_size(self): 

72 self.assertEqual(self.get_page_size({CustomPagination.page_size_query_param: -0}), None) 

73 

74 def test_page_size_too_big(self): 

75 self.assertEqual(self.get_page_size({'page_size': '1000'}, max_page_size=10), 10) 

76 

77 def test_page_size_big_but_no_limits(self): 

78 self.assertEqual(self.get_page_size({'page_size': '1000'}, max_page_size=None), 1000) 

79 

80 def test_acceptable_page_size(self): 

81 self.assertEqual(self.get_page_size({'page_size2': '42'}, page_size_query_param='page_size2'), 42) 

82 

83 

84class IssueFilterTests(APITestCase): 

85 maxDiff = None 

86 

87 def setUp(self): 

88 self.view = IssueFilterViewSet() 

89 self.user = None 

90 

91 def __post__(self, body_dict, logged_in=True, permissions=['add_issuefilter']): 

92 if logged_in: 

93 self.user = create_user_and_log_in(self.client, permissions=permissions) 

94 

95 return self.client.post(reverse('api:issuefilter-list'), body_dict, format='json') 

96 

97 def test__get_or_None__empty_field(self): 

98 errors = [] 

99 self.assertIsNone(self.view.__get_or_None__(Machine, 'field', {'field': ''}, errors)) 

100 self.assertEqual(errors, []) 

101 

102 def test__get_or_None__invalid_id(self): 

103 errors = [] 

104 self.assertIsNone(self.view.__get_or_None__(Machine, 'field', {'field': '12'}, errors)) 

105 self.assertEqual(errors, ["The object referenced by 'field' does not exist"]) 

106 

107 def test_get_filter_by_description(self): 

108 IssueFilter.objects.create(description="filter one") 

109 IssueFilter.objects.create(description="filter two") 

110 response = self.client.get(reverse('api:issuefilter-list'), {'description': 'one'}) 

111 self.assertEqual(response.status_code, 200) 

112 self.assertEqual(response.data["count"], 1) 

113 self.assertEqual(response.data["results"][0]["description"], "filter one") 

114 

115 def test_create_empty(self): 

116 response = self.__post__({}) 

117 self.assertEqual(response.status_code, 400) 

118 self.assertEqual(response.data, ["The field 'description' cannot be empty"]) 

119 

120 def test_invalid_regexps(self): 

121 response = self.__post__({"description": "Minimal IssueFilter", 

122 "stdout_regex": '(', "stderr_regex": '(', 

123 "dmesg_regex": '('}) 

124 self.assertEqual(response.status_code, 400) 

125 self.assertEqual(response.data, ["The field 'stdout_regex' does not contain a valid regular expression", 

126 "The field 'stderr_regex' does not contain a valid regular expression", 

127 "The field 'dmesg_regex' does not contain a valid regular expression"]) 

128 

129 def test_create_minimal__unauthenticated(self): 

130 response = self.__post__({"description": "Minimal IssueFilter"}, logged_in=False) 

131 self.assertEqual(response.status_code, 401) 

132 

133 # FIXME: This is not currently working 

134 # def test_create_minimal__invalid_permissions(self): 

135 # response = self.__post__({"description": "Minimal IssueFilter"}, logged_in=True, permissions=[]) 

136 # self.assertEqual(response.status_code, 403) 

137 

138 def test_create_minimal(self): 

139 response = self.__post__({"description": "Minimal IssueFilter"}) 

140 self.assertEqual(response.status_code, 201) 

141 self.assertEqual(response.data, 

142 {'id': response.data['id'], 'description': 'Minimal IssueFilter', 

143 'tags': [], 'machine_tags': [], 'machines': [], 'tests': [], 

144 'statuses': [], 'stdout_regex': '', 

145 'stderr_regex': '', 'dmesg_regex': '', 

146 'user_query': '', '__str__': 'Minimal IssueFilter'}) 

147 

148 def test_create_invalid(self): 

149 response = self.__post__({ 

150 "description": "Invalid tags, machines, tests, and statuses", 

151 'tags': [1], 'machines': [2], 'tests': [3], 'statuses': [4] 

152 }) 

153 self.assertEqual(response.status_code, 400, response.data) 

154 self.assertEqual(response.data, ['At least one tag does not exist', 

155 'At least one machine does not exist', 

156 'At least one test does not exist', 

157 'At least one status does not exist']) 

158 

159 def test_create_complete(self): 

160 # Create some objects before referencing them 

161 ts = TestSuite.objects.create(name="ts", description="", url="", public=True) 

162 for i in range(1, 6): 

163 RunConfigTag.objects.create(id=i, name="tag{}".format(i), description="", url="", public=True) 

164 Machine.objects.create(id=i, name="machine{}".format(i), public=True) 

165 Test.objects.create(id=i, name="test{}".format(i), testsuite=ts, public=True) 

166 TextStatus.objects.create(id=i, testsuite=ts, name="status{}".format(i)) 

167 MachineTag.objects.create(id=i, name="TAG{}".format(i), public=True) 

168 

169 # Make the request and check that we get the expected output 

170 with transaction.atomic(): 

171 response = self.__post__({ 

172 "description": "Minimal IssueFilter", 

173 'tags': [1, 2], 'machine_tags': [1, 5], 'machines': [2, 3], 'tests': [3, 4], 'statuses': [4, 5], 

174 'stdout_regex': 'stdout', 'stderr_regex': 'stderr', 'dmesg_regex': 'dmesg' 

175 }) 

176 

177 self.assertEqual(response.status_code, 201, response.data) 

178 self.assertEqual(dict(response.data), 

179 {'id': response.data['id'], 'description': 'Minimal IssueFilter', 

180 'tags': [ 

181 {'id': 1, 'description': '', 'url': '', 'public': True, '__str__': 'tag1'}, 

182 {'id': 2, 'description': '', 'url': '', 'public': True, '__str__': 'tag2'} 

183 ], 

184 'machine_tags': [ 

185 {'id': 1, 'name': 'TAG1', 'public': True}, 

186 {'id': 5, 'name': 'TAG5', 'public': True}, 

187 ], 

188 'machines': [ 

189 {'id': 2, 'vetted_on': None, 'public': True, '__str__': 'machine2'}, 

190 {'id': 3, 'vetted_on': None, 'public': True, '__str__': 'machine3'}, 

191 ], 

192 'tests': [ 

193 {'id': 3, 'testsuite': {'id': ts.id, '__str__': 'ts'}, 'public': True, 

194 'vetted_on': None, 'first_runconfig': None, 'name': 'test3', '__str__': 'ts: test3'}, 

195 {'id': 4, 'testsuite': {'id': ts.id, '__str__': 'ts'}, 'public': True, 

196 'vetted_on': None, 'first_runconfig': None, 'name': 'test4', '__str__': 'ts: test4'}, 

197 ], 

198 'statuses': [ 

199 {'id': 4, 'testsuite': {'id': ts.id, '__str__': 'ts'}, 'name': 'status4', 

200 '__str__': 'ts: status4'}, 

201 {'id': 5, 'testsuite': {'id': ts.id, '__str__': 'ts'}, 'name': 'status5', 

202 '__str__': 'ts: status5'}, 

203 ], 

204 'stdout_regex': 'stdout', 'stderr_regex': 'stderr', 

205 'dmesg_regex': 'dmesg', '__str__': 'Minimal IssueFilter', 

206 'user_query': ''}) 

207 

208 def test_edit_invalid(self): 

209 response = self.__post__({"description": "new filter", 

210 "edit_filter": "a", "edit_issue": "b"}) 

211 self.assertEqual(response.status_code, 400, response.data) 

212 self.assertEqual(response.data, ["The field 'edit_filter' needs to be an integer", 

213 "The field 'edit_issue' needs to be an integer"]) 

214 

215 @patch('CIResults.models.IssueFilter.replace') 

216 @patch('CIResults.models.Issue.replace_filter') 

217 def test_edit_all_issues(self, mock_replace_filter, mock_filter_replace): 

218 filter = IssueFilter.objects.create(description="old filter") 

219 

220 with transaction.atomic(): 

221 response = self.__post__({"description": "new filter", "edit_filter": filter.id}) 

222 self.assertEqual(response.status_code, 201, response.data) 

223 self.assertEqual(response.data, 

224 {'id': response.data['id'], 'description': 'new filter', 

225 'tags': [], 'machine_tags': [], 'machines': [], 'tests': [], 

226 'statuses': [], 'stdout_regex': '', 

227 'stderr_regex': '', 'dmesg_regex': '', 

228 'user_query': '', '__str__': 'new filter'}) 

229 

230 new_filter = IssueFilter.objects.get(pk=response.data['id']) 

231 mock_replace_filter.assert_not_called() 

232 mock_filter_replace.assert_called_once_with(new_filter, self.user) 

233 self.assertEqual(mock_filter_replace.call_args_list[0][0][0].id, response.data['id']) 

234 

235 @patch('CIResults.models.IssueFilter.replace') 

236 @patch('CIResults.models.Issue.replace_filter') 

237 def test_edit_one_issue(self, mock_replace_filter, mock_filter_replace): 

238 filter = IssueFilter.objects.create(description="desc") 

239 issue = Issue.objects.create(description="issue") 

240 

241 with transaction.atomic(): 

242 response = self.__post__({"description": "new filter", 

243 "edit_filter": "{}".format(filter.id), 

244 "edit_issue": issue.id}) 

245 

246 self.assertEqual(response.status_code, 201, response.data) 

247 self.assertEqual(response.data, 

248 {'id': response.data['id'], 'description': 'new filter', 

249 'tags': [], 'machine_tags': [], 'machines': [], 'tests': [], 

250 'statuses': [], 'stdout_regex': '', 

251 'stderr_regex': '', 'dmesg_regex': '', 

252 'user_query': '', '__str__': 'new filter'}) 

253 

254 new_filter = IssueFilter.objects.get(pk=response.data['id']) 

255 mock_filter_replace.assert_not_called() 

256 mock_replace_filter.assert_called_once_with(filter, new_filter, self.user) 

257 self.assertEqual(mock_replace_filter.call_args_list[0][0][0].id, filter.id) 

258 self.assertEqual(mock_replace_filter.call_args_list[0][0][1].id, response.data['id']) 

259 

260 

261class RunConfigTests(APITestCase): 

262 def setUp(self): 

263 self.view = RunConfigViewSet() 

264 self.arf = APIRequestFactory() 

265 

266 def test_retrieve__by_id(self): 

267 rc = RunConfig.objects.create(name='valid', temporary=True) 

268 response = self.client.get(reverse("api:runconfig-detail", args=["valid"])) 

269 data = response.json() 

270 self.assertEqual(data["id"], rc.id) 

271 self.assertEqual(data["name"], "valid") 

272 self.assertEqual(data["tags"], []) 

273 self.assertIsNone(data["url"]) 

274 self.assertIsNotNone(data["added_on"]) 

275 

276 def test_list(self): 

277 runcfg1 = RunConfig.objects.create(name='runcfg1', temporary=True, environment="env") 

278 runcfg2 = RunConfig.objects.create(name='runcfg2', temporary=False, url="http://url.com") 

279 RunConfig.objects.create(name='other_name', temporary=False, url="http://url.com") 

280 response = self.client.get(reverse("api:runconfig-list"), {"name__contains": "runcfg"}) 

281 data = response.json() 

282 self.assertEqual(data["count"], 2) 

283 for result in data["results"]: 

284 self.assertIsNotNone(result.pop("added_on")) 

285 for rc in (runcfg1, runcfg2): 

286 self.assertIn( 

287 { 

288 "id": rc.id, "name": rc.name, "tags": list(rc.tags.all()), "temporary": rc.temporary, 

289 "url": rc.url, "builds": list(rc.builds.all()), "environment": rc.environment, 

290 "__str__": str(rc), 

291 }, 

292 data["results"], 

293 ) 

294 

295 def test_known_failures(self): 

296 # TODO: Revisit the test whenever we start generating fixtures 

297 runcfg = RunConfig.objects.create(name='valid', temporary=True) 

298 response = self.client.get(reverse("api:runconfig-known-failures", args=[runcfg.name])) 

299 self.assertEqual(response.data, []) 

300 

301 def test_compare(self): 

302 runcfg1 = RunConfig.objects.create(name='runcfg1', temporary=True) 

303 runcfg2 = RunConfig.objects.create(name='runcfg2', temporary=True) 

304 with patch( 

305 "CIResults.models.RunConfig.compare", return_value=runcfg1.compare(runcfg2, no_compress=False) 

306 ) as compare_mocked: 

307 response = self.client.get(reverse("api:runconfig-compare", args=[runcfg1.name]), {'to': 'runcfg2'}) 

308 compare_mocked.assert_called_once_with(runcfg2, no_compress=False) 

309 self.assertEqual(response.data["runcfg_from"]["name"], "runcfg1") 

310 self.assertEqual(response.data["runcfg_to"]["name"], "runcfg2") 

311 self.assertEqual(response.data["status"], "SUCCESS") 

312 self.assertTrue(len(response.data["text"]) > 0) 

313 

314 def test_compare__no_compress(self): 

315 runcfg1 = RunConfig.objects.create(name='runcfg1', temporary=True) 

316 runcfg2 = RunConfig.objects.create(name='runcfg2', temporary=True) 

317 with patch( 

318 "CIResults.models.RunConfig.compare", return_value=runcfg1.compare(runcfg2, no_compress='1'), 

319 ) as compare_mocked: 

320 response = self.client.get( 

321 reverse("api:runconfig-compare", args=[runcfg1.name]), 

322 {'to': 'runcfg2', "no_compress": "1"}, 

323 ) 

324 compare_mocked.assert_called_once_with(runcfg2, no_compress=True) 

325 self.assertEqual(response.data["runcfg_from"]["name"], "runcfg1") 

326 self.assertEqual(response.data["runcfg_to"]["name"], "runcfg2") 

327 self.assertEqual(response.data["status"], "SUCCESS") 

328 self.assertTrue(len(response.data["text"]) > 0) 

329 

330 def test_compare__only_summary(self): 

331 runcfg1 = RunConfig.objects.create(name='runcfg1', temporary=True) 

332 RunConfig.objects.create(name='runcfg2', temporary=True) 

333 response = self.client.get( 

334 reverse("api:runconfig-compare", args=[runcfg1.name]), 

335 {'to': 'runcfg2', "summary": ""}, 

336 ) 

337 self.assertIn('CI Bug Log', response.content.decode()) 

338 

339 def test_create__no_permissions(self): 

340 response = self.client.post(reverse('api:runconfig-list'), 

341 data={"name": "runcfg", "tags": ["tag1"], "temporary": False}, 

342 content_type="application/json") 

343 self.assertEqual(response.status_code, 401) 

344 

345 def test_create(self): 

346 RunConfigTag.objects.create(id=1, name="tag1", public=True) 

347 create_user_and_log_in(self.client, permissions=['add_runconfig']) 

348 response = self.client.post(reverse('api:runconfig-list'), 

349 data={"name": "runcfg", "tags": ["tag1"], "builds": [], "temporary": False}, 

350 content_type="application/json") 

351 self.assertEqual(response.status_code, 201) 

352 self.assertEqual(response.data["name"], "runcfg") 

353 self.assertEqual(response.data["tags"], ["tag1"]) 

354 

355 def test_create__invalid_data(self): 

356 create_user_and_log_in(self.client, permissions=['add_runconfig']) 

357 response = self.client.post(reverse('api:runconfig-list'), data={"name": "runcfg"}, 

358 content_type="application/json") 

359 self.assertEqual(response.status_code, 400) 

360 

361 def test_create__invalid_data_missing_tag(self): 

362 create_user_and_log_in(self.client, permissions=['add_runconfig']) 

363 response = self.client.post(reverse('api:runconfig-list'), 

364 data={"name": "runcfg", "tags": ["tag1"], "temporary": False}, 

365 content_type="application/json") 

366 self.assertEqual(response.status_code, 400) 

367 

368 def test_import_testsuite_run(self): 

369 RunConfig.objects.create(name='runcfg1', temporary=False) 

370 component = TestSuite.objects.create(id=1, name='testsuite', public=True) 

371 Build.objects.create(name='testsuite-1', component=component) 

372 create_user_and_log_in(self.client, admin=True) 

373 url = reverse("api:runconfig-import-test-suite-run", args=["runcfg1"]) 

374 response = self.client.post(url, content_type="application/json", data={ 

375 "test_suite_name": "testsuite-1", 

376 "test_results": { 

377 "machine-1": { 

378 "001": [{ 

379 "test_name": "test1", 

380 "status": "pass", 

381 "stdout": "output", 

382 }], 

383 "002": [{ 

384 "test_name": "test2", 

385 "status": "pass", 

386 "stdout": "output", 

387 }], 

388 }, 

389 }, 

390 }, 

391 ) 

392 self.assertEqual(response.status_code, 200) 

393 self.assertIsNotNone(Machine.objects.get(name="machine-1")) 

394 self.assertIsNotNone(Test.objects.get(name="test1")) 

395 self.assertIsNotNone(TextStatus.objects.get(name="pass")) 

396 self.assertEqual(TestResult.objects.count(), 2) 

397 for test_name in ["test1", "test2"]: 

398 result = TestResult.objects.filter(test__name=test_name).first() 

399 self.assertEqual(result.status.name, "pass") 

400 self.assertEqual(result.stdout, "output") 

401 

402 def test_import_testsuite_run__no_permissions(self): 

403 url = reverse("api:runconfig-import-test-suite-run", args=["runcfg1"]) 

404 response = self.client.post(url, content_type="application/json", data={}) 

405 self.assertEqual(response.status_code, 401) 

406 

407 def test_import_testsuite_run__invalid_data(self): 

408 RunConfig.objects.create(name='runcfg1', temporary=False) 

409 component = TestSuite.objects.create(id=1, name='testsuite', public=True) 

410 Build.objects.create(name='testsuite-1', component=component) 

411 create_user_and_log_in(self.client, admin=True) 

412 url = reverse("api:runconfig-import-test-suite-run", args=["runcfg1"]) 

413 response = self.client.post(url, content_type="application/json", data={ 

414 "test_suite": "testsuite-1", 

415 "test_results": "invalid_data" 

416 }) 

417 self.assertEqual(response.status_code, 400) 

418 self.assertEqual(response.json()["test_results"], 

419 ['Expected a dictionary of items but got type "str".']) 

420 

421 def test_import_testsuite_run__invalid_data_no_testsuite(self): 

422 RunConfig.objects.create(name='runcfg1', temporary=False) 

423 create_user_and_log_in(self.client, admin=True) 

424 url = reverse("api:runconfig-import-test-suite-run", args=["runcfg1"]) 

425 response = self.client.post(url, content_type="application/json", data={ 

426 "test_suite": "testsuite-1", 

427 "test_results": { 

428 "machine-1": { 

429 "001": [{ 

430 "test_name": "test1", 

431 "status": "pass", 

432 "stdout": "output", 

433 }], 

434 } 

435 } 

436 }) 

437 self.assertEqual(response.status_code, 400) 

438 self.assertEqual(response.json(), {'test_suite_name': ['This field is required.']}) 

439 

440 @patch("CIResults.serializers.ImportTestSuiteRunSerializer.save", side_effect=Exception("Some exception")) 

441 def test_import_testsuite_run__import_error(self, mock_serializer_save): 

442 runcfg1 = RunConfig.objects.create(name='runcfg1', temporary=False) 

443 component = TestSuite.objects.create(id=1, name='testsuite', public=True) 

444 Build.objects.create(name='testsuite-1', component=component) 

445 create_user_and_log_in(self.client, admin=True) 

446 response = self.client.post( 

447 reverse("api:runconfig-import-test-suite-run", args=[runcfg1]), 

448 content_type="application/json", 

449 data={ 

450 "test_suite_name": "testsuite-1", 

451 "test_results": { 

452 "machine-1": { 

453 "001": [{ 

454 "test_name": "test1", 

455 "status": "pass", 

456 "stdout": "output", 

457 }], 

458 "002": [{ 

459 "test_name": "test2", 

460 "status": "pass", 

461 "stdout": "output", 

462 }], 

463 } 

464 } 

465 } 

466 ) 

467 self.assertEqual(response.content.decode(), '"Some exception"') 

468 self.assertEqual(response.status_code, 400) 

469 

470 

471class BuildViewSetTests(APITestCase): 

472 def setUp(self) -> None: 

473 self.component = Component.objects.create(name="component", public=False) 

474 self.view = BuildViewSet() 

475 self.arf = APIRequestFactory() 

476 

477 def test_retrieve(self): 

478 rc = Build.objects.create(name='valid', component=self.component) 

479 data = self.view.retrieve(self.arf.get("/", {}, format="json"), pk="valid").data 

480 self.assertEqual(data["id"], rc.id) 

481 self.assertEqual(data["name"], "valid") 

482 self.assertIsNotNone(data["added_on"]) 

483 

484 def test_create_build_without_permissions(self): 

485 response = self.client.post(reverse('api:build-list'), 

486 data={"name": "test-build", "component": "component", "version": "1"}, 

487 format="json") 

488 self.assertEqual(response.status_code, 401) 

489 

490 def test_create_build(self): 

491 create_user_and_log_in(self.client, permissions=['add_build']) 

492 response = self.client.post(reverse('api:build-list'), 

493 data={"name": "build1", "component": "component", "version": "1", "parents": []}, 

494 format="json") 

495 self.assertEqual(response.status_code, 201) 

496 self.assertEqual(response.data["name"], "build1") 

497 

498 def test_create_build__invalid_data_schema(self): 

499 create_user_and_log_in(self.client, permissions=['add_build']) 

500 response = self.client.post(reverse('api:build-list'), 

501 data={"name": "test-build", "component": "component"}, 

502 format="json") 

503 self.assertEqual(response.status_code, 400) 

504 self.assertEqual(json.loads(response.content)["version"], ["This field is required."]) 

505 

506 def test_create_build__invalid_data(self): 

507 create_user_and_log_in(self.client, permissions=['add_build']) 

508 response = self.client.post(reverse('api:build-list'), 

509 data={"name": "test-build", "component": "no-component", "version": "1"}, 

510 format="json") 

511 self.assertEqual(response.status_code, 400) 

512 

513 

514class BugViewSetTests(APITestCase): 

515 def setUp(self): 

516 self.arf = APIRequestFactory() 

517 

518 @classmethod 

519 def setUpTestData(cls): 

520 cls.bt1 = BugTracker.objects.create(name='Test Suite 1', short_name='ts1', public=True) 

521 cls.bt2 = BugTracker.objects.create(name='Test Suite 2', short_name='ts2', public=True) 

522 

523 cls.bugs = dict() 

524 for bt in (cls.bt1, cls.bt2): 

525 cls.bugs[bt] = [] 

526 for i in range(2): 

527 cls.bugs[bt].append(Bug.objects.create(tracker=bt, bug_id='b_'+str(i))) 

528 

529 def test_retrieving_by_tracker_id(self): 

530 tracker = self.bt2 

531 bug = self.bugs[self.bt2][1] 

532 response = self.client.get(reverse('api:bug-detail', 

533 kwargs={'tracker': tracker.id, 'bug_id': bug.bug_id}), format='json') 

534 self.assertEqual(response.status_code, 200) 

535 self.assertEqual(response.data['bug_id'], bug.bug_id) 

536 self.assertEqual(response.data['tracker']['short_name'], tracker.short_name) 

537 

538 def test_retrieving_by_tracker_name(self): 

539 tracker = self.bt1 

540 bug = self.bugs[self.bt2][0] 

541 response = self.client.get(reverse('api:bug-detail', 

542 kwargs={'tracker': tracker.name, 'bug_id': bug.bug_id}), format='json') 

543 self.assertEqual(response.status_code, 200) 

544 self.assertEqual(response.data['bug_id'], bug.bug_id) 

545 self.assertEqual(response.data['tracker']['short_name'], tracker.short_name) 

546 

547 def test_retrieving_by_tracker_short_name(self): 

548 tracker = self.bt2 

549 bug = self.bugs[self.bt2][0] 

550 response = self.client.get(reverse('api:bug-detail', 

551 kwargs={'tracker': tracker.short_name, 'bug_id': bug.bug_id}), format='json') 

552 self.assertEqual(response.status_code, 200) 

553 self.assertEqual(response.data['bug_id'], bug.bug_id) 

554 self.assertEqual(response.data['tracker']['short_name'], tracker.short_name) 

555 

556 def test_creating_bug_without_permission(self): 

557 response = self.client.post(reverse('api:bug-list', kwargs={'tracker': self.bt1.id}), 

558 data={'tracker': self.bt1.id, 'bug_id': 'new_bug'}, format='json') 

559 self.assertEqual(response.status_code, 401) 

560 

561 def test_creating_bug(self): 

562 create_user_and_log_in(self.client, permissions=['add_bug']) 

563 response = self.client.post(reverse('api:bug-list', kwargs={'tracker': self.bt1.id}), 

564 data={'tracker': self.bt1.id, 'bug_id': 'new_bug'}, format='json') 

565 self.assertEqual(response.status_code, 201) 

566 self.assertEqual(response.data['bug_id'], 'new_bug') 

567 self.assertEqual(response.data['tracker']['id'], self.bt1.id) 

568 

569 def test_creating_bug_invalid_tracker(self): 

570 create_user_and_log_in(self.client, permissions=['add_bug']) 

571 response = self.client.post(reverse('api:bug-list', kwargs={'tracker': 999}), 

572 data={'tracker': 999, 'bug_id': 'new_bug'}, format='json') 

573 self.assertEqual(response.status_code, 404) 

574 

575 def test_updating_bug_without_permission(self): 

576 bug = self.bugs[self.bt1][0] 

577 response = self.client.put(reverse('api:bug-detail', 

578 kwargs={'tracker': self.bt1.id, 'bug_id': bug.bug_id}), 

579 data={'tracker': self.bt1.id, 'bug_id': bug.bug_id}, format='json') 

580 self.assertEqual(response.status_code, 401) 

581 

582 def test_updating_bug(self): 

583 create_user_and_log_in(self.client, permissions=['change_bug']) 

584 bug = self.bugs[self.bt1][0] 

585 response = self.client.put(reverse('api:bug-detail', 

586 kwargs={'tracker': self.bt1.id, 'bug_id': bug.bug_id}), 

587 data={'status': 'updated'}, format='json') 

588 self.assertEqual(response.status_code, 200) 

589 self.assertEqual(response.data['status'], 'updated') 

590 self.assertEqual(response.data['tracker']['id'], self.bt1.id) 

591 

592 

593class ShortenerViewSetTests(TestCase): 

594 def setUp(self): 

595 self.arf = APIRequestFactory() 

596 

597 @classmethod 

598 def setUpTestData(cls): 

599 cls.short1 = Shortener.get_or_create("Some sort of long query!") 

600 

601 def test_create_invalid_request(self): 

602 with self.assertRaisesMessage(ValueError, "Only JSON POST requests are supported"): 

603 self.client.post(reverse('api:shortener-list'), data={}) 

604 

605 def test_create_empty(self): 

606 with self.assertRaisesMessage(ValueError, 

607 "Missing the field 'full' which should contain the full text to be shortened"): 

608 self.client.post(reverse('api:shortener-list'), data={}, content_type="application/json") 

609 

610 def test_create_single(self): 

611 full = 'Some sort of long query, but different!' 

612 

613 response = self.client.post(reverse('api:shortener-list'), data={'full': full}, content_type="application/json") 

614 self.assertEqual(response.status_code, 200) 

615 

616 data = response.json() 

617 self.assertNotEqual(data['id'], self.short1.id) 

618 self.assertEqual(data['full'], full) 

619 

620 def test_create_multiple(self): 

621 fulls = ["query1", "query2"] 

622 

623 response = self.client.post(reverse('api:shortener-list'), data={'full': fulls}, 

624 content_type="application/json") 

625 self.assertEqual(response.status_code, 200) 

626 

627 data = response.json() 

628 self.assertEqual(data[0]['full'], fulls[0]) 

629 self.assertEqual(data[1]['full'], fulls[1]) 

630 

631 def test_retrieving_existing(self): 

632 response = self.client.post(reverse('api:shortener-list'), data={'full': self.short1.full}, 

633 content_type="application/json") 

634 self.assertEqual(response.status_code, 200) 

635 

636 data = response.json() 

637 self.assertEqual(data['id'], self.short1.id) 

638 

639 

640class MachineViewSetTests(APITestCase): 

641 def setUp(self): 

642 self.arf = APIRequestFactory() 

643 

644 def test_retrieve(self): 

645 machine = Machine.objects.create(name="machine_1", public=True) 

646 response = self.client.get(reverse("api:machine-detail", args=[machine.id]), content_type="application/json") 

647 self.assertEqual(response.status_code, 200) 

648 self.assertEqual( 

649 response.json(), 

650 { 

651 'id': machine.id, 

652 'name': machine.name, 

653 'description': machine.description, 

654 'public': machine.public, 

655 'vetted_on': machine.vetted_on, 

656 'aliases': machine.aliases, 

657 'tags': list(machine.tags.all()), 

658 } 

659 ) 

660 

661 def test_list_machines(self): 

662 Machine.objects.create(name="machine_1", public=True) 

663 response = self.client.get(reverse('api:machine-list'), content_type="application/json") 

664 self.assertEqual(response.status_code, 200) 

665 self.assertEqual(json.loads(response.content)["results"], 

666 [{'id': 1, 

667 'name': 'machine_1', 

668 'description': None, 

669 'public': True, 

670 'vetted_on': None, 

671 'aliases': None, 

672 'tags': []}]) 

673 

674 def test_create_machine_without_permission(self): 

675 response = self.client.post(reverse('api:machine-list'), data={'name': 'machine_1', 'tags': ['tag1']}, 

676 content_type="application/json") 

677 self.assertEqual(response.status_code, 401) 

678 

679 def test_create_machine(self): 

680 self.user = create_user_and_log_in(self.client, permissions=['add_machine']) 

681 response = self.client.post(reverse('api:machine-list'), data={'name': 'machine_1', 'tags': ['tag1']}, 

682 content_type="application/json") 

683 self.assertEqual(response.status_code, 201) 

684 self.assertEqual(response.json(), { 

685 "id": 1, 

686 "name": "machine_1", 

687 "description": None, 

688 "public": False, 

689 "vetted_on": None, 

690 "aliases": None, 

691 "tags": ["tag1"], 

692 }) 

693 

694 def test_create_machine_invalid_data(self): 

695 self.user = create_user_and_log_in(self.client, permissions=['add_machine']) 

696 response = self.client.post(reverse('api:machine-list'), data={'name': True}, 

697 content_type="application/json") 

698 self.assertEqual(response.status_code, 400) 

699 self.assertEqual(json.loads(response.content), { 

700 "name": ["Not a valid string."], 

701 }) 

702 

703 def test_create_machine_import_error(self): 

704 self.user = create_user_and_log_in(self.client, permissions=['add_machine']) 

705 response = self.client.post(reverse('api:machine-list'), data={'name': 'machine_1', 'aliases': 'machine_2'}, 

706 content_type="application/json") 

707 self.assertEqual(response.status_code, 400) 

708 self.assertEqual(response.json(), {'aliases': ['Object with name=machine_2 does not exist.']}) 

709 

710 def test_vet_machine_without_permission(self): 

711 machine = baker.make(Machine) 

712 self.user = create_user_and_log_in(self.client, admin=False) 

713 response = self.client.post(reverse('api:machine-vet', kwargs={"pk": machine.pk})) 

714 self.assertEqual(response.status_code, 403) 

715 

716 @patch("django.utils.timezone.now") 

717 def test_vet_machine(self, now_mocked): 

718 date = datetime.datetime(2021, 1, 1, tzinfo=datetime.timezone.utc) 

719 now_mocked.return_value = date 

720 machine = baker.make(Machine) 

721 self.user = create_user_and_log_in(self.client, admin=True) 

722 response = self.client.post(reverse("api:machine-vet", kwargs={"pk": machine.pk})) 

723 machine.refresh_from_db() 

724 self.assertEqual(machine.vetted_on, date) 

725 self.assertEqual(response.status_code, 200) 

726 

727 def test_vet_already_vetted(self): 

728 date = datetime.datetime(2021, 1, 1, tzinfo=datetime.timezone.utc) 

729 machine = baker.make(Machine, vetted_on=date) 

730 self.user = create_user_and_log_in(self.client, admin=True) 

731 response = self.client.post(reverse('api:machine-vet', kwargs={"pk": machine.pk})) 

732 machine.refresh_from_db() 

733 self.assertEqual(response.status_code, 200) 

734 self.assertEqual(machine.vetted_on, date) 

735 

736 def test_suppress_machine_without_permission(self): 

737 machine = baker.make(Machine) 

738 self.user = create_user_and_log_in(self.client, admin=False) 

739 response = self.client.post(reverse('api:machine-suppress', kwargs={"pk": machine.pk})) 

740 self.assertEqual(response.status_code, 403) 

741 

742 def test_suppress_machine(self): 

743 machine = baker.make(Machine, vetted_on="2021-01-01") 

744 self.user = create_user_and_log_in(self.client, admin=True) 

745 response = self.client.post(reverse('api:machine-suppress', kwargs={"pk": machine.pk})) 

746 self.assertEqual(response.status_code, 200) 

747 

748 

749class IssueViewSetTests(TestCase): 

750 def setUp(self): 

751 self.arf = APIRequestFactory() 

752 

753 def test_archive_issue_without_permission(self): 

754 response = self.client.get(reverse('api:issue-archive', kwargs={"pk": 1})) 

755 self.assertEqual(response.status_code, 401) 

756 self.assertEqual(response.json(), {"message": "User AnonymousUser doesn't have sufficient permissions"}) 

757 

758 def test_archive_issue(self): 

759 Issue.objects.create() 

760 self.user = create_user_and_log_in(self.client, admin=True) 

761 response = self.client.get(reverse('api:issue-archive', kwargs={"pk": 1})) 

762 self.assertEqual(response.status_code, 200) 

763 

764 def test_archive_archived_issue(self): 

765 self.user = create_user_and_log_in(self.client, admin=True) 

766 Issue.objects.create().archive(self.user) 

767 response = self.client.get(reverse('api:issue-archive', kwargs={"pk": 1})) 

768 self.assertEqual(response.status_code, 400) 

769 self.assertEqual(response.json(), {"message": "The issue is already archived"}) 

770 

771 def test_restore_issue_without_permission(self): 

772 response = self.client.get(reverse('api:issue-restore', kwargs={"pk": 1})) 

773 self.assertEqual(response.status_code, 401) 

774 self.assertEqual(response.json(), {"message": "User AnonymousUser doesn't have sufficient permissions"}) 

775 

776 def test_restore_issue(self): 

777 self.user = create_user_and_log_in(self.client, admin=True) 

778 Issue.objects.create().archive(self.user) 

779 response = self.client.get(reverse('api:issue-restore', kwargs={"pk": 1})) 

780 self.assertEqual(response.status_code, 200) 

781 

782 def test_restore_not_archived_issue(self): 

783 Issue.objects.create() 

784 self.user = create_user_and_log_in(self.client, admin=True) 

785 response = self.client.get(reverse('api:issue-restore', kwargs={"pk": 1})) 

786 self.assertEqual(response.status_code, 400) 

787 self.assertEqual(response.json(), {"message": "The issue is not currently archived"}) 

788 

789 def test_update_to_expected(self): 

790 Issue.objects.create(expected=False) 

791 self.user = create_user_and_log_in(self.client, admin=True) 

792 response = self.client.patch(reverse('api:issue-detail', kwargs={"pk": 1}), data={"id": 1, "expected": True}, 

793 content_type="application/json") 

794 self.assertEqual(response.status_code, 200) 

795 self.assertEqual(response.json()["expected"], True) 

796 self.assertEqual(Issue.objects.get(pk=1).expected, True) 

797 

798 def test_update_to_expected_wrong_value(self): 

799 Issue.objects.create(expected=False) 

800 self.user = create_user_and_log_in(self.client, admin=True) 

801 response = self.client.patch(reverse('api:issue-detail', kwargs={"pk": 1}), data={"id": 1, "expected": "Str"}, 

802 content_type="application/json") 

803 self.assertEqual(response.status_code, 400) 

804 self.assertEqual(response.json()["expected"], ["Must be a valid boolean."]) 

805 

806 def test_try_to_update_read_only_field(self): 

807 Issue.objects.create(runconfigs_covered_count=100) 

808 self.user = create_user_and_log_in(self.client, admin=True) 

809 response = self.client.patch(reverse('api:issue-detail', kwargs={"pk": 1}), 

810 data={"id": 1, "runconfigs_covered_count": 99}, content_type="application/json") 

811 self.assertEqual(response.status_code, 200) 

812 self.assertEqual(response.json()["runconfigs_covered_count"], 100) 

813 

814 def test_add_issue_filters_without_permission(self): 

815 issue = baker.make(Issue) 

816 f1 = baker.make(IssueFilter) 

817 f2 = baker.make(IssueFilter) 

818 self.user = create_user_and_log_in(self.client) 

819 response = self.client.post( 

820 reverse( 

821 'api:issue-add-filters', 

822 kwargs={"pk": issue.id}), 

823 data={"filters": [f1.id, f2.id]}, 

824 content_type="application/json", 

825 ) 

826 self.assertEqual(response.status_code, 403) 

827 self.assertEqual(response.json(), {'detail': 'You do not have permission to perform this action.'}) 

828 

829 def test_add_issue_filters(self): 

830 self.user = create_user_and_log_in(self.client, permissions=["change_issue", "add_issue"]) 

831 issue = baker.make(Issue) 

832 f_default = baker.make(IssueFilter) 

833 issue.filters.add(f_default) 

834 f1 = baker.make(IssueFilter) 

835 f2 = baker.make(IssueFilter) 

836 issue_filter_list = sorted(list(issue.filters.all()), key=lambda f: f.id) 

837 self.assertEqual(issue_filter_list, [f_default]) 

838 response = self.client.post( 

839 reverse( 

840 'api:issue-add-filters', 

841 kwargs={"pk": issue.id}), 

842 data={"filters": [f1.id, f2.id]}, 

843 content_type="application/json", 

844 ) 

845 issue_filter_list = sorted(list(issue.filters.all()), key=lambda f: f.id) 

846 expected_list = sorted([f_default, f1, f2], key=lambda f: f.id) 

847 self.assertEqual(issue_filter_list, expected_list) 

848 self.assertEqual(response.status_code, 200) 

849 

850 def test_add_issue_filters_duplicates(self): 

851 self.user = create_user_and_log_in(self.client, permissions=["change_issue", "add_issue"]) 

852 issue = baker.make(Issue) 

853 f1 = baker.make(IssueFilter) 

854 f2 = baker.make(IssueFilter) 

855 issue.filters.add(f1) 

856 issue.filters.add(f2) 

857 issue_filter_list = sorted(list(issue.filters.all()), key=lambda f: f.id) 

858 expected_list = sorted([f1, f2], key=lambda f: f.id) 

859 self.assertEqual(issue_filter_list, expected_list) 

860 response = self.client.post( 

861 reverse( 

862 'api:issue-add-filters', 

863 kwargs={"pk": issue.id}), 

864 data={"filters": [f1.id, f2.id]}, 

865 content_type="application/json", 

866 ) 

867 issue_filter_list = sorted(list(issue.filters.all()), key=lambda f: f.id) 

868 self.assertEqual(issue_filter_list, expected_list) 

869 self.assertEqual(response.status_code, 200) 

870 

871 

872class UnknownFailureViewSetTests(TestCase): 

873 def test_retrieve_by_id(self): 

874 unknown_failure = baker.make(UnknownFailure) 

875 response = self.client.get( 

876 reverse("api:unknownfailure-detail", kwargs={"pk": unknown_failure.id}), content_type="application/json" 

877 ) 

878 self.assertEqual(response.status_code, 200) 

879 data = response.json() 

880 self.assertEqual(data["id"], unknown_failure.id) 

881 self.assertEqual(data["test"], unknown_failure.result.test.name) 

882 self.assertEqual(data["status"], unknown_failure.result.status.name) 

883 self.assertRaises(KeyError, lambda: data["dmesg"]) 

884 self.assertRaises(KeyError, lambda: data["stdout"]) 

885 self.assertRaises(KeyError, lambda: data["stderr"]) 

886 self.assertEqual(data["testsuite"], unknown_failure.result.ts_run.testsuite.name) 

887 self.assertEqual(data["runconfig"], { 

888 "name": unknown_failure.result.ts_run.runconfig.name, 

889 "tags": list(unknown_failure.result.ts_run.runconfig.tags.values_list('name', flat=True)) 

890 }) 

891 self.assertEqual(data["machine"], { 

892 "name": unknown_failure.result.ts_run.machine.name, 

893 "tags": list(unknown_failure.result.ts_run.machine.tags.values_list('name', flat=True)) 

894 }) 

895 

896 def test_retrieve_by_id__extra_fields(self): 

897 unknown_failure = baker.make(UnknownFailure) 

898 response = self.client.get( 

899 reverse("api:unknownfailure-detail", kwargs={"pk": unknown_failure.id}), 

900 content_type="application/json", 

901 data={"extra_fields": "stdout"}, 

902 ) 

903 self.assertEqual(response.status_code, 200) 

904 data = response.json() 

905 self.assertRaises(KeyError, lambda: data["dmesg"]) 

906 self.assertRaises(KeyError, lambda: data["stderr"]) 

907 self.assertEqual(data["stdout"], unknown_failure.result.stdout) 

908 

909 def test_list(self): 

910 unknown_failures = baker.make(UnknownFailure, _quantity=3) 

911 response = self.client.get( 

912 reverse("api:unknownfailure-list"), 

913 content_type="application/json", 

914 ) 

915 data = response.json() 

916 self.assertEqual(response.status_code, 200) 

917 self.assertEqual(data["count"], 3) 

918 for unknown_failure in unknown_failures: 

919 self.assertIn( 

920 { 

921 "id": unknown_failure.id, 

922 "test": unknown_failure.result.test.name, 

923 "status": unknown_failure.result.status.name, 

924 "testsuite": unknown_failure.result.ts_run.testsuite.name, 

925 "runconfig": { 

926 "name": unknown_failure.result.ts_run.runconfig.name, 

927 "tags": list(unknown_failure.result.ts_run.runconfig.tags.values_list('name', flat=True)) 

928 }, 

929 "machine": { 

930 "name": unknown_failure.result.ts_run.machine.name, 

931 "tags": list(unknown_failure.result.ts_run.machine.tags.values_list('name', flat=True)) 

932 }, 

933 }, 

934 data["results"], 

935 ) 

936 

937 

938class TestSetTests(TestCase): 

939 def setUp(self): 

940 self.arf = APIRequestFactory() 

941 ts = TestSuite.objects.create(name="ts", description="", url="", public=True) 

942 Test.objects.create(id=1, name="test1", testsuite=ts, public=True) 

943 

944 def test_vet_test_without_permission(self): 

945 self.user = create_user_and_log_in(self.client, admin=False) 

946 response = self.client.post(reverse('api:test-vet', kwargs={"pk": 1})) 

947 self.assertEqual(response.status_code, 403) 

948 

949 @patch("django.utils.timezone.now") 

950 def test_vet_test(self, now_mocked): 

951 date = datetime.datetime(2021, 1, 1, tzinfo=datetime.timezone.utc) 

952 now_mocked.return_value = date 

953 test = baker.make(Test, vetted_on=None) 

954 self.user = create_user_and_log_in(self.client, admin=True) 

955 response = self.client.post(reverse('api:test-vet', kwargs={"pk": test.pk})) 

956 test.refresh_from_db() 

957 self.assertEqual(response.status_code, 200) 

958 self.assertEqual(test.vetted_on, date) 

959 

960 def test_vet_already_vetted(self): 

961 date = datetime.datetime(2021, 1, 1, tzinfo=datetime.timezone.utc) 

962 test = baker.make(Test, vetted_on=date) 

963 self.user = create_user_and_log_in(self.client, admin=True) 

964 response = self.client.post(reverse('api:test-vet', kwargs={"pk": test.pk})) 

965 test.refresh_from_db() 

966 self.assertEqual(response.status_code, 200) 

967 self.assertEqual(test.vetted_on, date) 

968 

969 def test_suppress_test_without_permission(self): 

970 self.user = create_user_and_log_in(self.client, admin=False) 

971 response = self.client.post(reverse('api:test-suppress', kwargs={"pk": 1})) 

972 self.assertEqual(response.status_code, 403) 

973 

974 def test_suppress_test(self): 

975 test = baker.make(Test, vetted_on=datetime.datetime(2021, 1, 1)) 

976 self.user = create_user_and_log_in(self.client, admin=True) 

977 response = self.client.post(reverse('api:test-suppress', kwargs={"pk": test.pk})) 

978 test.refresh_from_db() 

979 self.assertEqual(response.status_code, 200) 

980 self.assertIsNone(test.vetted_on) 

981 

982 

983class TextStatusViewSetTests(TestCase): 

984 def setUp(self): 

985 self.arf = APIRequestFactory() 

986 self.user = create_user_and_log_in(self.client, admin=True) 

987 

988 @patch("django.utils.timezone.now") 

989 def test_vet(self, now_mocked): 

990 date = datetime.datetime(2021, 1, 1, tzinfo=datetime.timezone.utc) 

991 now_mocked.return_value = date 

992 ts = baker.make(TextStatus, vetted_on=None) 

993 response = self.client.get(reverse('api:textstatus-vet', kwargs={"pk": ts.pk})) 

994 ts.refresh_from_db() 

995 self.assertEqual(response.status_code, 200) 

996 self.assertEqual(ts.vetted_on, date) 

997 

998 def test_suppress(self): 

999 ts = baker.make(TextStatus, vetted_on="2021-01-01") 

1000 response = self.client.get(reverse('api:textstatus-suppress', kwargs={"pk": ts.pk})) 

1001 ts.refresh_from_db() 

1002 self.assertEqual(response.status_code, 200) 

1003 self.assertIsNone(ts.vetted_on) 

1004 

1005 

1006class metrics_passrate_trend_viewTests(TestCase): 

1007 def setUp(self): 

1008 self.arf = APIRequestFactory() 

1009 

1010 def test_basic(self): 

1011 # TODO: Add a fixture that would return useful data here 

1012 response = self.client.get(reverse('api-metrics-passrate-per-runconfig'), 

1013 {'query': ""}, format='json') 

1014 self.assertEqual(response.status_code, 200) 

1015 

1016 

1017class metrics_passrate_viewTests(TestCase): 

1018 def setUp(self): 

1019 self.arf = APIRequestFactory() 

1020 

1021 def test_basic(self): 

1022 # TODO: Add a fixture that would return useful data here 

1023 response = self.client.get(reverse('api-metrics-passrate-per-test'), 

1024 {'query': ""}, format='json') 

1025 self.assertEqual(response.status_code, 200)