test_user_management.py 56 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425
  1. """
  2. This module tests deSEC's user management.
  3. The tests are separated into two categories, where
  4. (a) the client has an associated user account and
  5. (b) does not have an associated user account.
  6. This involves testing five separate endpoints:
  7. (1) Registration endpoint,
  8. (2) Reset password endpoint,
  9. (3) Change email address endpoint,
  10. (4) delete user endpoint, and
  11. (5) verify endpoint.
  12. Furthermore, domain renewals and unused domain/account scavenging are tested.
  13. """
  14. from datetime import timedelta
  15. import random
  16. import re
  17. import time
  18. from unittest import mock
  19. from urllib.parse import urlparse
  20. from django.contrib.auth.hashers import is_password_usable
  21. from django.core import mail
  22. from django.core.management import call_command
  23. from django.urls import resolve
  24. from django.utils import timezone
  25. from rest_framework import status
  26. from rest_framework.reverse import reverse
  27. from rest_framework.test import APIClient
  28. from api import settings
  29. from desecapi.models import Domain, User, Captcha
  30. from desecapi.tests.base import (
  31. DesecTestCase,
  32. DomainOwnerTestCase,
  33. PublicSuffixMockMixin,
  34. )
  35. class UserManagementClient(APIClient):
  36. def register(self, email, password, captcha=None, **kwargs):
  37. try:
  38. captcha_id, captcha_solution = captcha
  39. except TypeError:
  40. pass
  41. else:
  42. kwargs["captcha"] = {"id": captcha_id, "solution": captcha_solution}
  43. return self.post(
  44. reverse("v1:register"), {"email": email, "password": password, **kwargs}
  45. )
  46. def login_user(self, email, password):
  47. return self.post(
  48. reverse("v1:login"),
  49. {
  50. "email": email,
  51. "password": password,
  52. },
  53. )
  54. def logout(self, token):
  55. return self.post(reverse("v1:logout"), HTTP_AUTHORIZATION=f"Token {token}")
  56. def reset_password(self, email, captcha_id, captcha_solution):
  57. return self.post(
  58. reverse("v1:account-reset-password"),
  59. {
  60. "email": email,
  61. "captcha": {"id": captcha_id, "solution": captcha_solution},
  62. },
  63. )
  64. def change_email(self, email, password, **payload):
  65. payload["email"] = email
  66. payload["password"] = password
  67. return self.post(reverse("v1:account-change-email"), payload)
  68. def change_email_token_auth(self, token, **payload):
  69. return self.post(
  70. reverse("v1:account-change-email"),
  71. payload,
  72. HTTP_AUTHORIZATION="Token {}".format(token),
  73. )
  74. def delete_account(self, email, password):
  75. return self.post(
  76. reverse("v1:account-delete"),
  77. {
  78. "email": email,
  79. "password": password,
  80. },
  81. )
  82. def view_account(self, token):
  83. return self.get(
  84. reverse("v1:account"), HTTP_AUTHORIZATION="Token {}".format(token)
  85. )
  86. def verify(self, url, data=None, **kwargs):
  87. return self.post(url, data, **kwargs)
  88. def obtain_captcha(self, **kwargs):
  89. return self.post(reverse("v1:captcha"))
  90. class UserManagementTestCase(DesecTestCase, PublicSuffixMockMixin):
  91. client_class = UserManagementClient
  92. password = None
  93. token = None
  94. def get_captcha(self):
  95. response = self.client.obtain_captcha()
  96. self.assertStatus(response, status.HTTP_201_CREATED)
  97. id = response.data["id"]
  98. solution = Captcha.objects.get(id=id).content
  99. return id, solution
  100. def register_user(self, email=None, password=None, late_captcha=False, **kwargs):
  101. email = email if email is not None else self.random_username()
  102. captcha = None if late_captcha else self.get_captcha()
  103. return (
  104. email.strip(),
  105. password,
  106. self.client.register(email, password, captcha, **kwargs),
  107. )
  108. def login_user(self, email, password):
  109. return self.client.login_user(email, password)
  110. def logout(self, token):
  111. return self.client.logout(token)
  112. def reset_password(self, email):
  113. captcha_id, captcha_solution = self.get_captcha()
  114. return self.client.reset_password(email, captcha_id, captcha_solution)
  115. def change_email(self, new_email):
  116. return self.client.change_email(self.email, self.password, new_email=new_email)
  117. def delete_account(self, email, password):
  118. return self.client.delete_account(email, password)
  119. def assertContains(
  120. self, response, text, count=None, status_code=200, msg_prefix="", html=False
  121. ):
  122. msg_prefix += "\nResponse: %s" % response.data
  123. super().assertContains(response, text, count, status_code, msg_prefix, html)
  124. def assertPassword(self, email, password):
  125. if password is None:
  126. self.assertFalse(is_password_usable(User.objects.get(email=email).password))
  127. return
  128. password = password.strip()
  129. self.assertTrue(
  130. User.objects.get(email=email).check_password(password),
  131. 'Expected user password to be "%s" (potentially trimmed), but check failed.'
  132. % password,
  133. )
  134. def assertUserExists(self, email):
  135. try:
  136. User.objects.get(email=email)
  137. except User.DoesNotExist:
  138. self.fail("Expected user %s to exist, but did not." % email)
  139. def assertUserDoesNotExist(self, email):
  140. # noinspection PyTypeChecker
  141. with self.assertRaises(User.DoesNotExist):
  142. User.objects.get(email=email)
  143. def assertNoEmailSent(self):
  144. self.assertFalse(
  145. mail.outbox,
  146. "Expected no email to be sent, but %i were sent. First subject line is '%s'."
  147. % (len(mail.outbox), mail.outbox[0].subject if mail.outbox else "<n/a>"),
  148. )
  149. def assertEmailSent(
  150. self,
  151. subject_contains="",
  152. body_contains=None,
  153. recipient=None,
  154. reset=True,
  155. pattern=None,
  156. ):
  157. total = 1
  158. self.assertEqual(
  159. len(mail.outbox),
  160. total,
  161. "Expected %i message in the outbox, but found %i."
  162. % (total, len(mail.outbox)),
  163. )
  164. email = mail.outbox[-1]
  165. self.assertTrue(
  166. subject_contains in email.subject,
  167. "Expected '%s' in the email subject, but found '%s'"
  168. % (subject_contains, email.subject),
  169. )
  170. if type(body_contains) != list:
  171. body_contains = [] if body_contains is None else [body_contains]
  172. for elem in body_contains:
  173. self.assertTrue(
  174. elem in email.body,
  175. f"Expected '{elem}' in the email body, but found '{email.body}'",
  176. )
  177. if recipient is not None:
  178. if isinstance(recipient, list):
  179. self.assertListEqual(recipient, email.recipients())
  180. else:
  181. self.assertIn(recipient, email.recipients())
  182. body = email.body
  183. self.assertIn("user_id = ", body)
  184. if reset:
  185. mail.outbox = []
  186. return body if not pattern else re.search(pattern, body).group(1)
  187. def assertConfirmationLinkRedirect(self, confirmation_link):
  188. response = self.client.get(confirmation_link)
  189. self.assertResponse(response, status.HTTP_406_NOT_ACCEPTABLE)
  190. response = self.client.get(confirmation_link, HTTP_ACCEPT="text/html")
  191. self.assertResponse(response, status.HTTP_302_FOUND)
  192. self.assertNoEmailSent()
  193. def assertRegistrationEmail(self, recipient, reset=True):
  194. return self.assertEmailSent(
  195. subject_contains="deSEC",
  196. body_contains="Thank you for registering with deSEC!",
  197. recipient=[recipient],
  198. reset=reset,
  199. pattern=r"following link[^:]*:\s+([^\s]*)",
  200. )
  201. def assertResetPasswordEmail(self, recipient, reset=True):
  202. return self.assertEmailSent(
  203. subject_contains="Password reset",
  204. body_contains="We received a request to reset the password for your deSEC account.",
  205. recipient=[recipient],
  206. reset=reset,
  207. pattern=r"following link[^:]*:\s+([^\s]*)",
  208. )
  209. def assertChangeEmailVerificationEmail(self, recipient, reset=True):
  210. return self.assertEmailSent(
  211. subject_contains="Confirmation required: Email address change",
  212. body_contains="You requested to change the email address associated",
  213. recipient=[recipient],
  214. reset=reset,
  215. pattern=r"following link[^:]*:\s+([^\s]*)",
  216. )
  217. def assertChangeEmailNotificationEmail(self, recipient, reset=True):
  218. return self.assertEmailSent(
  219. subject_contains="Account email address changed",
  220. body_contains="We're writing to let you know that the email address associated with",
  221. recipient=[recipient],
  222. reset=reset,
  223. )
  224. def assertDeleteAccountEmail(self, recipient, reset=True):
  225. return self.assertEmailSent(
  226. subject_contains="Confirmation required: Delete account",
  227. body_contains="confirm once more",
  228. recipient=[recipient],
  229. reset=reset,
  230. pattern=r"following link[^:]*:\s+([^\s]*)",
  231. )
  232. def assertRegistrationSuccessResponse(self, response):
  233. return self.assertContains(
  234. response=response,
  235. text="Welcome! Please check your mailbox.",
  236. status_code=status.HTTP_202_ACCEPTED,
  237. )
  238. def assertLoginSuccessResponse(self, response):
  239. return self.assertContains(
  240. response=response, text="token", status_code=status.HTTP_200_OK
  241. )
  242. def assertLogoutSuccessResponse(self, response):
  243. return self.assertStatus(response, status.HTTP_204_NO_CONTENT)
  244. def assertRegistrationFailurePasswordRequiredResponse(self, response):
  245. self.assertContains(
  246. response=response,
  247. text="This field may not be blank",
  248. status_code=status.HTTP_400_BAD_REQUEST,
  249. )
  250. self.assertEqual(response.data["password"][0].code, "blank")
  251. def assertRegistrationFailurePasswordMinLengthResponse(self, response):
  252. self.assertContains(
  253. response=response,
  254. text="This password is too short. It must contain at least 8 characters.",
  255. status_code=status.HTTP_400_BAD_REQUEST,
  256. )
  257. self.assertEqual(response.data["password"][0].code, "password_too_short")
  258. def assertRegistrationFailureDomainUnavailableResponse(self, response, domain):
  259. self.assertContains(
  260. response=response,
  261. text="This domain name conflicts with an existing zone, or is disallowed by policy.",
  262. status_code=status.HTTP_400_BAD_REQUEST,
  263. msg_prefix=str(response.data),
  264. )
  265. def assertRegistrationFailureDomainInvalidResponse(self, response, domain):
  266. self.assertContains(
  267. response=response,
  268. text="Domain names must be labels separated by dots. Labels",
  269. status_code=status.HTTP_400_BAD_REQUEST,
  270. msg_prefix=str(response.data),
  271. )
  272. def assertRegistrationFailureCaptchaInvalidResponse(self, response):
  273. self.assertContains(
  274. response=response,
  275. text="CAPTCHA could not be validated. Please obtain a new one and try again.",
  276. status_code=status.HTTP_400_BAD_REQUEST,
  277. msg_prefix=str(response.data),
  278. )
  279. def assertRegistrationVerificationSuccessResponse(self, response):
  280. return self.assertContains(
  281. response=response, text="Success!", status_code=status.HTTP_200_OK
  282. )
  283. def assertRegistrationVerificationFailureResponse(self, response):
  284. self.assertEqual(response.data["captcha"][0], "This field is required.")
  285. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  286. def assertRegistrationWithDomainVerificationSuccessResponse(
  287. self, response, domain=None, email=None
  288. ):
  289. self.assertNoEmailSent() # do not send email in any case
  290. text = "Success! Please check the docs for the next steps"
  291. if domain:
  292. has_local_suffix = self.has_local_suffix(domain)
  293. if has_local_suffix:
  294. text = "Success! Here is the password"
  295. self.assertEqual("keys" in response.data["domain"], not has_local_suffix)
  296. self.assertEqual(response.data["domain"]["name"], domain)
  297. self.assertContains(
  298. response=response, text=text, status_code=status.HTTP_200_OK
  299. )
  300. def assertResetPasswordSuccessResponse(self, response):
  301. return self.assertContains(
  302. response=response,
  303. text="Please check your mailbox for further password reset instructions.",
  304. status_code=status.HTTP_202_ACCEPTED,
  305. )
  306. def assertResetPasswordVerificationSuccessResponse(self, response):
  307. return self.assertContains(
  308. response=response,
  309. text="Success! Your password has been changed.",
  310. status_code=status.HTTP_200_OK,
  311. )
  312. def assertResetPasswordInactiveUserVerificationFailedResponse(self, response):
  313. return self.assertContains(
  314. response=response,
  315. text="User inactive.",
  316. status_code=status.HTTP_403_FORBIDDEN,
  317. )
  318. def assertChangeEmailSuccessResponse(self, response):
  319. return self.assertContains(
  320. response=response,
  321. text="Please check your mailbox to confirm email address change.",
  322. status_code=status.HTTP_202_ACCEPTED,
  323. )
  324. def assert401InvalidPasswordResponse(self, response):
  325. return self.assertContains(
  326. response=response,
  327. text="Invalid password.",
  328. status_code=status.HTTP_401_UNAUTHORIZED,
  329. )
  330. def assertChangeEmailFailureAddressTakenResponse(self, response):
  331. return self.assertContains(
  332. response=response,
  333. text="You already have another account with this email address.",
  334. status_code=status.HTTP_400_BAD_REQUEST,
  335. )
  336. def assertChangeEmailFailureSameAddressResponse(self, response):
  337. return self.assertContains(
  338. response=response,
  339. text="Email address unchanged.",
  340. status_code=status.HTTP_400_BAD_REQUEST,
  341. )
  342. def assertChangeEmailVerificationSuccessResponse(self, response, new_email):
  343. return self.assertContains(
  344. response=response,
  345. text=f"Success! Your email address has been changed to {new_email}.",
  346. status_code=status.HTTP_200_OK,
  347. )
  348. def assertChangeEmailVerificationFailureChangePasswordResponse(self, response):
  349. return self.assertContains(
  350. response=response,
  351. text="This field is not allowed for action ",
  352. status_code=status.HTTP_400_BAD_REQUEST,
  353. )
  354. def assertDeleteAccountSuccessResponse(self, response):
  355. return self.assertContains(
  356. response=response,
  357. text="Please check your mailbox for further account deletion instructions.",
  358. status_code=status.HTTP_202_ACCEPTED,
  359. )
  360. def assertDeleteAccountFailureStillHasDomainsResponse(self, response):
  361. return self.assertContains(
  362. response=response,
  363. text="To delete your user account, first delete all of your domains.",
  364. status_code=status.HTTP_409_CONFLICT,
  365. )
  366. def assertDeleteAccountVerificationSuccessResponse(self, response):
  367. return self.assertContains(
  368. response=response,
  369. text="All your data has been deleted. Bye bye, see you soon! <3",
  370. status_code=status.HTTP_200_OK,
  371. )
  372. def assertVerificationFailureInvalidCodeResponse(self, response):
  373. return self.assertContains(
  374. response=response,
  375. text="This action cannot be carried out because another operation has been performed",
  376. status_code=status.HTTP_409_CONFLICT,
  377. )
  378. def assertVerificationFailureExpiredCodeResponse(self, response):
  379. return self.assertContains(
  380. response=response,
  381. text="This code is invalid, possibly because it expired (validity: ",
  382. status_code=status.HTTP_400_BAD_REQUEST,
  383. )
  384. def assertVerificationFailureUnknownUserResponse(self, response):
  385. return self.assertContains(
  386. response=response,
  387. text="This user does not exist.",
  388. status_code=status.HTTP_400_BAD_REQUEST,
  389. )
  390. def _test_registration(
  391. self, email=None, password=None, late_captcha=True, **kwargs
  392. ):
  393. email, password, response = self.register_user(
  394. email, password, late_captcha, **kwargs
  395. )
  396. self.assertRegistrationSuccessResponse(response)
  397. self.assertUserExists(email)
  398. self.assertFalse(User.objects.get(email=email).is_active)
  399. self.assertIsNone(User.objects.get(email=email).is_active)
  400. self.assertEqual(User.objects.get(email=email).needs_captcha, late_captcha)
  401. self.assertEqual(
  402. User.objects.get(email=email).outreach_preference,
  403. kwargs.get("outreach_preference", True),
  404. )
  405. self.assertPassword(email, password)
  406. confirmation_link = self.assertRegistrationEmail(email)
  407. self.assertConfirmationLinkRedirect(confirmation_link)
  408. response = self.client.verify(confirmation_link)
  409. if late_captcha:
  410. self.assertRegistrationVerificationFailureResponse(response)
  411. captcha_id, captcha_solution = self.get_captcha()
  412. data = {"captcha": {"id": captcha_id, "solution": captcha_solution}}
  413. response = self.client.verify(confirmation_link, data=data)
  414. self.assertRegistrationVerificationSuccessResponse(response)
  415. self.assertTrue(User.objects.get(email=email).is_active)
  416. self.assertFalse(User.objects.get(email=email).needs_captcha)
  417. self.assertPassword(email, password)
  418. return email, password
  419. def _test_registration_with_domain(
  420. self,
  421. email=None,
  422. password=None,
  423. domain=None,
  424. expect_failure_response=None,
  425. tampered_domain=None,
  426. ):
  427. domain = domain or self.random_domain_name()
  428. email, password, response = self.register_user(email, password, domain=domain)
  429. if expect_failure_response:
  430. expect_failure_response(response, domain)
  431. self.assertUserDoesNotExist(email)
  432. return
  433. self.assertRegistrationSuccessResponse(response)
  434. self.assertUserExists(email)
  435. self.assertFalse(User.objects.get(email=email).is_active)
  436. self.assertIsNone(User.objects.get(email=email).is_active)
  437. self.assertPassword(email, password)
  438. confirmation_link = self.assertRegistrationEmail(email)
  439. if tampered_domain is not None:
  440. self.assertNotEqual(domain, tampered_domain)
  441. path = urlparse(confirmation_link).path
  442. serializer_class = resolve(path).func.cls.serializer_class
  443. code = resolve(path).kwargs.get("code")
  444. serializer = serializer_class(data={}, context={"code": code})
  445. serializer.is_valid()
  446. self.assertEqual(
  447. serializer.validated_data["domain"], domain
  448. ) # preparation check: domain as expected
  449. serializer = serializer_class(
  450. data={"domain": tampered_domain}, context={"code": code}
  451. )
  452. serializer.is_valid()
  453. self.assertEqual(
  454. serializer.validated_data["domain"], domain
  455. ) # extra domain from data not injected
  456. _, data = serializer_class._unpack_code(code, ttl=None)
  457. data["domain"] = tampered_domain
  458. tampered_code = serializer_class._pack_code(data)
  459. confirmation_link = confirmation_link.replace(code, tampered_code)
  460. response = self.client.verify(confirmation_link)
  461. self.assertVerificationFailureInvalidCodeResponse(response)
  462. return
  463. if self.has_local_suffix(domain):
  464. cm = self.requests_desec_domain_creation_auto_delegation(domain)
  465. else:
  466. cm = self.requests_desec_domain_creation(domain)
  467. with self.assertPdnsRequests(cm):
  468. response = self.client.verify(confirmation_link)
  469. self.assertRegistrationWithDomainVerificationSuccessResponse(
  470. response, domain, email
  471. )
  472. self.assertTrue(User.objects.get(email=email).is_active)
  473. self.assertPassword(email, password)
  474. self.assertTrue(Domain.objects.filter(name=domain, owner__email=email).exists())
  475. return email, password, domain
  476. def _test_login(self):
  477. response = self.login_user(self.email, self.password)
  478. self.assertLoginSuccessResponse(response)
  479. self.assertEqual(response.data["max_age"], "7 00:00:00")
  480. self.assertEqual(response.data["max_unused_period"], "01:00:00")
  481. return response.data["token"]
  482. def _test_logout(self):
  483. response = self.logout(self.token)
  484. self.assertLogoutSuccessResponse(response)
  485. return response
  486. def _test_reset_password(self, email, new_password=None, **kwargs):
  487. new_password = new_password or self.random_password()
  488. try:
  489. confirmation_link = kwargs.pop("confirmation_link")
  490. except KeyError:
  491. self.assertResetPasswordSuccessResponse(self.reset_password(email))
  492. confirmation_link = self.assertResetPasswordEmail(email)
  493. self.assertConfirmationLinkRedirect(confirmation_link)
  494. self.assertResetPasswordVerificationSuccessResponse(
  495. self.client.verify(
  496. confirmation_link, data={"new_password": new_password}, **kwargs
  497. )
  498. )
  499. self.assertPassword(email, new_password)
  500. return new_password
  501. def _test_change_email(self):
  502. old_email = self.email
  503. new_email = " {} ".format(self.random_username()) # test trimming
  504. self.assertChangeEmailSuccessResponse(self.change_email(new_email))
  505. new_email = new_email.strip()
  506. confirmation_link = self.assertChangeEmailVerificationEmail(new_email)
  507. self.assertConfirmationLinkRedirect(confirmation_link)
  508. self.assertChangeEmailVerificationSuccessResponse(
  509. self.client.verify(confirmation_link), new_email
  510. )
  511. self.assertChangeEmailNotificationEmail(old_email)
  512. self.assertUserExists(new_email)
  513. self.assertUserDoesNotExist(old_email)
  514. self.email = new_email
  515. return self.email
  516. def _test_delete_account(self, email, password):
  517. self.assertDeleteAccountSuccessResponse(self.delete_account(email, password))
  518. confirmation_link = self.assertDeleteAccountEmail(email)
  519. self.assertConfirmationLinkRedirect(confirmation_link)
  520. self.assertDeleteAccountVerificationSuccessResponse(
  521. self.client.verify(confirmation_link)
  522. )
  523. self.assertUserDoesNotExist(email)
  524. class UserLifeCycleTestCase(UserManagementTestCase):
  525. def test_life_cycle(self):
  526. self.email, self.password = self._test_registration(
  527. self.random_username(), self.random_password()
  528. )
  529. self.password = self._test_reset_password(self.email)
  530. mail.outbox = []
  531. self.token = self._test_login()
  532. email = self._test_change_email()
  533. self._test_logout()
  534. self._test_delete_account(email, self.password)
  535. class NoUserAccountTestCase(UserLifeCycleTestCase):
  536. def test_home(self):
  537. self.assertResponse(self.client.get(reverse("v1:root")), status.HTTP_200_OK)
  538. def test_authenticated_action_redirect_with_invalid_code(self):
  539. # This tests that the code is not processed when Accept: text/html is not set (redirect without further ado)
  540. confirmation_link = self.reverse("v1:confirm-activate-account", code="foobar")
  541. self.assertConfirmationLinkRedirect(confirmation_link)
  542. def test_registration(self):
  543. for outreach_preference in [None, True, False]:
  544. kwargs = (
  545. dict(outreach_preference=outreach_preference)
  546. if outreach_preference is not None
  547. else {}
  548. )
  549. self._test_registration(password=self.random_password(), **kwargs)
  550. def test_registration_trim_email(self):
  551. user_email = " {} ".format(self.random_username())
  552. email, _ = self._test_registration(user_email)
  553. self.assertEqual(email, user_email.strip())
  554. def test_registration_with_domain(self):
  555. PublicSuffixMockMixin.setUpMockPatch(self)
  556. with self.get_psl_context_manager("."):
  557. _, _, domain = self._test_registration_with_domain()
  558. self._test_registration_with_domain(
  559. domain=domain,
  560. expect_failure_response=self.assertRegistrationFailureDomainUnavailableResponse,
  561. )
  562. self._test_registration_with_domain(
  563. domain="töö--",
  564. expect_failure_response=self.assertRegistrationFailureDomainInvalidResponse,
  565. )
  566. with self.get_psl_context_manager("co.uk"):
  567. self._test_registration_with_domain(
  568. domain="co.uk",
  569. expect_failure_response=self.assertRegistrationFailureDomainUnavailableResponse,
  570. )
  571. local_public_suffix = random.sample(self.AUTO_DELEGATION_DOMAINS, 1)[0]
  572. with self.get_psl_context_manager(local_public_suffix):
  573. self._test_registration_with_domain(
  574. domain=self.random_domain_name(suffix=local_public_suffix)
  575. )
  576. def test_registration_without_domain_and_password(self):
  577. email, password = self._test_registration(self.random_username(), None)
  578. confirmation_link = self.assertResetPasswordEmail(email)
  579. self._test_reset_password(email, confirmation_link=confirmation_link)
  580. def test_registration_with_tampered_domain(self):
  581. PublicSuffixMockMixin.setUpMockPatch(self)
  582. with self.get_psl_context_manager("."):
  583. self._test_registration_with_domain(tampered_domain="evil.com")
  584. def test_registration_known_account(self):
  585. email, _ = self._test_registration(
  586. self.random_username(), self.random_password()
  587. )
  588. self.assertRegistrationSuccessResponse(
  589. self.register_user(email, self.random_password())[2]
  590. )
  591. self.assertNoEmailSent()
  592. def test_registration_password_required(self):
  593. email = self.random_username()
  594. self.assertRegistrationFailurePasswordRequiredResponse(
  595. response=self.register_user(email=email, password="")[2]
  596. )
  597. self.assertNoEmailSent()
  598. self.assertUserDoesNotExist(email)
  599. def test_registration_password_min_length(self):
  600. email = self.random_username()
  601. self.assertRegistrationFailurePasswordMinLengthResponse(
  602. response=self.register_user(email=email, password="asdf123")[2]
  603. )
  604. self.assertNoEmailSent()
  605. self.assertUserDoesNotExist(email)
  606. def test_no_login_with_unusable_password(self):
  607. email, password = self._test_registration(password=None)
  608. response = self.client.login_user(email, password)
  609. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  610. self.assertEqual(response.data["password"][0], "This field may not be null.")
  611. def test_registration_spam_protection(self):
  612. email = self.random_username()
  613. self.assertRegistrationSuccessResponse(
  614. response=self.register_user(email=email)[2]
  615. )
  616. self.assertRegistrationEmail(email)
  617. for _ in range(5):
  618. self.assertRegistrationSuccessResponse(
  619. response=self.register_user(email=email)[2]
  620. )
  621. self.assertNoEmailSent()
  622. def test_registration_wrong_captcha(self):
  623. email = self.random_username()
  624. password = self.random_password()
  625. captcha_id, _ = self.get_captcha()
  626. self.assertRegistrationFailureCaptchaInvalidResponse(
  627. self.client.register(
  628. email,
  629. password,
  630. (captcha_id, "this is most definitely not a CAPTCHA solution"),
  631. )
  632. )
  633. def test_registration_late_captcha(self):
  634. self._test_registration(password=self.random_password(), late_captcha=True)
  635. class OtherUserAccountTestCase(UserManagementTestCase):
  636. def setUp(self):
  637. super().setUp()
  638. self.other_email, self.other_password = self._test_registration(
  639. password=self.random_password()
  640. )
  641. def test_reset_password_unknown_user(self):
  642. self.assertResetPasswordSuccessResponse(
  643. response=self.reset_password(self.random_username())
  644. )
  645. self.assertNoEmailSent()
  646. class HasUserAccountTestCase(UserManagementTestCase):
  647. def __init__(self, methodName: str = ...) -> None:
  648. super().__init__(methodName)
  649. self.email = None
  650. self.password = None
  651. def setUp(self):
  652. super().setUp()
  653. self.email, self.password = self._test_registration(
  654. password=self.random_password()
  655. )
  656. self.token = self._test_login()
  657. def _start_reset_password(self):
  658. self.assertResetPasswordSuccessResponse(
  659. response=self.reset_password(self.email)
  660. )
  661. return self.assertResetPasswordEmail(self.email)
  662. def _start_change_email(self):
  663. new_email = self.random_username()
  664. self.assertChangeEmailSuccessResponse(response=self.change_email(new_email))
  665. return self.assertChangeEmailVerificationEmail(new_email), new_email
  666. def _start_delete_account(self):
  667. self.assertDeleteAccountSuccessResponse(
  668. self.delete_account(self.email, self.password)
  669. )
  670. return self.assertDeleteAccountEmail(self.email)
  671. def _finish_reset_password(self, confirmation_link, expect_success=True):
  672. new_password = self.random_password()
  673. response = self.client.verify(
  674. confirmation_link, data={"new_password": new_password}
  675. )
  676. if expect_success:
  677. self.assertResetPasswordVerificationSuccessResponse(response=response)
  678. else:
  679. self.assertVerificationFailureInvalidCodeResponse(response)
  680. return new_password
  681. def _finish_change_email(
  682. self, confirmation_link, new_email="", expect_success=True
  683. ):
  684. response = self.client.verify(confirmation_link)
  685. if expect_success:
  686. self.assertChangeEmailVerificationSuccessResponse(response, new_email)
  687. self.assertChangeEmailNotificationEmail(self.email)
  688. else:
  689. self.assertVerificationFailureInvalidCodeResponse(response)
  690. def _finish_delete_account(self, confirmation_link):
  691. self.assertDeleteAccountVerificationSuccessResponse(
  692. self.client.verify(confirmation_link)
  693. )
  694. self.assertUserDoesNotExist(self.email)
  695. def test_view_account(self):
  696. response = self.client.view_account(self.token)
  697. self.assertEqual(response.status_code, 200)
  698. self.assertEqual(
  699. response.data.keys(),
  700. {"created", "email", "id", "limit_domains", "outreach_preference"},
  701. )
  702. self.assertEqual(response.data["email"], self.email)
  703. self.assertEqual(
  704. response.data["id"], str(User.objects.get(email=self.email).pk)
  705. )
  706. self.assertEqual(
  707. response.data["limit_domains"], settings.LIMIT_USER_DOMAIN_COUNT_DEFAULT
  708. )
  709. self.assertTrue(response.data["outreach_preference"])
  710. def test_view_account_forbidden_methods(self):
  711. for method in [self.client.post, self.client.delete]:
  712. response = method(
  713. reverse("v1:account"),
  714. {"limit_domains": 99},
  715. HTTP_AUTHORIZATION="Token {}".format(self.token),
  716. )
  717. self.assertResponse(response, status.HTTP_405_METHOD_NOT_ALLOWED)
  718. def test_view_account_update(self):
  719. user = User.objects.get(email=self.email)
  720. immutable_fields = (
  721. "created",
  722. "email",
  723. "id",
  724. "limit_domains",
  725. "password",
  726. )
  727. immutable_values = [getattr(user, key) for key in immutable_fields]
  728. outreach_preference = user.outreach_preference
  729. for method in [self.client.patch, self.client.put]:
  730. outreach_preference = not outreach_preference
  731. response = method(
  732. reverse("v1:account"),
  733. {
  734. "created": "2019-10-16T18:09:17.715702Z",
  735. "email": "youremailaddress@example.com",
  736. "id": "9ab16e5c-805d-4ab1-9030-af3f5a541d47",
  737. "limit_domains": 42,
  738. "password": self.random_password(),
  739. "outreach_preference": outreach_preference,
  740. },
  741. HTTP_AUTHORIZATION="Token {}".format(self.token),
  742. )
  743. self.assertResponse(response, status.HTTP_200_OK)
  744. user = User.objects.get(email=self.email)
  745. self.assertEqual(
  746. outreach_preference, user.outreach_preference
  747. ) # `outreach_preference` updated
  748. self.assertEqual(
  749. immutable_values, [getattr(user, k) for k in immutable_fields]
  750. ) # read-only fields ignored
  751. def test_reset_password(self):
  752. self._test_reset_password(self.email)
  753. def test_reset_password_inactive_user(self):
  754. user = User.objects.get(email=self.email)
  755. for is_active in [False, None]:
  756. user.is_active = is_active
  757. user.save()
  758. self.assertResetPasswordSuccessResponse(self.reset_password(self.email))
  759. self.assertNoEmailSent()
  760. def test_reset_password_inactive_user_old_confirmation_link(self):
  761. user = User.objects.get(email=self.email)
  762. user.needs_captcha = False
  763. self.assertResetPasswordSuccessResponse(self.reset_password(self.email))
  764. confirmation_link = self.assertResetPasswordEmail(self.email)
  765. user.is_active = False
  766. user.save()
  767. new_password = self.random_password()
  768. self.assertConfirmationLinkRedirect(confirmation_link)
  769. self.assertResetPasswordInactiveUserVerificationFailedResponse(
  770. self.client.verify(confirmation_link, data={"new_password": new_password})
  771. )
  772. def test_reset_password_multiple_times(self):
  773. for _ in range(3):
  774. self._test_reset_password(self.email)
  775. mail.outbox = []
  776. def test_reset_password_during_change_email_interleaved(self):
  777. reset_password_verification_code = self._start_reset_password()
  778. change_email_verification_code, new_email = self._start_change_email()
  779. new_password = self._finish_reset_password(reset_password_verification_code)
  780. self._finish_change_email(change_email_verification_code, expect_success=False)
  781. self.assertUserExists(self.email)
  782. self.assertUserDoesNotExist(new_email)
  783. self.assertPassword(self.email, new_password)
  784. def test_reset_password_during_change_email_nested(self):
  785. change_email_verification_code, new_email = self._start_change_email()
  786. reset_password_verification_code = self._start_reset_password()
  787. new_password = self._finish_reset_password(reset_password_verification_code)
  788. self._finish_change_email(change_email_verification_code, expect_success=False)
  789. self.assertUserExists(self.email)
  790. self.assertUserDoesNotExist(new_email)
  791. self.assertPassword(self.email, new_password)
  792. def test_reset_password_without_new_password(self):
  793. confirmation_link = self._start_reset_password()
  794. response = self.client.verify(confirmation_link)
  795. self.assertResponse(response, status.HTTP_400_BAD_REQUEST)
  796. self.assertEqual(response.data["new_password"][0], "This field is required.")
  797. self.assertNoEmailSent()
  798. def test_reset_password_validation_unknown_user(self):
  799. confirmation_link = self._start_reset_password()
  800. self._test_delete_account(self.email, self.password)
  801. self.assertVerificationFailureUnknownUserResponse(
  802. response=self.client.verify(
  803. confirmation_link, data={"new_password": "foobar"}
  804. )
  805. )
  806. self.assertNoEmailSent()
  807. def test_change_email(self):
  808. self._test_change_email()
  809. def test_change_email_requires_password(self):
  810. # Make sure that the account's email address cannot be changed with a token (password required)
  811. new_email = self.random_username()
  812. response = self.client.change_email_token_auth(self.token, new_email=new_email)
  813. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  814. self.assertEqual(response.data["email"][0], "This field is required.")
  815. self.assertEqual(response.data["password"][0], "This field is required.")
  816. self.assertNoEmailSent()
  817. def test_change_email_multiple_times(self):
  818. for _ in range(3):
  819. self._test_change_email()
  820. def test_change_email_user_exists(self):
  821. known_email, _ = self._test_registration()
  822. # We send a verification link to the new email and check account existence only later, upon verification
  823. self.assertChangeEmailSuccessResponse(response=self.change_email(known_email))
  824. def test_change_email_verification_user_exists(self):
  825. new_email = self.random_username()
  826. self.assertChangeEmailSuccessResponse(self.change_email(new_email))
  827. confirmation_link = self.assertChangeEmailVerificationEmail(new_email)
  828. new_email, new_password = self._test_registration(new_email)
  829. self.assertChangeEmailFailureAddressTakenResponse(
  830. response=self.client.verify(confirmation_link)
  831. )
  832. self.assertUserExists(self.email)
  833. self.assertPassword(self.email, self.password)
  834. self.assertUserExists(new_email)
  835. self.assertPassword(new_email, new_password)
  836. def test_change_email_same_email(self):
  837. self.assertChangeEmailFailureSameAddressResponse(
  838. response=self.change_email(self.email)
  839. )
  840. self.assertUserExists(self.email)
  841. def test_change_email_during_reset_password_interleaved(self):
  842. change_email_verification_code, new_email = self._start_change_email()
  843. reset_password_verification_code = self._start_reset_password()
  844. self._finish_change_email(change_email_verification_code, new_email)
  845. self._finish_reset_password(
  846. reset_password_verification_code, expect_success=False
  847. )
  848. self.assertUserExists(new_email)
  849. self.assertUserDoesNotExist(self.email)
  850. self.assertPassword(new_email, self.password)
  851. def test_change_email_during_reset_password_nested(self):
  852. reset_password_verification_code = self._start_reset_password()
  853. change_email_verification_code, new_email = self._start_change_email()
  854. self._finish_change_email(change_email_verification_code, new_email)
  855. self._finish_reset_password(
  856. reset_password_verification_code, expect_success=False
  857. )
  858. self.assertUserExists(new_email)
  859. self.assertUserDoesNotExist(self.email)
  860. self.assertPassword(new_email, self.password)
  861. def test_change_email_nested(self):
  862. verification_code_1, new_email_1 = self._start_change_email()
  863. verification_code_2, new_email_2 = self._start_change_email()
  864. self._finish_change_email(verification_code_2, new_email_2)
  865. self.assertUserDoesNotExist(self.email)
  866. self.assertUserDoesNotExist(new_email_1)
  867. self.assertUserExists(new_email_2)
  868. self._finish_change_email(verification_code_1, expect_success=False)
  869. self.assertUserDoesNotExist(self.email)
  870. self.assertUserDoesNotExist(new_email_1)
  871. self.assertUserExists(new_email_2)
  872. def test_change_email_interleaved(self):
  873. verification_code_1, new_email_1 = self._start_change_email()
  874. verification_code_2, new_email_2 = self._start_change_email()
  875. self._finish_change_email(verification_code_1, new_email_1)
  876. self.assertUserDoesNotExist(self.email)
  877. self.assertUserExists(new_email_1)
  878. self.assertUserDoesNotExist(new_email_2)
  879. self._finish_change_email(verification_code_2, expect_success=False)
  880. self.assertUserDoesNotExist(self.email)
  881. self.assertUserExists(new_email_1)
  882. self.assertUserDoesNotExist(new_email_2)
  883. def test_change_email_validation_unknown_user(self):
  884. confirmation_link, new_email = self._start_change_email()
  885. self._test_delete_account(self.email, self.password)
  886. self.assertVerificationFailureUnknownUserResponse(
  887. response=self.client.verify(confirmation_link)
  888. )
  889. self.assertNoEmailSent()
  890. def test_delete_account_validation_unknown_user(self):
  891. confirmation_link = self._start_delete_account()
  892. self._test_delete_account(self.email, self.password)
  893. self.assertVerificationFailureUnknownUserResponse(
  894. response=self.client.verify(confirmation_link)
  895. )
  896. self.assertNoEmailSent()
  897. def test_delete_account_domains_present(self):
  898. user = User.objects.get(email=self.email)
  899. domain = self.create_domain(owner=user)
  900. self.assertDeleteAccountFailureStillHasDomainsResponse(
  901. self.delete_account(self.email, self.password)
  902. )
  903. domain.delete()
  904. confirmation_link = self._start_delete_account()
  905. domain = self.create_domain(owner=user)
  906. self.assertDeleteAccountFailureStillHasDomainsResponse(
  907. response=self.client.verify(confirmation_link)
  908. )
  909. domain.delete()
  910. self._finish_delete_account(confirmation_link)
  911. def test_reset_password_password_strip(self):
  912. password = " %s " % self.random_password()
  913. self._test_reset_password(self.email, password)
  914. self.assertPassword(self.email, password.strip())
  915. self.assertPassword(self.email, password)
  916. def test_reset_password_no_code_override(self):
  917. password = self.random_password()
  918. self._test_reset_password(self.email, password, code="foobar")
  919. self.assertPassword(self.email, password)
  920. def test_action_code_expired(self):
  921. self.assertResetPasswordSuccessResponse(self.reset_password(self.email))
  922. confirmation_link = self.assertResetPasswordEmail(self.email)
  923. with mock.patch(
  924. "time.time",
  925. return_value=time.time()
  926. + settings.VALIDITY_PERIOD_VERIFICATION_SIGNATURE.total_seconds()
  927. + 1,
  928. ):
  929. response = self.client.verify(
  930. confirmation_link, data={"new_password": self.random_password()}
  931. )
  932. self.assertVerificationFailureExpiredCodeResponse(response)
  933. def test_action_code_confusion(self):
  934. # Obtain change password code
  935. self.assertResetPasswordSuccessResponse(self.reset_password(self.email))
  936. reset_password_link = self.assertResetPasswordEmail(self.email)
  937. path = urlparse(reset_password_link).path
  938. reset_password_code = resolve(path).kwargs.get("code")
  939. # Obtain deletion code
  940. self.assertDeleteAccountSuccessResponse(
  941. self.delete_account(self.email, self.password)
  942. )
  943. delete_link = self.assertDeleteAccountEmail(self.email)
  944. path = urlparse(delete_link).path
  945. deletion_code = resolve(path).kwargs.get("code")
  946. # Swap codes
  947. self.assertNotEqual(reset_password_code, deletion_code)
  948. delete_link = delete_link.replace(deletion_code, reset_password_code)
  949. reset_password_link = reset_password_link.replace(
  950. reset_password_code, deletion_code
  951. )
  952. # Make sure links don't work
  953. self.assertVerificationFailureInvalidCodeResponse(
  954. self.client.verify(delete_link)
  955. )
  956. self.assertVerificationFailureInvalidCodeResponse(
  957. self.client.verify(reset_password_link, data={"new_password": "dummy"})
  958. )
  959. def test_action_code_updates_email_verified(self):
  960. email_verified = User.objects.get(email=self.email).email_verified
  961. with mock.patch("time.time", return_value=time.time() + 1):
  962. self.assertResetPasswordSuccessResponse(self.reset_password(self.email))
  963. confirmation_link = self.assertResetPasswordEmail(self.email)
  964. self.client.verify(confirmation_link) # even without payload
  965. self.assertGreaterEqual(
  966. (
  967. User.objects.get(email=self.email).email_verified - email_verified
  968. ).total_seconds(),
  969. 1,
  970. )
  971. class RenewTestCase(UserManagementTestCase, DomainOwnerTestCase):
  972. DYN = False
  973. def setUp(self):
  974. super().setUp()
  975. self.email, self.password = self._test_registration(
  976. password=self.random_password()
  977. )
  978. def assertRenewDomainEmail(self, recipient, body_contains, pattern, reset=True):
  979. return self.assertEmailSent(
  980. subject_contains="Upcoming domain deletion",
  981. body_contains=body_contains,
  982. recipient=[recipient],
  983. reset=reset,
  984. pattern=pattern,
  985. )
  986. def assertRenewDomainVerificationSuccessResponse(self, response):
  987. return self.assertContains(
  988. response=response,
  989. text="We recorded that your domain ",
  990. status_code=status.HTTP_200_OK,
  991. )
  992. def test_renew_domain_immortal(self):
  993. domain = self.my_domains[0]
  994. domain.renewal_state = Domain.RenewalState.IMMORTAL
  995. domain.save()
  996. for days in [182, 184]:
  997. domain.published = timezone.now() - timedelta(days=days)
  998. domain.renewal_changed = domain.published
  999. domain.save()
  1000. call_command("scavenge-unused")
  1001. self.assertEqual(len(mail.outbox), 0)
  1002. self.assertEqual(
  1003. Domain.objects.get(pk=domain.pk).renewal_state,
  1004. Domain.RenewalState.IMMORTAL,
  1005. )
  1006. self.assertEqual(
  1007. Domain.objects.get(pk=domain.pk).renewal_changed, domain.renewal_changed
  1008. )
  1009. self.assertEqual(
  1010. Domain.objects.get(pk=domain.pk).published, domain.published
  1011. )
  1012. def test_renew_domain_recently_published(self):
  1013. domain = self.my_domains[0]
  1014. for days in [5, 182, 184]:
  1015. domain.published = timezone.now() - timedelta(days=1)
  1016. domain.renewal_changed = timezone.now() - timedelta(days=days)
  1017. domain.rrset_set.update(touched=domain.renewal_changed)
  1018. for renewal_state in [
  1019. Domain.RenewalState.FRESH,
  1020. Domain.RenewalState.NOTIFIED,
  1021. Domain.RenewalState.WARNED,
  1022. ]:
  1023. domain.renewal_state = renewal_state
  1024. domain.save()
  1025. self.assertEqual(
  1026. Domain.objects.get(pk=domain.pk).renewal_state, domain.renewal_state
  1027. )
  1028. call_command("scavenge-unused")
  1029. self.assertEqual(
  1030. Domain.objects.get(pk=domain.pk).renewal_state,
  1031. Domain.RenewalState.FRESH,
  1032. )
  1033. self.assertEqual(
  1034. Domain.objects.get(pk=domain.pk).renewal_changed, domain.published
  1035. )
  1036. self.assertEqual(
  1037. Domain.objects.get(pk=domain.pk).published, domain.published
  1038. )
  1039. self.assertEqual(len(mail.outbox), 0)
  1040. def test_renew_domain_recently_touched(self):
  1041. domain = self.my_domains[0]
  1042. last_active = timezone.now() - timedelta(days=1)
  1043. for days in [5, 182, 184]:
  1044. domain.published = timezone.now() - timedelta(days=days)
  1045. domain.renewal_changed = domain.published
  1046. domain.rrset_set.update(touched=last_active)
  1047. for renewal_state in [
  1048. Domain.RenewalState.FRESH,
  1049. Domain.RenewalState.NOTIFIED,
  1050. Domain.RenewalState.WARNED,
  1051. ]:
  1052. domain.renewal_state = renewal_state
  1053. domain.save()
  1054. self.assertEqual(
  1055. Domain.objects.get(pk=domain.pk).renewal_state, domain.renewal_state
  1056. )
  1057. call_command("scavenge-unused")
  1058. self.assertEqual(
  1059. Domain.objects.get(pk=domain.pk).renewal_state,
  1060. Domain.RenewalState.FRESH,
  1061. )
  1062. self.assertEqual(
  1063. Domain.objects.get(pk=domain.pk).renewal_changed, last_active
  1064. )
  1065. self.assertEqual(
  1066. Domain.objects.get(pk=domain.pk).published, domain.published
  1067. )
  1068. self.assertEqual(len(mail.outbox), 0)
  1069. def test_renew_domain_fresh_182_days(self):
  1070. domain = self.my_domains[0]
  1071. domain.published = timezone.now() - timedelta(days=182)
  1072. domain.renewal_changed = domain.published
  1073. domain.renewal_state = Domain.RenewalState.FRESH
  1074. domain.save()
  1075. self.assertEqual(
  1076. Domain.objects.get(pk=domain.pk).renewal_state, Domain.RenewalState.FRESH
  1077. )
  1078. call_command("scavenge-unused")
  1079. self.assertEqual(
  1080. Domain.objects.get(pk=domain.pk).renewal_state, Domain.RenewalState.FRESH
  1081. )
  1082. self.assertEqual(len(mail.outbox), 0)
  1083. def test_renew_domain_fresh_183_days(self):
  1084. domain = self.my_domains[0]
  1085. domain.published = timezone.now() - timedelta(days=183)
  1086. domain.renewal_changed = domain.published
  1087. domain.renewal_state = Domain.RenewalState.FRESH
  1088. domain.save()
  1089. domain.rrset_set.update(touched=domain.published)
  1090. self.assertEqual(
  1091. Domain.objects.get(pk=domain.pk).renewal_state, Domain.RenewalState.FRESH
  1092. )
  1093. call_command("scavenge-unused")
  1094. self.assertEqual(
  1095. Domain.objects.get(pk=domain.pk).renewal_state, Domain.RenewalState.NOTIFIED
  1096. )
  1097. deletion_date = timezone.localdate() + timedelta(days=28)
  1098. body_contains = [domain.name, deletion_date.strftime("%B %-d, %Y")]
  1099. pattern = (
  1100. r"following link[^:]*:\s+\* "
  1101. + domain.name.replace(".", r"\.")
  1102. + r"\s+([^\s]*)"
  1103. )
  1104. confirmation_link = self.assertRenewDomainEmail(
  1105. domain.owner.email, body_contains, pattern
  1106. )
  1107. self.assertConfirmationLinkRedirect(confirmation_link)
  1108. # Use link after 14 days
  1109. with mock.patch("time.time", return_value=time.time() + 86400 * 14):
  1110. self.assertRenewDomainVerificationSuccessResponse(
  1111. self.client.verify(confirmation_link)
  1112. )
  1113. self.assertLess(
  1114. timezone.now() - Domain.objects.get(pk=domain.pk).renewal_changed,
  1115. timedelta(seconds=1),
  1116. )
  1117. self.assertEqual(
  1118. Domain.objects.get(pk=domain.pk).renewal_state, Domain.RenewalState.FRESH
  1119. )
  1120. # Check that other domains aren't affected
  1121. self.assertGreater(len(self.my_domains), 1)
  1122. for domain in self.my_domains[1:]:
  1123. self.assertLess(
  1124. Domain.objects.get(pk=domain.pk).renewal_state,
  1125. Domain.RenewalState.NOTIFIED,
  1126. )
  1127. self.assertEqual(
  1128. Domain.objects.get(pk=domain.pk).renewal_changed, domain.renewal_changed
  1129. )
  1130. def test_renew_domain_notified_21_days(self):
  1131. domain = self.my_domains[0]
  1132. domain.published = timezone.now() - timedelta(days=183 + 21)
  1133. domain.renewal_state = Domain.RenewalState.NOTIFIED
  1134. domain.renewal_changed = timezone.now() - timedelta(days=21)
  1135. domain.save()
  1136. domain.rrset_set.update(touched=domain.published)
  1137. call_command("scavenge-unused")
  1138. self.assertEqual(
  1139. Domain.objects.get(pk=domain.pk).renewal_state, Domain.RenewalState.WARNED
  1140. )
  1141. deletion_date = timezone.localdate() + timedelta(days=7)
  1142. body_contains = [domain.name, deletion_date.strftime("%B %-d, %Y")]
  1143. pattern = (
  1144. r"following link[^:]*:\s+\* "
  1145. + domain.name.replace(".", r"\.")
  1146. + r"\s+([^\s]*)"
  1147. )
  1148. confirmation_link = self.assertRenewDomainEmail(
  1149. domain.owner.email, body_contains, pattern
  1150. )
  1151. self.assertConfirmationLinkRedirect(confirmation_link)
  1152. # Use link after 6 days
  1153. with mock.patch("time.time", return_value=time.time() + 86400 * 6):
  1154. self.assertRenewDomainVerificationSuccessResponse(
  1155. self.client.verify(confirmation_link)
  1156. )
  1157. self.assertLess(
  1158. timezone.now() - Domain.objects.get(pk=domain.pk).renewal_changed,
  1159. timedelta(seconds=1),
  1160. )
  1161. self.assertEqual(
  1162. Domain.objects.get(pk=domain.pk).renewal_state, Domain.RenewalState.FRESH
  1163. )
  1164. # Check that other domains aren't affected
  1165. self.assertGreater(len(self.my_domains), 1)
  1166. for domain in self.my_domains[1:]:
  1167. self.assertLess(
  1168. Domain.objects.get(pk=domain.pk).renewal_state,
  1169. Domain.RenewalState.NOTIFIED,
  1170. )
  1171. self.assertEqual(
  1172. Domain.objects.get(pk=domain.pk).renewal_changed, domain.renewal_changed
  1173. )
  1174. def test_renew_domain_warned_7_days(self):
  1175. domains = self.my_domains[
  1176. :
  1177. ] # copy list so we can modify it later without side effects
  1178. self.assertGreaterEqual(len(domains), 2)
  1179. while domains:
  1180. domain = domains.pop()
  1181. domain.published = timezone.now() - timedelta(days=183 + 28)
  1182. domain.renewal_state = Domain.RenewalState.WARNED
  1183. domain.renewal_changed = timezone.now() - timedelta(days=7)
  1184. domain.save()
  1185. domain.rrset_set.update(touched=domain.published)
  1186. with self.assertPdnsRequests(
  1187. self.requests_desec_domain_deletion(domain=domain)
  1188. ):
  1189. call_command("scavenge-unused")
  1190. self.assertFalse(Domain.objects.filter(pk=domain.pk).exists())
  1191. # User gets deleted when last domain is purged
  1192. self.assertEqual(
  1193. User.objects.filter(pk=self.owner.pk).exists(), bool(domains)
  1194. )
  1195. # Check that other domains are not affected
  1196. for domain in domains:
  1197. self.assertLess(
  1198. Domain.objects.get(pk=domain.pk).renewal_state,
  1199. Domain.RenewalState.NOTIFIED,
  1200. )
  1201. def test_renew_domain_inactive_user(self):
  1202. domain = self.my_domains[0]
  1203. for is_active in (False, None):
  1204. self.owner.is_active = is_active
  1205. self.owner.save()
  1206. for days in [5, 182, 184]:
  1207. for published_days_ago in [1, 183 + 21, 183 + 28]:
  1208. domain.published = timezone.now() - timedelta(
  1209. days=published_days_ago
  1210. )
  1211. domain.renewal_changed = timezone.now() - timedelta(days=days)
  1212. domain.rrset_set.update(touched=domain.renewal_changed)
  1213. for renewal_state, _ in Domain.RenewalState.choices:
  1214. domain.renewal_state = renewal_state
  1215. domain.save()
  1216. self.assertEqual(
  1217. Domain.objects.get(pk=domain.pk).renewal_state,
  1218. renewal_state,
  1219. )
  1220. call_command("scavenge-unused")
  1221. self.assertEqual(
  1222. Domain.objects.get(pk=domain.pk).renewal_state,
  1223. renewal_state,
  1224. )
  1225. class RenewDynTestCase(RenewTestCase):
  1226. DYN = True
  1227. class RenewNoRRsetTestCase(RenewTestCase):
  1228. def setUp(self):
  1229. super().setUp()
  1230. self.my_domains[0].rrset_set.all().delete()
  1231. def test_renew_domain_recently_touched(self):
  1232. pass
  1233. class RenewDynNoRRsetTestCase(RenewNoRRsetTestCase):
  1234. DYN = True