test_auth_api.py 24 KB

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