test_auth_api.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749
  1. from django.contrib.auth import get_user_model
  2. from django.core import mail
  3. from django.test import TestCase
  4. from misago.users.models import Ban
  5. from misago.users.tokens import make_password_change_token
  6. UserModel = get_user_model()
  7. class GatewayTests(TestCase):
  8. def test_api_invalid_credentials(self):
  9. """login api returns 400 on invalid POST"""
  10. response = self.client.post(
  11. '/api/auth/', data={
  12. 'username': 'nope',
  13. 'password': 'nope',
  14. }
  15. )
  16. self.assertEqual(response.status_code, 400)
  17. self.assertEqual(response.json(), {
  18. 'non_field_errors': ["Login or password is incorrect."],
  19. })
  20. response = self.client.get('/api/auth/')
  21. self.assertEqual(response.status_code, 200)
  22. response_json = response.json()
  23. self.assertIsNone(response_json['id'])
  24. def test_login(self):
  25. """api signs user in"""
  26. user = UserModel.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
  27. response = self.client.post(
  28. '/api/auth/',
  29. data={
  30. 'username': 'Bob',
  31. 'password': 'Pass.123',
  32. },
  33. )
  34. self.assertEqual(response.status_code, 200)
  35. response = self.client.get('/api/auth/')
  36. self.assertEqual(response.status_code, 200)
  37. response_json = response.json()
  38. self.assertEqual(response_json['id'], user.id)
  39. self.assertEqual(response_json['username'], user.username)
  40. def test_login_whitespaces_password(self):
  41. """api signs user in with password left untouched"""
  42. user = UserModel.objects.create_user('Bob', 'bob@test.com', ' Pass.123 ')
  43. response = self.client.post(
  44. '/api/auth/',
  45. data={
  46. 'username': 'Bob',
  47. 'password': 'Pass.123',
  48. },
  49. )
  50. self.assertEqual(response.status_code, 400)
  51. self.assertEqual(response.json(), {
  52. 'non_field_errors': ["Login or password is incorrect."],
  53. })
  54. response = self.client.post(
  55. '/api/auth/',
  56. data={
  57. 'username': 'Bob',
  58. 'password': ' Pass.123 ',
  59. },
  60. )
  61. self.assertEqual(response.status_code, 200)
  62. response = self.client.get('/api/auth/')
  63. self.assertEqual(response.status_code, 200)
  64. response_json = response.json()
  65. self.assertEqual(response_json['id'], user.id)
  66. self.assertEqual(response_json['username'], user.username)
  67. def test_submit_no_data(self):
  68. """login api errors for no body"""
  69. response = self.client.post('/api/auth/')
  70. self.assertEqual(response.status_code, 400)
  71. self.assertEqual(response.json(), {
  72. 'username': ['This field is required.'],
  73. 'password': ['This field is required.'],
  74. })
  75. def test_submit_empty(self):
  76. """login api errors for empty fields"""
  77. response = self.client.post('/api/auth/', data={
  78. 'username': '',
  79. 'password': '',
  80. })
  81. self.assertEqual(response.status_code, 400)
  82. self.assertEqual(response.json(), {
  83. 'username': ['This field may not be blank.'],
  84. 'password': ['This field may not be blank.'],
  85. })
  86. def test_submit_invalid(self):
  87. """login api errors for invalid data"""
  88. response = self.client.post(
  89. '/api/auth/',
  90. 'false',
  91. content_type="application/json",
  92. )
  93. self.assertEqual(response.status_code, 400)
  94. self.assertEqual(response.json(), {
  95. 'non_field_errors': ["Invalid data. Expected a dictionary, but got bool."],
  96. })
  97. def test_login_not_usable_password(self):
  98. """login api fails to sign user with not-usable password in"""
  99. UserModel.objects.create_user('Bob', 'bob@test.com')
  100. response = self.client.post(
  101. '/api/auth/',
  102. data={
  103. 'username': 'Bob',
  104. 'password': 'Pass.123',
  105. },
  106. )
  107. self.assertEqual(response.status_code, 400)
  108. self.assertEqual(response.json(), {
  109. 'non_field_errors': ["Login or password is incorrect."],
  110. })
  111. def test_login_banned(self):
  112. """login api fails to sign banned user in"""
  113. UserModel.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
  114. ban = Ban.objects.create(
  115. check_type=Ban.USERNAME,
  116. banned_value='bob',
  117. user_message='You are tragically banned.',
  118. )
  119. response = self.client.post(
  120. '/api/auth/',
  121. data={
  122. 'username': 'Bob',
  123. 'password': 'Pass.123',
  124. },
  125. )
  126. self.assertEqual(response.status_code, 403)
  127. self.assertEqual(response.json(), {
  128. 'detail': {
  129. 'html': '<p>%s</p>' % ban.user_message,
  130. 'plain': ban.user_message,
  131. },
  132. 'expires_on': None,
  133. })
  134. response = self.client.get('/api/auth/')
  135. self.assertEqual(response.status_code, 200)
  136. response_json = response.json()
  137. self.assertIsNone(response_json['id'])
  138. def test_login_banned_staff(self):
  139. """login api signs banned staff member in"""
  140. user = UserModel.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
  141. user.is_staff = True
  142. user.save()
  143. Ban.objects.create(
  144. check_type=Ban.USERNAME,
  145. banned_value='bob',
  146. user_message='You are tragically banned.',
  147. )
  148. response = self.client.post(
  149. '/api/auth/',
  150. data={
  151. 'username': 'Bob',
  152. 'password': 'Pass.123',
  153. },
  154. )
  155. self.assertEqual(response.status_code, 200)
  156. response = self.client.get('/api/auth/')
  157. self.assertEqual(response.status_code, 200)
  158. response_json = response.json()
  159. self.assertEqual(response_json['id'], user.id)
  160. self.assertEqual(response_json['username'], user.username)
  161. def test_login_ban_registration_only(self):
  162. """login api ignores registration-only bans"""
  163. user = UserModel.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
  164. Ban.objects.create(
  165. check_type=Ban.USERNAME,
  166. banned_value='bob',
  167. registration_only=True,
  168. )
  169. response = self.client.post(
  170. '/api/auth/',
  171. data={
  172. 'username': 'Bob',
  173. 'password': 'Pass.123',
  174. },
  175. )
  176. self.assertEqual(response.status_code, 200)
  177. response = self.client.get('/api/auth/')
  178. self.assertEqual(response.status_code, 200)
  179. user_json = response.json()
  180. self.assertEqual(user_json['id'], user.id)
  181. self.assertEqual(user_json['username'], user.username)
  182. def test_login_inactive_admin(self):
  183. """login api fails to sign admin-activated user in"""
  184. UserModel.objects.create_user('Bob', 'bob@test.com', 'Pass.123', requires_activation=2)
  185. response = self.client.post(
  186. '/api/auth/',
  187. data={
  188. 'username': 'Bob',
  189. 'password': 'Pass.123',
  190. },
  191. )
  192. self.assertEqual(response.status_code, 400)
  193. self.assertEqual(response.json(), {
  194. 'non_field_errors': [
  195. "Your account has to be activated by Administrator before you will be able to sign in.",
  196. ],
  197. })
  198. response = self.client.get('/api/auth/')
  199. self.assertEqual(response.status_code, 200)
  200. response_json = response.json()
  201. self.assertIsNone(response_json['id'])
  202. def test_login_inactive_user(self):
  203. """login api fails to sign user-activated user in"""
  204. UserModel.objects.create_user('Bob', 'bob@test.com', 'Pass.123', requires_activation=1)
  205. response = self.client.post(
  206. '/api/auth/',
  207. data={
  208. 'username': 'Bob',
  209. 'password': 'Pass.123',
  210. },
  211. )
  212. self.assertEqual(response.status_code, 400)
  213. self.assertEqual(response.json(), {
  214. 'non_field_errors': [
  215. "You have to activate your account before you will be able to sign in.",
  216. ],
  217. })
  218. response = self.client.get('/api/auth/')
  219. self.assertEqual(response.status_code, 200)
  220. response_json = response.json()
  221. self.assertIsNone(response_json['id'])
  222. def test_login_disabled_user(self):
  223. """its impossible to sign in to disabled account"""
  224. user = UserModel.objects.create_user('Bob', 'bob@test.com', 'Pass.123', is_active=False)
  225. user.is_staff = True
  226. user.save()
  227. response = self.client.post(
  228. '/api/auth/',
  229. data={
  230. 'username': 'Bob',
  231. 'password': 'Pass.123',
  232. },
  233. )
  234. self.assertEqual(response.status_code, 400)
  235. self.assertEqual(response.json(), {
  236. 'non_field_errors': ["Login or password is incorrect."],
  237. })
  238. response = self.client.get('/api/auth/')
  239. self.assertEqual(response.status_code, 200)
  240. response_json = response.json()
  241. self.assertIsNone(response_json['id'])
  242. class UserRequirementsTests(TestCase):
  243. def test_edge_returns_response(self):
  244. """api edge has no showstoppers"""
  245. response = self.client.get('/api/auth/requirements/')
  246. self.assertEqual(response.status_code, 200)
  247. self.assertEqual(response.json(), {
  248. 'username': {'max_length': 14, 'min_length': 3},
  249. 'password': [
  250. {
  251. 'name': 'UserAttributeSimilarityValidator',
  252. 'user_attributes': ['username', 'email'],
  253. },
  254. {
  255. 'name': 'MinimumLengthValidator',
  256. 'min_length': 7,
  257. },
  258. {'name': 'CommonPasswordValidator'},
  259. {'name': 'NumericPasswordValidator'},
  260. ],
  261. })
  262. class SendActivationAPITests(TestCase):
  263. def setUp(self):
  264. self.user = UserModel.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
  265. self.user.requires_activation = 1
  266. self.user.save()
  267. self.link = '/api/auth/send-activation/'
  268. def test_submit_valid(self):
  269. """request activation link api sends reset link mail"""
  270. response = self.client.post(
  271. self.link,
  272. data={
  273. 'email': self.user.email,
  274. },
  275. )
  276. self.assertEqual(response.status_code, 200)
  277. self.assertIn('Activate Bob', mail.outbox[0].subject)
  278. def test_submit_banned(self):
  279. """request activation link api errors for banned users"""
  280. ban = Ban.objects.create(
  281. check_type=Ban.USERNAME,
  282. banned_value=self.user.username,
  283. user_message='Nope!',
  284. )
  285. response = self.client.post(
  286. self.link,
  287. data={
  288. 'email': self.user.email,
  289. },
  290. )
  291. self.assertEqual(response.status_code, 403)
  292. self.assertEqual(response.json(), {
  293. 'detail': {
  294. 'html': '<p>%s</p>' % ban.user_message,
  295. 'plain': ban.user_message,
  296. },
  297. 'expires_on': None,
  298. })
  299. self.assertTrue(not mail.outbox)
  300. def test_submit_disabled(self):
  301. """request activation link api fails disabled users"""
  302. self.user.is_active = False
  303. self.user.save()
  304. response = self.client.post(
  305. self.link,
  306. data={
  307. 'email': self.user.email,
  308. },
  309. )
  310. self.assertEqual(response.status_code, 400)
  311. # fixme: don't leak out the info that email is invalid in auth forms
  312. # instead, message that if email was valid you'll get an email
  313. self.assertEqual(response.json(), {
  314. 'non_field_errors': ["No user with this e-mail exists."],
  315. })
  316. self.assertTrue(not mail.outbox)
  317. def test_submit_empty(self):
  318. """request activation link api errors for no body"""
  319. response = self.client.post(self.link)
  320. self.assertEqual(response.status_code, 400)
  321. self.assertEqual(response.json(), {
  322. 'email': ["This field is required."],
  323. })
  324. self.assertTrue(not mail.outbox)
  325. def test_submit_invalid_data(self):
  326. """login api errors for invalid data"""
  327. response = self.client.post(
  328. self.link,
  329. 'false',
  330. content_type="application/json",
  331. )
  332. self.assertEqual(response.status_code, 400)
  333. self.assertEqual(response.json(), {
  334. 'non_field_errors': ["Invalid data. Expected a dictionary, but got bool."],
  335. })
  336. def test_submit_invalid_email(self):
  337. """request activation link api errors for invalid email"""
  338. response = self.client.post(
  339. self.link,
  340. data={
  341. 'email': 'fake@mail.com',
  342. },
  343. )
  344. self.assertEqual(response.status_code, 400)
  345. # fixme: don't leak out the info that email is invalid in auth forms
  346. # instead, message that if email was valid you'll get an email
  347. self.assertEqual(response.json(), {
  348. 'non_field_errors': ["No user with this e-mail exists."],
  349. })
  350. self.assertTrue(not mail.outbox)
  351. def test_submit_active_user(self):
  352. """request activation link api errors for active user"""
  353. self.user.requires_activation = 0
  354. self.user.save()
  355. response = self.client.post(
  356. self.link,
  357. data={
  358. 'email': self.user.email,
  359. },
  360. )
  361. self.assertEqual(response.status_code, 400)
  362. self.assertEqual(response.json(), {
  363. 'non_field_errors': ["Bob, your account is already active."],
  364. })
  365. def test_submit_inactive_user(self):
  366. """request activation link api errors for admin-activated users"""
  367. self.user.requires_activation = 2
  368. self.user.save()
  369. response = self.client.post(
  370. self.link,
  371. data={
  372. 'email': self.user.email,
  373. },
  374. )
  375. self.assertEqual(response.status_code, 400)
  376. self.assertEqual(response.json(), {
  377. 'non_field_errors': ["Bob, only administrator may activate your account."],
  378. })
  379. self.assertTrue(not mail.outbox)
  380. # but succeed for user-activated
  381. self.user.requires_activation = 1
  382. self.user.save()
  383. response = self.client.post(
  384. self.link, data={
  385. 'email': self.user.email,
  386. }
  387. )
  388. self.assertEqual(response.json(), {
  389. 'username': self.user.username,
  390. 'email': self.user.email,
  391. })
  392. self.assertTrue(mail.outbox)
  393. class SendPasswordFormAPITests(TestCase):
  394. def setUp(self):
  395. self.user = UserModel.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
  396. self.link = '/api/auth/send-password-form/'
  397. def test_submit_valid(self):
  398. """request change password form link api sends reset link mail"""
  399. response = self.client.post(
  400. self.link,
  401. data={
  402. 'email': self.user.email,
  403. },
  404. )
  405. self.assertEqual(response.status_code, 200)
  406. self.assertIn('Change Bob password', mail.outbox[0].subject)
  407. def test_submit_banned(self):
  408. """request change password form link api errors for banned users"""
  409. ban = Ban.objects.create(
  410. check_type=Ban.USERNAME,
  411. banned_value=self.user.username,
  412. user_message='Nope!',
  413. )
  414. response = self.client.post(
  415. self.link,
  416. data={
  417. 'email': self.user.email,
  418. },
  419. )
  420. self.assertEqual(response.status_code, 403)
  421. self.assertEqual(response.json(), {
  422. 'detail': {
  423. 'html': '<p>%s</p>' % ban.user_message,
  424. 'plain': ban.user_message,
  425. },
  426. 'expires_on': None,
  427. })
  428. self.assertTrue(not mail.outbox)
  429. def test_submit_disabled(self):
  430. """request change password form api fails disabled users"""
  431. self.user.is_active = False
  432. self.user.save()
  433. response = self.client.post(
  434. self.link,
  435. data={
  436. 'email': self.user.email,
  437. },
  438. )
  439. self.assertEqual(response.status_code, 400)
  440. self.assertEqual(response.json(), {
  441. 'non_field_errors': ["No user with this e-mail exists."],
  442. })
  443. self.assertTrue(not mail.outbox)
  444. def test_submit_empty(self):
  445. """request change password form link api errors for no body"""
  446. response = self.client.post(self.link)
  447. self.assertEqual(response.status_code, 400)
  448. self.assertEqual(response.json(), {
  449. 'email': ["This field is required."],
  450. })
  451. self.assertTrue(not mail.outbox)
  452. def test_submit_invalid(self):
  453. """request change password form link api errors for invalid email"""
  454. response = self.client.post(
  455. self.link,
  456. data={
  457. 'email': 'fake@mail.com',
  458. },
  459. )
  460. self.assertEqual(response.status_code, 400)
  461. self.assertEqual(response.json(), {
  462. 'non_field_errors': ["No user with this e-mail exists."],
  463. })
  464. self.assertTrue(not mail.outbox)
  465. def test_submit_invalid_data(self):
  466. """login api errors for invalid data"""
  467. response = self.client.post(
  468. self.link,
  469. 'false',
  470. content_type="application/json",
  471. )
  472. self.assertEqual(response.status_code, 400)
  473. self.assertEqual(response.json(), {
  474. 'non_field_errors': ["Invalid data. Expected a dictionary, but got bool."],
  475. })
  476. def test_submit_inactive_user(self):
  477. """request change password form link api errors for inactive users"""
  478. self.user.requires_activation = 1
  479. self.user.save()
  480. response = self.client.post(
  481. self.link,
  482. data={
  483. 'email': self.user.email,
  484. },
  485. )
  486. self.assertEqual(response.status_code, 400)
  487. self.assertEqual(response.json(), {
  488. 'non_field_errors': [
  489. "You have to activate your account before you will "
  490. "be able to request new password.",
  491. ],
  492. })
  493. self.assertTrue(not mail.outbox)
  494. self.user.requires_activation = 2
  495. self.user.save()
  496. response = self.client.post(
  497. self.link,
  498. data={
  499. 'email': self.user.email,
  500. },
  501. )
  502. self.assertEqual(response.status_code, 400)
  503. self.assertEqual(response.json(), {
  504. 'non_field_errors': [
  505. "Administrator has to activate your account before you "
  506. "will be able to request new password.",
  507. ],
  508. })
  509. self.assertTrue(not mail.outbox)
  510. class ChangePasswordAPITests(TestCase):
  511. def setUp(self):
  512. self.user = UserModel.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
  513. self.link = '/api/auth/change-password/%s/'
  514. def test_submit_valid(self):
  515. """submit change password form api changes password"""
  516. response = self.client.post(
  517. self.link % self.user.pk,
  518. data={
  519. 'password': 'n3wp4ss!',
  520. 'token': make_password_change_token(self.user),
  521. },
  522. )
  523. self.assertEqual(response.status_code, 200)
  524. user = UserModel.objects.get(id=self.user.pk)
  525. self.assertTrue(user.check_password('n3wp4ss!'))
  526. def test_submit_with_whitespaces(self):
  527. """submit change password form api changes password with whitespaces"""
  528. response = self.client.post(
  529. self.link % self.user.pk,
  530. data={
  531. 'password': ' n3wp4ss! ',
  532. 'token': make_password_change_token(self.user),
  533. },
  534. )
  535. self.assertEqual(response.status_code, 200)
  536. user = UserModel.objects.get(id=self.user.pk)
  537. self.assertTrue(user.check_password(' n3wp4ss! '))
  538. def test_submit_invalid_data(self):
  539. """login api errors for invalid data"""
  540. response = self.client.post(
  541. self.link % self.user.pk,
  542. 'false',
  543. content_type="application/json",
  544. )
  545. self.assertEqual(response.status_code, 400)
  546. self.assertEqual(response.json(), {
  547. 'non_field_errors': ["Invalid data. Expected a dictionary, but got bool."],
  548. })
  549. def test_invalid_token(self):
  550. """api errors on invalid user id link"""
  551. response = self.client.post(
  552. self.link % self.user.pk,
  553. data={
  554. 'password': 'n3wp4ss!',
  555. 'token': 'invalid!',
  556. },
  557. )
  558. self.assertEqual(response.status_code, 400)
  559. self.assertEqual(response.json(), {
  560. 'token': ["Form link is invalid or expired. Please try again."],
  561. })
  562. def test_banned_user_link(self):
  563. """request errors because user is banned"""
  564. ban = Ban.objects.create(
  565. check_type=Ban.USERNAME,
  566. banned_value=self.user.username,
  567. user_message='Nope!',
  568. )
  569. response = self.client.post(
  570. self.link % self.user.pk,
  571. data={
  572. 'password': 'n3wp4ss!',
  573. 'token': make_password_change_token(self.user),
  574. },
  575. )
  576. self.assertEqual(response.status_code, 403)
  577. self.assertEqual(response.json(), {
  578. 'detail': {
  579. 'html': '<p>%s</p>' % ban.user_message,
  580. 'plain': ban.user_message,
  581. },
  582. 'expires_on': None,
  583. })
  584. def test_inactive_user(self):
  585. """change password api errors for inactive users"""
  586. self.user.requires_activation = 1
  587. self.user.save()
  588. response = self.client.post(
  589. self.link % self.user.pk,
  590. data={
  591. 'password': 'n3wp4ss!',
  592. 'token': make_password_change_token(self.user),
  593. },
  594. )
  595. self.assertEqual(response.status_code, 400)
  596. self.assertEqual(response.json(), {
  597. 'non_field_errors': [
  598. "You have to activate your account before you will "
  599. "be able to change your password.",
  600. ],
  601. })
  602. self.user.requires_activation = 2
  603. self.user.save()
  604. response = self.client.post(
  605. self.link % self.user.pk,
  606. data={
  607. 'password': 'n3wp4ss!',
  608. 'token': make_password_change_token(self.user),
  609. },
  610. )
  611. self.assertEqual(response.status_code, 400)
  612. self.assertEqual(response.json(), {
  613. 'non_field_errors': [
  614. "Administrator has to activate your account before you "
  615. "will be able to change your password.",
  616. ],
  617. })
  618. def test_disabled_user(self):
  619. """change password api errors for disabled users"""
  620. self.user.is_active = False
  621. self.user.save()
  622. response = self.client.post(self.link % self.user.pk)
  623. self.assertEqual(response.status_code, 404)
  624. self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
  625. def test_submit_empty(self):
  626. """change password api errors for empty body"""
  627. response = self.client.post(self.link % self.user.pk)
  628. self.assertEqual(response.status_code, 400)
  629. self.assertEqual(response.json(), {
  630. 'password': ["This field is required."],
  631. 'token': ["This field is required."],
  632. })