tests_control.py 92 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2013-2015 The Distro Tracker Developers
  3. # See the COPYRIGHT file at the top-level directory of this distribution and
  4. # at http://deb.li/DTAuthors
  5. #
  6. # This file is part of Distro Tracker. It is subject to the license terms
  7. # in the LICENSE file found in the top-level directory of this
  8. # distribution and at http://deb.li/DTLicense. No part of Distro Tracker,
  9. # including this file, may be copied, modified, propagated, or distributed
  10. # except according to the terms contained in the LICENSE file.
  11. """
  12. Tests for :mod:`distro_tracker.mail.tracker_control`.
  13. """
  14. from __future__ import unicode_literals
  15. from django.conf import settings
  16. from distro_tracker.test import TestCase
  17. from django.core import mail
  18. from distro_tracker.mail import control
  19. from distro_tracker.core.utils import distro_tracker_render_to_string
  20. from distro_tracker.core.utils import extract_email_address_from_header
  21. from distro_tracker.core.utils import get_or_none
  22. from distro_tracker.core.models import PackageName, UserEmail, Subscription
  23. from distro_tracker.core.models import EmailSettings
  24. from distro_tracker.core.models import Keyword
  25. from distro_tracker.core.models import Team
  26. from distro_tracker.core.models import BinaryPackageName
  27. from distro_tracker.core.models import SourcePackageName
  28. from distro_tracker.core.models import SourcePackage
  29. from distro_tracker.accounts.models import User
  30. from distro_tracker.mail.models import CommandConfirmation
  31. from distro_tracker.mail.control.commands import UNIQUE_COMMANDS
  32. from email import encoders
  33. from email.message import Message
  34. from email.mime.multipart import MIMEMultipart
  35. from email.mime.text import MIMEText
  36. from email.mime.base import MIMEBase
  37. from email.utils import make_msgid
  38. from datetime import timedelta
  39. import re
  40. import six
  41. DISTRO_TRACKER_CONTACT_EMAIL = settings.DISTRO_TRACKER_CONTACT_EMAIL
  42. DISTRO_TRACKER_CONTROL_EMAIL = settings.DISTRO_TRACKER_CONTROL_EMAIL
  43. MAX_ALLOWED_ERRORS = settings.DISTRO_TRACKER_MAX_ALLOWED_ERRORS_CONTROL_COMMANDS
  44. class EmailControlTest(TestCase):
  45. def control_process(self):
  46. """
  47. Helper method. Passes the constructed control message to the control
  48. processor.
  49. """
  50. control.process(self.message)
  51. def setUp(self):
  52. self.reset_message()
  53. def set_default_headers(self):
  54. """
  55. Helper method which adds the default headers for each test message.
  56. """
  57. self.message.add_header('From', 'John Doe <john.doe@unknown.com>')
  58. self.message.add_header('To', DISTRO_TRACKER_CONTROL_EMAIL)
  59. self.message.add_header('Subject', 'Commands')
  60. self.message.add_header('Message-ID', make_msgid())
  61. def set_header(self, header_name, header_value):
  62. """
  63. Helper method which sets the given value for the given header.
  64. :param header_name: The name of the header to set
  65. :param header_value: The value of the header to set
  66. """
  67. if header_name in self.message:
  68. del self.message[header_name]
  69. self.message.add_header(header_name, header_value)
  70. def set_input_lines(self, lines):
  71. """
  72. Sets the lines of the message body which represent sent commands.
  73. :param lines: All lines of commands
  74. :param type: iterable
  75. """
  76. payload = '\n'.join(lines)
  77. if self.multipart:
  78. plain_text = MIMEText('plain')
  79. plain_text.set_payload(payload)
  80. self.message.attach(plain_text)
  81. else:
  82. self.message.set_payload(payload)
  83. def make_multipart(self, alternative=False):
  84. """
  85. Helper method which converts the test message into a multipart message.
  86. """
  87. if alternative:
  88. self.message = MIMEMultipart('alternative')
  89. else:
  90. self.message = MIMEMultipart()
  91. self.set_default_headers()
  92. self.multipart = True
  93. def add_part(self, mime_type, subtype, data):
  94. """
  95. Adds the given part to the test message.
  96. :param mime_type: The main MIME type of the new part
  97. :param subtype: The MIME subtype of the new part
  98. :param data: The payload of the part
  99. """
  100. part = MIMEBase(mime_type, subtype)
  101. part.set_payload(data)
  102. if mime_type != 'text':
  103. encoders.encode_base64(part)
  104. self.message.attach(part)
  105. def reset_message(self):
  106. """
  107. Helper method resets any changes made to the test message.
  108. """
  109. self.message = Message()
  110. self.multipart = False
  111. self.set_default_headers()
  112. def make_comment(self, text):
  113. """
  114. Helper function which creates a comment from the given text.
  115. """
  116. return '# ' + text
  117. def assert_response_sent(self, number_of_responses=1):
  118. """
  119. Helper method which asserts that the expected number of responses is
  120. sent.
  121. :param number_of_responses: The expected number of responses.
  122. """
  123. self.assertEqual(len(mail.outbox), number_of_responses)
  124. def assert_response_not_sent(self):
  125. """
  126. Helper method which asserts that no responses were sent.
  127. """
  128. self.assertEqual(len(mail.outbox), 0)
  129. def assert_in_response(self, text, response_number=-1):
  130. """
  131. Helper method which asserts that the given text is found in the given
  132. response message.
  133. :param text: The text which needs to be found in the response.
  134. :param response_number: The index number of the response message.
  135. Standard Python indexing applies, which means that -1 means the
  136. last sent message.
  137. """
  138. self.assertTrue(mail.outbox)
  139. out_mail = mail.outbox[response_number]
  140. self.assertIn(text, out_mail.body)
  141. def assert_line_in_response(self, line, response_number=-1):
  142. """
  143. Helper method which asserts that the given full line of text is found
  144. in the given response message.
  145. :param line: The line of text which needs to be found in the response.
  146. :param response_number: The index number of the response message.
  147. Standard Python indexing applies, which means that -1 means the
  148. last sent message.
  149. """
  150. self.assertTrue(mail.outbox)
  151. out_mail = mail.outbox[response_number]
  152. self.assertIn(line, out_mail.body.splitlines())
  153. def assert_line_not_in_response(self, line, response_number=-1):
  154. """
  155. Helper method which asserts that the given full line of text is not
  156. found in the given response message.
  157. :param line: The line of text which needs to be found in the response.
  158. :param response_number: The index number of the response message.
  159. Standard Python indexing applies, which means that -1 means the
  160. last sent message.
  161. """
  162. self.assertTrue(mail.outbox)
  163. out_mail = mail.outbox[response_number]
  164. self.assertNotIn(line, out_mail.body.splitlines())
  165. def get_list_item(self, item, bullet='*'):
  166. """
  167. Helper method which returns a representation of a list item.
  168. :param item: The list item's content
  169. :type item: string
  170. :param bullet: The character used as the "bullet" of the list.
  171. """
  172. return bullet + ' ' + str(item)
  173. def assert_list_in_response(self, items, bullet='*'):
  174. """
  175. Helper method which asserts that a list of items is found in the
  176. response.
  177. """
  178. self.assert_in_response('\n'.join(
  179. self.get_list_item(item, bullet)
  180. for item in items
  181. ))
  182. def assert_list_item_in_response(self, item, bullet='*'):
  183. """
  184. Helper method which asserts that a single list item is found in the
  185. response.
  186. """
  187. self.assert_line_in_response(self.get_list_item(item, bullet))
  188. def assert_list_item_not_in_response(self, item, bullet='*'):
  189. """
  190. Helper method which asserts that a single list item is not found in the
  191. response.
  192. """
  193. self.assert_line_not_in_response(self.get_list_item(item, bullet))
  194. def assert_not_in_response(self, text, response_number=-1):
  195. """
  196. Helper method which asserts that the given text is not found in the
  197. given response message.
  198. :param text: The text which needs to be found in the response.
  199. :param response_number: The index number of the response message.
  200. Standard Python indexing applies, which means that -1 means the
  201. last sent message.
  202. """
  203. out_mail = mail.outbox[response_number]
  204. self.assertNotIn(text, out_mail.body)
  205. def assert_response_equal(self, text, response_number=-1):
  206. """
  207. Helper method which asserts that the response is completely identical
  208. to the given text.
  209. :param text: The text which the response is compared to.
  210. :param response_number: The index number of the response message.
  211. Standard Python indexing applies, which means that -1 means the
  212. last sent message.
  213. """
  214. out_mail = mail.outbox[response_number]
  215. self.assertEqual(text, out_mail.body)
  216. def assert_header_equal(self, header_name, header_value,
  217. response_number=-1):
  218. """
  219. Helper method which asserts that a particular response's
  220. header value is equal to an expected value.
  221. :param header_name: The name of the header to be tested
  222. :param header_value: The expected value of the header
  223. :param response_number: The index number of the response message.
  224. Standard Python indexing applies, which means that -1 means the
  225. last sent message.
  226. """
  227. out_mail = mail.outbox[response_number].message()
  228. self.assertEqual(out_mail[header_name], header_value)
  229. def assert_command_echo_in_response(self, command):
  230. """
  231. Helper method which asserts that a given command's echo is found in
  232. the response.
  233. """
  234. self.assert_in_response('> ' + command)
  235. def assert_command_echo_not_in_response(self, command):
  236. """
  237. Helper method which asserts that a given command's echo is not found
  238. in the response.
  239. """
  240. self.assert_not_in_response('> ' + command)
  241. def assert_warning_in_response(self, text):
  242. """
  243. Helper method which asserts that a particular warning is found in the
  244. response.
  245. :param text: The text of the warning message.
  246. """
  247. self.assert_in_response("Warning: " + text)
  248. def assert_error_in_response(self, text):
  249. """
  250. Helper method which asserts that a particular error is found in the
  251. response.
  252. :param text: The text of the error message.
  253. """
  254. self.assert_in_response("Error: " + text)
  255. def assert_cc_contains_address(self, email_address):
  256. """
  257. Helper method which checks that the Cc header of the response contains
  258. the given email address.
  259. """
  260. response_mail = mail.outbox[-1]
  261. self.assertIn(
  262. email_address, (
  263. extract_email_address_from_header(email)
  264. for email in response_mail.cc
  265. )
  266. )
  267. def reset_outbox(self):
  268. """
  269. Helper method which resets the structure containing all outgoing
  270. emails.
  271. """
  272. mail.outbox = []
  273. def regex_search_in_response(self, regexp, response_number=0):
  274. """
  275. Helper method which performs a regex search in a response.
  276. """
  277. return regexp.search(mail.outbox[response_number].body)
  278. class ControlBotBasic(EmailControlTest):
  279. def test_basic_headers(self):
  280. """
  281. Tests if the proper headers are set for the reply message, that the
  282. output contains original lines prepended with '>'
  283. """
  284. input_lines = [
  285. "#command",
  286. " thanks",
  287. ]
  288. self.set_header('Subject', 'Commands')
  289. self.set_input_lines(input_lines)
  290. self.control_process()
  291. self.assert_response_sent()
  292. self.assert_header_equal('Subject', 'Re: Commands')
  293. self.assert_header_equal('X-Loop', DISTRO_TRACKER_CONTROL_EMAIL)
  294. self.assert_header_equal('To', self.message['From'])
  295. self.assert_header_equal('From', DISTRO_TRACKER_CONTACT_EMAIL)
  296. self.assert_header_equal('In-Reply-To', self.message['Message-ID'])
  297. references = self.message['Message-ID']
  298. if 'References' in self.message:
  299. references = " " + self.message['References'] + references
  300. self.assert_header_equal('References', references)
  301. def test_response_when_headers_have_two_lines(self):
  302. """
  303. Tests that we don't fail when header fields (References, From, Subject)
  304. contain multiple lines.
  305. """
  306. for sep in ["\n\t", "\n ", "\r\n "]:
  307. self.set_header('References', "<foo@bar>" + sep + "<foo2@bar2>")
  308. self.set_header('From', "This is long" + sep + "<foo@bar.baz>")
  309. self.set_header('Subject', "This is a long" + sep + " subject")
  310. self.set_input_lines(['help'])
  311. self.control_process()
  312. def test_response_when_empty_subject(self):
  313. """
  314. Tests that the subject of the response when there is no subject set in
  315. the request is correct.
  316. """
  317. self.set_header('Subject', '')
  318. self.set_input_lines(['help'])
  319. self.control_process()
  320. self.assert_header_equal('Subject', 'Re: Your mail')
  321. def test_response_when_no_subject(self):
  322. """
  323. Tests that the subject of the response when there is no subject set in
  324. the request is correct.
  325. """
  326. del self.message['Subject']
  327. self.set_input_lines(['help'])
  328. self.control_process()
  329. self.assert_header_equal('Subject', 'Re: Your mail')
  330. def test_basic_echo_commands(self):
  331. """
  332. Tests that commands are echoed in the response.
  333. """
  334. input_lines = [
  335. "#command",
  336. " thanks",
  337. ]
  338. self.set_header('Subject', 'Commands')
  339. self.set_input_lines(input_lines)
  340. self.control_process()
  341. for line in input_lines:
  342. self.assert_command_echo_in_response(line.strip())
  343. def test_not_plaintext(self):
  344. """
  345. Tests that the response to a non-plaintext message is a warning email.
  346. """
  347. self.make_multipart()
  348. self.add_part('application', 'octet-stream', b'asdf')
  349. self.control_process()
  350. self.assert_response_sent()
  351. self.assert_response_equal(distro_tracker_render_to_string(
  352. 'control/email-plaintext-warning.txt'))
  353. def test_multipart_with_plaintext(self):
  354. """
  355. Tests that the response to a multipart message which contains a
  356. text/plain part is correct.
  357. """
  358. self.make_multipart(alternative=True)
  359. input_lines = [
  360. '#command',
  361. 'thanks',
  362. ]
  363. self.set_input_lines(input_lines)
  364. self.add_part('text', 'html', "#command\nthanks")
  365. self.control_process()
  366. self.assert_response_sent()
  367. for line in input_lines:
  368. self.assert_command_echo_in_response(line.strip())
  369. def test_empty_no_response(self):
  370. """
  371. Tests that there is no response to an empty message.
  372. """
  373. self.set_input_lines([])
  374. self.control_process()
  375. self.assert_response_not_sent()
  376. def test_loop_no_response(self):
  377. """
  378. Tests that there is no response if the message's X-Loop is set to
  379. DISTRO_TRACKER_CONTROL_EMAIL
  380. """
  381. self.set_header('X-Loop', 'something-else')
  382. self.set_header('X-Loop', DISTRO_TRACKER_CONTROL_EMAIL)
  383. self.set_input_lines(['thanks'])
  384. self.control_process()
  385. self.assert_response_not_sent()
  386. def test_no_valid_command_no_response(self):
  387. """
  388. Tests that there is no response for a message which does not contain
  389. any valid commands.
  390. """
  391. self.set_input_lines(['Some text', 'Some more text'])
  392. self.control_process()
  393. self.assert_response_not_sent()
  394. def test_stop_after_five_garbage_lines(self):
  395. """
  396. Tests that processing stops after encountering five garbage lines.
  397. """
  398. self.set_input_lines(
  399. ['help'] + ['garbage'] * MAX_ALLOWED_ERRORS + ['#command'])
  400. self.control_process()
  401. self.assert_response_sent()
  402. self.assert_command_echo_not_in_response('#command')
  403. def test_stop_on_thanks_or_quit(self):
  404. """
  405. Tests that processing stops after encountering the thanks or quit
  406. command.
  407. """
  408. self.set_input_lines(['thanks', '#command'])
  409. self.control_process()
  410. self.assert_response_sent()
  411. self.assert_command_echo_in_response('thanks')
  412. self.assert_in_response("Stopping processing here.")
  413. self.assert_command_echo_not_in_response('#command')
  414. def test_blank_line_skip(self):
  415. """
  416. Tests that processing skips any blank lines in the message. They are
  417. not considered garbage.
  418. """
  419. self.set_input_lines(['help', ''] + [' '] * 5 + ['#comment'])
  420. self.control_process()
  421. self.assert_response_sent()
  422. self.assert_command_echo_in_response('#comment')
  423. def test_comment_line_skip(self):
  424. """
  425. Tests that processing skips commented lines and that they are not
  426. considered garbage.
  427. """
  428. self.set_input_lines(
  429. [self.make_comment(command)
  430. for command in ['comment'] * MAX_ALLOWED_ERRORS] + ['help']
  431. )
  432. self.control_process()
  433. self.assert_command_echo_in_response('help')
  434. def test_utf8_message(self):
  435. """
  436. Tests that the bot sends replies to utf-8 encoded messages.
  437. """
  438. lines = ['üšßč', '한글ᥡ╥ສए', 'help']
  439. self.set_input_lines(lines)
  440. self.message.set_charset('utf-8')
  441. self.control_process()
  442. self.assert_response_sent()
  443. for line in lines:
  444. self.assert_command_echo_in_response(line)
  445. def test_subject_command(self):
  446. """
  447. Tests that a command given in the subject of the message is executed.
  448. """
  449. self.set_header('Subject', 'help')
  450. self.set_input_lines([])
  451. self.control_process()
  452. self.assert_response_sent()
  453. self.assert_command_echo_in_response('# Message subject')
  454. self.assert_command_echo_in_response('help')
  455. def test_ensure_no_failure_with_multiline_subject(self):
  456. """Non-regression test for a failure with multi-line subjects"""
  457. self.set_header('Subject', '=?utf-8?B?UkU66L2m6Ze05Li75Lu75LiO54+t6ZW'
  458. '/5Yiw5bqV5piv5LiN5piv55yf5q2j55qE6aKG5a+8?=\n\t'
  459. '=?utf-8?B?Ow==?=')
  460. self.set_input_lines(['help'])
  461. self.control_process()
  462. def test_end_processing_on_signature_delimiter(self):
  463. """
  464. Tests that processing commands halts when the signature delimiter is
  465. reached (--)
  466. """
  467. self.set_input_lines(['help', '--', '# command'])
  468. self.control_process()
  469. self.assert_command_echo_not_in_response('# command')
  470. class ConfirmationTests(EmailControlTest):
  471. """
  472. Tests the command confirmation mechanism.
  473. """
  474. def setUp(self):
  475. super(ConfirmationTests, self).setUp()
  476. self.user_email_address = 'dummy-user@domain.com'
  477. self.set_header('From',
  478. 'Dummy User <{user_email}>'.format(
  479. user_email=self.user_email_address))
  480. # Regular expression to extract the confirmation code from the body of
  481. # the response mail
  482. self.regexp = re.compile(r'^CONFIRM (.*)$', re.MULTILINE)
  483. self.packages = [
  484. PackageName.objects.create(name='dummy-package'),
  485. PackageName.objects.create(name='other-package'),
  486. ]
  487. def user_subscribed(self, email_address, package_name):
  488. """
  489. Helper method checks whether the given email is subscribed to the
  490. package.
  491. """
  492. user_email = get_or_none(UserEmail, email=email_address)
  493. if not user_email:
  494. return False
  495. return user_email.emailsettings.is_subscribed_to(package=package_name)
  496. def assert_confirmation_sent_to(self, email_address):
  497. """
  498. Helper method checks whether a confirmation mail was sent to the
  499. given email address.
  500. """
  501. self.assertIn(
  502. True, (
  503. extract_email_address_from_header(msg.to[0]) == email_address
  504. for msg in mail.outbox[:-1]
  505. )
  506. )
  507. def test_multiple_commands_single_confirmation_email(self):
  508. """
  509. Tests that multiple commands which require confirmation cause only a
  510. single confirmation email.
  511. """
  512. commands = [
  513. 'subscribe ' + package.name + ' ' + self.user_email_address
  514. for package in self.packages
  515. ]
  516. self.set_input_lines(commands)
  517. self.control_process()
  518. # A control commands response and confirmation email sent
  519. self.assert_response_sent(2)
  520. self.assert_confirmation_sent_to(self.user_email_address)
  521. # Contains the confirmation key
  522. self.assertIsNotNone(self.regex_search_in_response(self.regexp))
  523. # A confirmation key really created
  524. self.assertEqual(CommandConfirmation.objects.count(), 1)
  525. # Check the commands associated with the confirmation object.
  526. c = CommandConfirmation.objects.all()[0]
  527. self.assertEqual('\n'.join(commands), c.commands)
  528. for command in commands:
  529. self.assert_in_response(command)
  530. # Finally make sure the commands did not actually execute
  531. self.assertEqual(Subscription.objects.filter(active=True).count(), 0)
  532. def test_subscribe_command_confirmation_message(self):
  533. """
  534. Tests that the custom confirmation messages for commands are correctly
  535. included in the confirmation email.
  536. """
  537. Subscription.objects.create_for(
  538. email=self.user_email_address,
  539. package_name=self.packages[1].name)
  540. commands = [
  541. 'unsubscribeall',
  542. 'unsubscribe ' + self.packages[1].name,
  543. 'subscribe ' + self.packages[0].name,
  544. ]
  545. self.set_input_lines(commands)
  546. self.control_process()
  547. expected_messages = [
  548. distro_tracker_render_to_string(
  549. 'control/email-unsubscribeall-confirmation.txt'
  550. ),
  551. distro_tracker_render_to_string(
  552. 'control/email-unsubscribe-confirmation.txt', {
  553. 'package': self.packages[1].name,
  554. }
  555. ),
  556. distro_tracker_render_to_string(
  557. 'control/email-subscription-confirmation.txt', {
  558. 'package': self.packages[0].name,
  559. }
  560. )
  561. ]
  562. c = CommandConfirmation.objects.all()[0]
  563. self.assert_response_equal(
  564. distro_tracker_render_to_string(
  565. 'control/email-confirmation-required.txt', {
  566. 'command_confirmation': c,
  567. 'confirmation_messages': expected_messages,
  568. }
  569. ),
  570. response_number=0
  571. )
  572. def test_multiple_commands_confirmed(self):
  573. """
  574. Tests that multiple commands are actually confirmed by a single key.
  575. """
  576. commands = [
  577. 'subscribe ' + package.name + ' ' + self.user_email_address
  578. for package in self.packages
  579. ]
  580. c = CommandConfirmation.objects.create_for_commands(commands)
  581. self.set_input_lines(['CONFIRM ' + c.confirmation_key])
  582. self.control_process()
  583. self.assert_response_sent()
  584. for package in self.packages:
  585. self.assertTrue(
  586. self.user_subscribed(self.user_email_address, package.name))
  587. for command in commands:
  588. self.assert_command_echo_in_response(command)
  589. # Key no longer usable
  590. self.assertEqual(CommandConfirmation.objects.count(), 0)
  591. def test_multiple_commands_per_user(self):
  592. """
  593. Tests that if multiple emails should receive a confirmation email for
  594. some commands, each of them gets only one.
  595. """
  596. commands = []
  597. commands.extend([
  598. 'subscribe ' + package.name + ' ' + self.user_email_address
  599. for package in self.packages
  600. ])
  601. other_user = 'other-user@domain.com'
  602. commands.extend([
  603. 'subscribe ' + package.name + ' ' + other_user
  604. for package in self.packages
  605. ])
  606. self.set_input_lines(commands)
  607. self.control_process()
  608. # A control commands response and confirmation emails sent
  609. self.assert_response_sent(3)
  610. self.assert_confirmation_sent_to(self.user_email_address)
  611. self.assert_confirmation_sent_to(other_user)
  612. self.assertEqual(CommandConfirmation.objects.count(), 2)
  613. # Control message CCed to both of them.
  614. self.assert_cc_contains_address(self.user_email_address)
  615. self.assert_cc_contains_address(other_user)
  616. def test_same_command_repeated(self):
  617. """
  618. Tests that when the same command is repeated in the command email, it
  619. is included just once in the confirmation email.
  620. """
  621. package = self.packages[0]
  622. self.set_input_lines([
  623. 'subscribe ' + package.name + ' ' + self.user_email_address,
  624. 'subscribe ' + package.name + ' ' + self.user_email_address,
  625. ])
  626. self.control_process()
  627. self.assert_response_sent(2)
  628. c = CommandConfirmation.objects.all()[0]
  629. self.assertEqual(
  630. 'subscribe ' + package.name + ' ' + self.user_email_address,
  631. c.commands)
  632. def test_confirm_only_if_needs_confirmation(self):
  633. """
  634. Tests that only the commands which need confirmation are included in
  635. the confirmation email.
  636. """
  637. Subscription.objects.create_for(
  638. email=self.user_email_address,
  639. package_name=self.packages[1].name)
  640. package = self.packages[0]
  641. self.set_input_lines([
  642. 'unsubscribeall',
  643. 'which',
  644. 'help',
  645. 'subscribe ' + package.name + ' ' + self.user_email_address,
  646. 'who',
  647. 'keywords',
  648. 'unsubscribe ' + self.packages[1].name + ' ' +
  649. self.user_email_address,
  650. ])
  651. self.control_process()
  652. self.assert_response_sent(2)
  653. c = CommandConfirmation.objects.all()[0]
  654. expected = '\n'.join([
  655. 'unsubscribeall ' + self.user_email_address,
  656. 'subscribe ' + package.name + ' ' + self.user_email_address,
  657. 'unsubscribe ' + self.packages[1].name + ' ' +
  658. self.user_email_address,
  659. ])
  660. self.assertEqual(expected, c.commands)
  661. def test_unknown_confirmation_key(self):
  662. """
  663. Tests the confirm command when an unknown key is given.
  664. """
  665. self.set_input_lines(['CONFIRM asdf'])
  666. self.control_process()
  667. self.assert_response_sent()
  668. self.assert_error_in_response('Confirmation failed: unknown key')
  669. class HelpCommandTest(EmailControlTest):
  670. """
  671. Tests for the help command.
  672. """
  673. def get_all_help_command_descriptions(self):
  674. """
  675. Helper method returning the description of all commands.
  676. """
  677. return (cmd.META.get('description', '') for cmd in UNIQUE_COMMANDS)
  678. def test_help_command(self):
  679. """
  680. Tests that the help command returns all the available commands and
  681. their descriptions.
  682. """
  683. self.set_input_lines(['help'])
  684. self.control_process()
  685. self.assert_in_response(distro_tracker_render_to_string(
  686. 'control/help.txt',
  687. {'descriptions': self.get_all_help_command_descriptions()}
  688. ))
  689. class KeywordCommandHelperMixin(object):
  690. """
  691. Contains some methods which are used for testing all forms of the keyword
  692. command.
  693. """
  694. def assert_keywords_in_response(self, keywords):
  695. """
  696. Checks if the given keywords are found in the response.
  697. """
  698. for keyword in keywords:
  699. self.assert_list_item_in_response(keyword)
  700. def assert_keywords_not_in_response(self, keywords):
  701. """
  702. Checks that the given keywords are not found in the response.
  703. """
  704. for keyword in keywords:
  705. self.assert_list_item_not_in_response(keyword)
  706. class KeywordCommandSubscriptionSpecificTest(EmailControlTest,
  707. KeywordCommandHelperMixin):
  708. """
  709. Tests for the keyword command when modifying subscription specific
  710. keywords.
  711. """
  712. def setUp(self):
  713. super(KeywordCommandSubscriptionSpecificTest, self).setUp()
  714. # Setup a subscription
  715. self.package = PackageName.objects.create(name='dummy-package')
  716. self.user = UserEmail.objects.create(email='user@domain.com')
  717. self.email_settings = EmailSettings.objects.create(user_email=self.user)
  718. self.subscription = Subscription.objects.create(
  719. package=self.package,
  720. email_settings=self.email_settings
  721. )
  722. self.default_keywords = set(
  723. keyword.name
  724. for keyword in self.subscription.keywords.filter(default=True))
  725. self.commands = []
  726. self.set_header('From', self.user.email)
  727. def _to_command_string(self, command):
  728. """
  729. Helper method turning a tuple representing a keyword command into a
  730. string.
  731. """
  732. return ' '.join(
  733. command[:-1] + (', '.join(command[-1]),)
  734. )
  735. def add_keyword_command(self, package, operator, keywords, email=None,
  736. use_tag=False):
  737. if email is None:
  738. email = ''
  739. command = 'keyword' if not use_tag else 'tag'
  740. self.commands.append((
  741. command,
  742. package,
  743. email,
  744. operator,
  745. keywords,
  746. ))
  747. self.set_input_lines(self._to_command_string(command)
  748. for command in self.commands)
  749. def get_new_list_of_keywords_text(self, package, email):
  750. """
  751. Returns the status text which should precede a new list of keywords.
  752. """
  753. return (
  754. "Here's the new list of accepted keywords associated to package\n"
  755. "{package} for {address} :".format(package=package,
  756. address=self.user.email)
  757. )
  758. def assert_error_user_not_subscribed_in_response(self, email, package):
  759. """
  760. Checks whether an error saying the user is not subscribed to a package
  761. is in the response.
  762. """
  763. self.assert_error_in_response(
  764. '{email} is not subscribed to the package {package}'.format(
  765. email=email, package=package)
  766. )
  767. def assert_subscription_keywords_equal(self, keywords):
  768. """
  769. Asserts that the subscription of the test user to the test package is
  770. equal to the given keywords.
  771. """
  772. self.subscription = Subscription.objects.get(
  773. package=self.package,
  774. email_settings=self.email_settings
  775. )
  776. all_keywords = self.subscription.keywords.all()
  777. self.assertEqual(all_keywords.count(), len(keywords))
  778. for keyword in all_keywords:
  779. self.assertIn(keyword.name, keywords)
  780. def assert_subscription_has_keywords(self, keywords):
  781. """
  782. Check if the subscription of the test user to the test package has the
  783. given keywords.
  784. """
  785. self.subscription = Subscription.objects.get(
  786. package=self.package,
  787. email_settings=self.email_settings
  788. )
  789. all_keywords = self.subscription.keywords.all()
  790. for keyword in keywords:
  791. self.assertIn(Keyword.objects.get(name=keyword), all_keywords)
  792. def assert_subscription_not_has_keywords(self, keywords):
  793. """
  794. Assert that the subscription of the test user to the test package does
  795. not have the given keywords.
  796. """
  797. self.subscription = Subscription.objects.get(
  798. package=self.package,
  799. email_settings=self.email_settings
  800. )
  801. all_keywords = self.subscription.keywords.all()
  802. for keyword in keywords:
  803. self.assertNotIn(Keyword.objects.get(name=keyword), all_keywords)
  804. def test_add_keyword_to_subscription(self):
  805. """
  806. Tests the keyword command version which should add a keyword to the
  807. subscription.
  808. """
  809. keywords = ['vcs', 'contact']
  810. self.add_keyword_command(self.package.name,
  811. '+',
  812. keywords,
  813. self.user.email)
  814. self.control_process()
  815. self.assert_keywords_in_response(keywords)
  816. self.assert_subscription_has_keywords(keywords)
  817. def test_remove_keyword_from_subscription(self):
  818. """
  819. Tests the keyword command version which should remove a keyword from a
  820. subscription.
  821. """
  822. keywords = ['bts']
  823. self.add_keyword_command(self.package.name,
  824. '-',
  825. keywords,
  826. self.user.email)
  827. self.control_process()
  828. self.assert_keywords_not_in_response(keywords)
  829. self.assert_subscription_not_has_keywords(keywords)
  830. def test_set_keywords_for_subscription(self):
  831. """
  832. Tests the keyword command version which should set a new keyword list
  833. for a subscription.
  834. """
  835. keywords = ['vcs', 'bts']
  836. self.add_keyword_command(self.package.name,
  837. '=',
  838. keywords,
  839. self.user.email)
  840. self.control_process()
  841. self.assert_in_response(self.get_new_list_of_keywords_text(
  842. self.package.name, self.user.email))
  843. self.assert_keywords_in_response(keywords)
  844. self.assert_subscription_keywords_equal(keywords)
  845. def test_keyword_email_not_given(self):
  846. """
  847. Tests the keyword command when the email is not given.
  848. """
  849. self.add_keyword_command(self.package.name, '+', ['vcs'])
  850. self.control_process()
  851. self.assert_in_response(self.get_new_list_of_keywords_text(
  852. self.package.name, self.user.email))
  853. self.assert_keywords_in_response(['vcs'])
  854. self.assert_subscription_has_keywords(['vcs'])
  855. def test_keyword_doesnt_exist(self):
  856. """
  857. Tests the keyword command when the given keyword does not exist.
  858. """
  859. self.add_keyword_command(self.package.name, '+', ['no-exist'])
  860. self.control_process()
  861. self.assert_warning_in_response('no-exist is not a valid keyword')
  862. # Subscription has not changed.
  863. self.assert_keywords_in_response(self.default_keywords)
  864. self.assert_subscription_keywords_equal(self.default_keywords)
  865. def test_keyword_add_subscription_not_confirmed(self):
  866. """
  867. Tests the keyword command when the user has not yet confirmed the
  868. subscription (it is pending).
  869. """
  870. self.subscription.active = False
  871. self.subscription.save()
  872. self.add_keyword_command(self.package.name, '+', ['vcs'])
  873. self.control_process()
  874. self.assert_in_response(self.get_new_list_of_keywords_text(
  875. self.package.name, self.user.email))
  876. self.assert_keywords_in_response(['vcs'])
  877. self.assert_subscription_has_keywords(['vcs'])
  878. def test_keyword_add_package_doesnt_exist(self):
  879. """
  880. Tests the keyword command when the given package does not exist.
  881. """
  882. self.add_keyword_command('package-no-exist', '+', ['vcs'])
  883. self.control_process()
  884. self.assert_in_response('Package package-no-exist does not exist')
  885. self.assert_not_in_response(self.get_new_list_of_keywords_text(
  886. self.package.name, self.user.email))
  887. def test_keyword_user_not_subscribed(self):
  888. """
  889. Tests the keyword command when the user is not subscribed to the given
  890. package.
  891. """
  892. other_user = UserEmail.objects.create(email='other-user@domain.com')
  893. self.add_keyword_command(self.package.name,
  894. '+',
  895. ['vcs'],
  896. other_user.email)
  897. self.control_process()
  898. self.assert_error_user_not_subscribed_in_response(other_user.email,
  899. self.package.name)
  900. self.assert_not_in_response(self.get_new_list_of_keywords_text(
  901. self.package.name, other_user.email))
  902. def test_keyword_user_doesnt_exist(self):
  903. """
  904. Tests the keyword command when the user is not subscribed to any
  905. package.
  906. """
  907. email = 'other-user@domain.com'
  908. self.add_keyword_command(self.package.name,
  909. '+',
  910. ['vcs'],
  911. email)
  912. self.control_process()
  913. self.assert_error_user_not_subscribed_in_response(email,
  914. self.package.name)
  915. self.assert_not_in_response(self.get_new_list_of_keywords_text(
  916. self.package.name, self.user.email))
  917. def test_keyword_alias_tag(self):
  918. """
  919. Tests that tag works as an alias for keyword.
  920. """
  921. keywords = ['vcs', 'contact']
  922. self.add_keyword_command(self.package.name,
  923. '+',
  924. keywords,
  925. self.user.email,
  926. use_tag=True)
  927. self.control_process()
  928. self.assert_keywords_in_response(keywords)
  929. self.assert_subscription_has_keywords(keywords)
  930. class KeywordCommandListSubscriptionSpecific(EmailControlTest,
  931. KeywordCommandHelperMixin):
  932. """
  933. Tests the keyword command when used to list keywords associated with a
  934. subscription.
  935. """
  936. def setUp(self):
  937. super(KeywordCommandListSubscriptionSpecific, self).setUp()
  938. # Setup a subscription
  939. self.package = PackageName.objects.create(name='dummy-package')
  940. self.user = UserEmail.objects.create(email='user@domain.com')
  941. self.email_settings = EmailSettings.objects.create(user_email=self.user)
  942. self.subscription = Subscription.objects.create(
  943. package=self.package,
  944. email_settings=self.email_settings
  945. )
  946. self.commands = []
  947. self.set_header('From', self.user.email)
  948. def _to_command_string(self, command):
  949. return ' '.join(command)
  950. def add_keyword_command(self, package, email='', use_tag=False):
  951. command = 'keyword' if not use_tag else 'tag'
  952. self.commands.append((
  953. command,
  954. package,
  955. email,
  956. ))
  957. self.set_input_lines(self._to_command_string(command)
  958. for command in self.commands)
  959. def get_list_of_keywords(self, package, email):
  960. return (
  961. "Here's the list of accepted keywords associated to package\n"
  962. "{package} for {user}".format(
  963. package=self.package.name, user=self.user.email)
  964. )
  965. def test_keyword_user_default(self):
  966. """
  967. Tests the keyword command when the subscription is using the user's
  968. default keywords.
  969. """
  970. self.email_settings.default_keywords.add(
  971. Keyword.objects.create(name='new-keyword'))
  972. self.add_keyword_command(self.package.name, self.user.email)
  973. self.control_process()
  974. self.assert_in_response(
  975. self.get_list_of_keywords(self.package.name, self.user.email))
  976. self.assert_keywords_in_response(
  977. keyword.name for keyword in self.subscription.keywords.all())
  978. def test_keyword_subscription_specific(self):
  979. """
  980. Tests the keyword command when the subscription has specific keywords
  981. associated with it.
  982. """
  983. self.subscription.keywords.add(Keyword.objects.get(name='vcs'))
  984. self.add_keyword_command(self.package.name, self.user.email)
  985. self.control_process()
  986. self.assert_in_response(
  987. self.get_list_of_keywords(self.package.name, self.user.email))
  988. self.assert_keywords_in_response(
  989. keyword.name for keyword in self.subscription.keywords.all())
  990. def test_keyword_package_doesnt_exist(self):
  991. """
  992. Tests the keyword command when the given package does not exist.
  993. """
  994. self.add_keyword_command('no-exist', self.user.email)
  995. self.control_process()
  996. self.assert_error_in_response('Package no-exist does not exist')
  997. self.assert_not_in_response("Here's the list of accepted keywords")
  998. def test_keyword_subscription_not_active(self):
  999. """
  1000. Tests the keyword command when the user has not yet confirmed the
  1001. subscription to the given package.
  1002. """
  1003. self.subscription.active = False
  1004. self.subscription.save()
  1005. self.add_keyword_command(self.package.name, self.user.email)
  1006. self.control_process()
  1007. self.assert_in_response(
  1008. self.get_list_of_keywords(self.package.name, self.user.email))
  1009. self.assert_keywords_in_response(
  1010. keyword.name for keyword in self.subscription.keywords.all())
  1011. def test_keyword_user_not_subscribed(self):
  1012. """
  1013. Tests the keyword command when the given user is not subscribed to the
  1014. given package.
  1015. """
  1016. self.subscription.delete()
  1017. self.add_keyword_command(self.package.name, self.user.email)
  1018. self.control_process()
  1019. self.assert_response_sent()
  1020. self.assert_error_in_response(
  1021. '{email} is not subscribed to the package {pkg}'.format(
  1022. email=self.user.email,
  1023. pkg=self.package.name)
  1024. )
  1025. self.assert_not_in_response("Here's the list of accepted keywords")
  1026. def test_keyword_email_not_given(self):
  1027. """
  1028. Tests the keyword command when the email is not given in the command.
  1029. """
  1030. self.add_keyword_command(self.package.name)
  1031. self.control_process()
  1032. self.assert_in_response(
  1033. self.get_list_of_keywords(self.package.name, self.user.email))
  1034. self.assert_keywords_in_response(
  1035. keyword.name for keyword in self.subscription.keywords.all())
  1036. def test_tag_same_as_keyword(self):
  1037. """
  1038. Tests that "tag" acts as an alias for "keyword"
  1039. """
  1040. self.add_keyword_command(self.package.name, self.user.email)
  1041. self.control_process()
  1042. self.assert_in_response(
  1043. self.get_list_of_keywords(self.package.name, self.user.email))
  1044. self.assert_keywords_in_response(
  1045. keyword.name for keyword in self.subscription.keywords.all())
  1046. class KeywordCommandModifyDefault(EmailControlTest, KeywordCommandHelperMixin):
  1047. """
  1048. Tests the keyword command version which modifies a user's list of default
  1049. keywords.
  1050. """
  1051. def setUp(self):
  1052. super(KeywordCommandModifyDefault, self).setUp()
  1053. # Setup a subscription
  1054. self.user = UserEmail.objects.create(email='user@domain.com')
  1055. self.email_settings = EmailSettings.objects.create(user_email=self.user)
  1056. self.default_keywords = set([
  1057. keyword.name
  1058. for keyword in self.email_settings.default_keywords.all()
  1059. ])
  1060. self.commands = []
  1061. self.set_header('From', self.user.email)
  1062. def _to_command_string(self, command):
  1063. """
  1064. Helper method turning a tuple representing a keyword command into a
  1065. string.
  1066. """
  1067. return ' '.join(
  1068. command[:-1] + (', '.join(command[-1]),)
  1069. )
  1070. def get_new_default_list_output_message(self, email):
  1071. """
  1072. Returns the message which should precede the list of new default
  1073. keywords.
  1074. """
  1075. return (
  1076. "Here's the new default list of accepted "
  1077. "keywords for {email} :".format(email=email)
  1078. )
  1079. def add_keyword_command(self, operator, keywords, email='', use_tag=False):
  1080. command = 'keyword' if not use_tag else 'tag'
  1081. self.commands.append((
  1082. command,
  1083. email,
  1084. operator,
  1085. keywords,
  1086. ))
  1087. self.set_input_lines(self._to_command_string(command)
  1088. for command in self.commands)
  1089. def assert_keywords_in_user_default_list(self, keywords):
  1090. """
  1091. Asserts that the given keywords are found in the user's list of default
  1092. keywords.
  1093. """
  1094. default_keywords = self.email_settings.default_keywords.all()
  1095. for keyword in keywords:
  1096. self.assertIn(Keyword.objects.get(name=keyword), default_keywords)
  1097. def assert_keywords_not_in_user_default_list(self, keywords):
  1098. """
  1099. Asserts that the given keywords are not found in the user's list of
  1100. default keywords.
  1101. """
  1102. default_keywords = self.email_settings.default_keywords.all()
  1103. for keyword in keywords:
  1104. self.assertNotIn(
  1105. Keyword.objects.get(name=keyword), default_keywords)
  1106. def assert_keywords_user_default_list_equal(self, keywords):
  1107. """
  1108. Asserts that the user's list of default keywords exactly matches the
  1109. given keywords.
  1110. """
  1111. default_keywords = self.email_settings.default_keywords.all()
  1112. self.assertEqual(default_keywords.count(), len(keywords))
  1113. for keyword in default_keywords:
  1114. self.assertIn(keyword.name, keywords)
  1115. def test_keyword_add_default(self):
  1116. """
  1117. Tests that the keyword command adds a new keyword to the user's list of
  1118. default keywords.
  1119. """
  1120. keywords = [
  1121. keyword.name
  1122. for keyword in Keyword.objects.filter(default=False)[:3]
  1123. ]
  1124. self.add_keyword_command('+', keywords, self.user.email)
  1125. self.control_process()
  1126. self.assert_in_response(
  1127. self.get_new_default_list_output_message(self.user.email))
  1128. self.assert_keywords_in_response(keywords)
  1129. self.assert_keywords_in_user_default_list(keywords)
  1130. def test_keyword_remove_default(self):
  1131. """
  1132. Tests that the keyword command removes keywords from the user's list of
  1133. default keywords.
  1134. """
  1135. keywords = [
  1136. keyword.name
  1137. for keyword in Keyword.objects.filter(default=True)[:3]
  1138. ]
  1139. self.add_keyword_command('-', keywords, self.user.email)
  1140. self.control_process()
  1141. self.assert_in_response(
  1142. self.get_new_default_list_output_message(self.user.email))
  1143. self.assert_keywords_not_in_response(keywords)
  1144. self.assert_keywords_not_in_user_default_list(keywords)
  1145. def test_keyword_set_default(self):
  1146. """
  1147. Tests that the keyword command sets a new list of the user's default
  1148. keywords.
  1149. """
  1150. keywords = [
  1151. keyword.name
  1152. for keyword in Keyword.objects.filter(default=False)[:5]
  1153. ]
  1154. keywords.extend(
  1155. keyword.name
  1156. for keyword in Keyword.objects.filter(default=True)[:2]
  1157. )
  1158. self.add_keyword_command(' = ', keywords, self.user.email)
  1159. self.control_process()
  1160. self.assert_in_response(
  1161. self.get_new_default_list_output_message(self.user.email))
  1162. self.assert_keywords_in_response(keywords)
  1163. self.assert_keywords_user_default_list_equal(keywords)
  1164. def test_keyword_email_not_given(self):
  1165. """
  1166. Tests the keyword command when the email is not given.
  1167. """
  1168. keywords = [
  1169. keyword.name
  1170. for keyword in Keyword.objects.filter(default=False)[:3]
  1171. ]
  1172. self.add_keyword_command(' +', keywords)
  1173. self.control_process()
  1174. self.assert_in_response(
  1175. self.get_new_default_list_output_message(self.user.email))
  1176. self.assert_keywords_in_response(keywords)
  1177. self.assert_keywords_in_user_default_list(keywords)
  1178. def test_keyword_doesnt_exist(self):
  1179. """
  1180. Tests the keyword command when a nonexistant keyword is given.
  1181. """
  1182. self.add_keyword_command('+', ['no-exist'])
  1183. self.control_process()
  1184. self.assert_warning_in_response('no-exist is not a valid keyword')
  1185. self.assert_keywords_not_in_response(['no-exist'])
  1186. def test_user_doesnt_exist(self):
  1187. """
  1188. Tests adding a keyword to a user's default list of subscriptions when
  1189. the given user is not subscribed to any packages (it does not exist yet)
  1190. """
  1191. all_default_keywords = [
  1192. keyword.name
  1193. for keyword in Keyword.objects.filter(default=True)
  1194. ]
  1195. new_user = 'doesnt-exist@domain.com'
  1196. keywords = [Keyword.objects.filter(default=False)[0].name]
  1197. self.add_keyword_command('+', keywords, new_user)
  1198. self.control_process()
  1199. # User created
  1200. self.assertEqual(UserEmail.objects.filter(email=new_user).count(), 1)
  1201. self.assert_in_response(
  1202. self.get_new_default_list_output_message(new_user))
  1203. self.assert_keywords_in_response(keywords + all_default_keywords)
  1204. class KeywordCommandShowDefault(EmailControlTest, KeywordCommandHelperMixin):
  1205. def setUp(self):
  1206. super(KeywordCommandShowDefault, self).setUp()
  1207. self.user = UserEmail.objects.create(email='user@domain.com')
  1208. self.email_settings = EmailSettings.objects.create(user_email=self.user)
  1209. self.email_settings.default_keywords.add(
  1210. Keyword.objects.filter(default=False)[0])
  1211. self.set_header('From', self.user.email)
  1212. def get_default_keywords_list_message(self, email):
  1213. """
  1214. Returns the message which should precede the list of all default
  1215. keywords in the output of the command.
  1216. """
  1217. return (
  1218. "Here's the default list of accepted keywords for {email}:".format(
  1219. email=email)
  1220. )
  1221. def test_show_default_keywords(self):
  1222. """
  1223. Tests that the keyword command outputs all default keywords of a user.
  1224. """
  1225. self.set_input_lines(['keyword ' + self.user.email])
  1226. self.control_process()
  1227. self.assert_in_response(
  1228. self.get_default_keywords_list_message(self.user.email))
  1229. self.assert_keywords_in_response(
  1230. keyword.name for keyword in
  1231. self.email_settings.default_keywords.all()
  1232. )
  1233. def test_show_default_keywords_email_not_given(self):
  1234. """
  1235. Tests that the keyword command shows all default keywords of a user
  1236. when the email is not given in the command.
  1237. """
  1238. self.set_input_lines(['keyword'])
  1239. self.control_process()
  1240. self.assert_in_response(
  1241. self.get_default_keywords_list_message(self.user.email))
  1242. self.assert_keywords_in_response(
  1243. keyword.name for keyword in
  1244. self.email_settings.default_keywords.all()
  1245. )
  1246. def test_show_default_keywords_email_no_subscriptions(self):
  1247. """
  1248. Tests that the keyword command returns a list of default keywords for
  1249. users that are not subscribed to any packages.
  1250. """
  1251. email = 'no-exist@domain.com'
  1252. self.set_input_lines(['keyword ' + email])
  1253. self.control_process()
  1254. # User created first...
  1255. self.assertEqual(UserEmail.objects.filter(email=email).count(), 1)
  1256. user = UserEmail.objects.get(email=email)
  1257. self.assert_in_response(
  1258. self.get_default_keywords_list_message(user.email))
  1259. self.assert_keywords_in_response(
  1260. keyword.name for keyword in
  1261. user.emailsettings.default_keywords.all()
  1262. )
  1263. def test_tag_alias_for_keyword(self):
  1264. """
  1265. Tests that "tag" is an alias for "keyword"
  1266. """
  1267. self.set_input_lines(['tag ' + self.user.email])
  1268. self.control_process()
  1269. self.assert_in_response(
  1270. self.get_default_keywords_list_message(self.user.email))
  1271. self.assert_keywords_in_response(
  1272. keyword.name for keyword in
  1273. self.email_settings.default_keywords.all()
  1274. )
  1275. def test_tags_alias_for_keyword(self):
  1276. """
  1277. Tests that 'tags' is an alias for 'keyword'
  1278. """
  1279. self.set_input_lines(['tags ' + self.user.email])
  1280. self.control_process()
  1281. self.assert_in_response(
  1282. self.get_default_keywords_list_message(self.user.email))
  1283. self.assert_keywords_in_response(
  1284. keyword.name for keyword in
  1285. self.email_settings.default_keywords.all()
  1286. )
  1287. def test_keywords_alias_for_keyword(self):
  1288. """
  1289. Tests that 'keywords' is an alias for 'keyword'
  1290. """
  1291. self.set_input_lines(['keywords ' + self.user.email])
  1292. self.control_process()
  1293. self.assert_in_response(
  1294. self.get_default_keywords_list_message(self.user.email))
  1295. self.assert_keywords_in_response(
  1296. keyword.name for keyword in
  1297. self.email_settings.default_keywords.all()
  1298. )
  1299. class SubscribeToPackageTest(EmailControlTest):
  1300. """
  1301. Tests for the subscribe to package story.
  1302. """
  1303. def setUp(self):
  1304. super(SubscribeToPackageTest, self).setUp()
  1305. self.user_email_address = 'dummy-user@domain.com'
  1306. self.set_header('From',
  1307. 'Dummy User <{user_email}>'.format(
  1308. user_email=self.user_email_address))
  1309. # Regular expression to extract the confirmation code from the body of
  1310. # the response mail
  1311. self.regexp = re.compile(r'^CONFIRM (.*)$', re.MULTILINE)
  1312. self.package = PackageName.objects.create(
  1313. source=True,
  1314. name='dummy-package')
  1315. def user_subscribed(self, email_address):
  1316. """
  1317. Helper method checks whether the given email is subscribed to the
  1318. package.
  1319. """
  1320. u = get_or_none(UserEmail, email=email_address)
  1321. if not u:
  1322. return False
  1323. return u.emailsettings.is_subscribed_to(package=self.package)
  1324. def assert_confirmation_sent_to(self, email_address):
  1325. """
  1326. Helper method checks whether a confirmation mail was sent to the
  1327. given email address.
  1328. """
  1329. self.assertIn(
  1330. True, (
  1331. extract_email_address_from_header(msg.to[0]) == email_address
  1332. for msg in mail.outbox[:-1]
  1333. )
  1334. )
  1335. def add_binary_package(self, source_package, binary_package):
  1336. """
  1337. Helper method which creates a binary package for the given source
  1338. package.
  1339. """
  1340. binary_pkg = BinaryPackageName.objects.create(
  1341. name=binary_package)
  1342. src_pkg_name = SourcePackageName.objects.get(name=source_package.name)
  1343. src_pkg, _ = SourcePackage.objects.get_or_create(
  1344. source_package_name=src_pkg_name, version='1.0.0')
  1345. src_pkg.binary_packages = [binary_pkg]
  1346. src_pkg.save()
  1347. def add_subscribe_command(self, package, email=None):
  1348. """
  1349. Helper method which adds a subscribe command to the command message.
  1350. """
  1351. if not email:
  1352. email = ''
  1353. payload = self.message.get_payload() or ''
  1354. commands = payload.splitlines()
  1355. commands.append('subscribe ' + package + ' ' + email)
  1356. self.set_input_lines(commands)
  1357. def get_not_source_nor_binary_warning(self, package_name):
  1358. return (
  1359. '{pkg} is neither a source package nor a binary package.'.format(
  1360. pkg=package_name)
  1361. )
  1362. def test_subscribe_and_confirm_normal(self):
  1363. """
  1364. Tests that the user is subscribed to the pacakge after running
  1365. subscribe and confirm.
  1366. """
  1367. package_name = self.package.name
  1368. self.add_subscribe_command(package_name, self.user_email_address)
  1369. self.control_process()
  1370. self.assert_in_response(
  1371. 'A confirmation mail has been sent to {email}'.format(
  1372. email=self.user_email_address))
  1373. self.assert_confirmation_sent_to(self.user_email_address)
  1374. # User still not actually subscribed
  1375. self.assertFalse(self.user_subscribed(self.user_email_address))
  1376. # Check that the confirmation mail contains the confirmation code
  1377. match = self.regex_search_in_response(self.regexp)
  1378. self.assertIsNotNone(match)
  1379. # Extract the code and send a confirmation mail
  1380. self.reset_message()
  1381. self.reset_outbox()
  1382. self.set_input_lines([match.group(0)])
  1383. self.control_process()
  1384. self.assert_in_response(
  1385. '{email} has been subscribed to {package}'.format(
  1386. email=self.user_email_address,
  1387. package=package_name))
  1388. self.assertTrue(self.user_subscribed(self.user_email_address))
  1389. def test_subscribe_when_user_already_subscribed(self):
  1390. """
  1391. Tests the subscribe command in the case that the user is trying to
  1392. subscribe to a package he is already subscribed to.
  1393. """
  1394. # Make sure the user is already subscribed.
  1395. Subscription.objects.create_for(
  1396. package_name=self.package.name,
  1397. email=self.user_email_address
  1398. )
  1399. # Try subscribing again
  1400. self.add_subscribe_command(self.package.name, self.user_email_address)
  1401. self.control_process()
  1402. self.assert_warning_in_response(
  1403. '{email} is already subscribed to {package}'.format(
  1404. email=self.user_email_address,
  1405. package=self.package.name))
  1406. def test_subscribe_no_email_given(self):
  1407. """
  1408. Tests the subscribe command when there is no email address given.
  1409. """
  1410. self.add_subscribe_command(self.package.name)
  1411. self.control_process()
  1412. self.assert_confirmation_sent_to(self.user_email_address)
  1413. def test_subscribe_email_different_than_from(self):
  1414. """
  1415. Tests the subscribe command when the given email address is different
  1416. than the From address of the received message.
  1417. """
  1418. subscribe_email_address = 'another-user@domain.com'
  1419. self.assertNotEqual(
  1420. subscribe_email_address,
  1421. self.user_email_address,
  1422. 'The test checks the case when <email> is different than From'
  1423. )
  1424. self.add_subscribe_command(self.package.name, subscribe_email_address)
  1425. self.control_process()
  1426. self.assert_cc_contains_address(subscribe_email_address)
  1427. self.assert_confirmation_sent_to(subscribe_email_address)
  1428. def test_subscribe_unexisting_source_package(self):
  1429. """
  1430. Tests the subscribe command when the given package is not an existing
  1431. source package.
  1432. """
  1433. binary_package = 'binary-package'
  1434. self.add_binary_package(self.package, binary_package)
  1435. self.add_subscribe_command(binary_package)
  1436. self.control_process()
  1437. self.assert_warning_in_response(
  1438. '{package} is not a source package.'.format(
  1439. package=binary_package))
  1440. self.assert_in_response(
  1441. '{package} is the source package '
  1442. 'for the {binary} binary package'.format(
  1443. package=self.package.name,
  1444. binary=binary_package))
  1445. self.assert_confirmation_sent_to(self.user_email_address)
  1446. def test_subscribe_unexisting_package(self):
  1447. """
  1448. Tests the subscribe command when the given package is not an existing
  1449. source, binary or pseudo package.
  1450. """
  1451. package_name = 'random-package-name'
  1452. self.add_subscribe_command(package_name)
  1453. self.control_process()
  1454. self.assert_warning_in_response(
  1455. self.get_not_source_nor_binary_warning(package_name))
  1456. self.assert_warning_in_response(
  1457. 'Package {package} is not even a pseudo package'.format(
  1458. package=package_name))
  1459. self.assert_confirmation_sent_to(self.user_email_address)
  1460. # A new package was created.
  1461. self.assertIsNotNone(get_or_none(PackageName, name=package_name))
  1462. def test_subscribe_subscription_only_package(self):
  1463. """
  1464. Tests that when subscribing to a subscription-only package the correct
  1465. warning is displayed even when it already contains subscriptions.
  1466. """
  1467. package_name = 'random-package-name'
  1468. Subscription.objects.create_for(
  1469. email='user@domain.com', package_name=package_name)
  1470. # Make sure the package actually exists before running the test
  1471. pkg = get_or_none(PackageName, name=package_name)
  1472. self.assertIsNotNone(pkg)
  1473. self.assertFalse(pkg.binary)
  1474. self.add_subscribe_command(package_name)
  1475. self.control_process()
  1476. self.assert_warning_in_response(
  1477. self.get_not_source_nor_binary_warning(package_name))
  1478. self.assert_warning_in_response(
  1479. 'Package {package} is not even a pseudo package'.format(
  1480. package=package_name))
  1481. self.assert_confirmation_sent_to(self.user_email_address)
  1482. def test_subscribe_pseudo_package(self):
  1483. """
  1484. Tests the subscribe command when the given package is an existing
  1485. pseudo-package.
  1486. """
  1487. pseudo_package = 'pseudo-package'
  1488. PackageName.pseudo_packages.create(name=pseudo_package)
  1489. self.add_subscribe_command(pseudo_package)
  1490. self.control_process()
  1491. self.assert_warning_in_response(
  1492. self.get_not_source_nor_binary_warning(pseudo_package))
  1493. self.assert_warning_in_response(
  1494. 'Package {package} is a pseudo package'.format(
  1495. package=pseudo_package))
  1496. self.assert_confirmation_sent_to(self.user_email_address)
  1497. def test_subscribe_execute_once(self):
  1498. """
  1499. If the command message includes the same subscribe command multiple
  1500. times, it is executed only once.
  1501. """
  1502. self.add_subscribe_command(self.package.name)
  1503. self.add_subscribe_command(self.package.name, self.user_email_address)
  1504. self.control_process()
  1505. # Only one confirmation email required as the subscribe commands are
  1506. # equivalent.
  1507. self.assert_response_sent(2)
  1508. def test_confirm_expired(self):
  1509. """
  1510. Tests that an expired confirmation does not subscribe the user.
  1511. """
  1512. # Set up an expired CommandConfirmation object.
  1513. c = CommandConfirmation.objects.create_for_commands(
  1514. ['subscribe {package} {user}'.format(user=self.user_email_address,
  1515. package=self.package.name)])
  1516. delta = timedelta(
  1517. days=settings.DISTRO_TRACKER_CONFIRMATION_EXPIRATION_DAYS + 1)
  1518. c.date_created = c.date_created - delta
  1519. c.save()
  1520. self.set_input_lines(['confirm ' + c.confirmation_key])
  1521. self.control_process()
  1522. self.assert_error_in_response('Confirmation failed')
  1523. def test_subscribe_to_invalid_package_name(self):
  1524. self.set_input_lines(['subscribe /..abc'])
  1525. self.control_process()
  1526. self.assert_warning_in_response('Invalid package name: /..abc')
  1527. def test_bug_user_without_emailsettings(self):
  1528. """
  1529. Non-regression test for a failure when UserEmail has no associated
  1530. EmailSettings object.
  1531. """
  1532. user, _ = UserEmail.objects.get_or_create(email=self.user_email_address)
  1533. if six.PY3:
  1534. with self.assertRaisesRegex(Exception,
  1535. 'UserEmail has no emailsettings'):
  1536. user.emailsettings
  1537. else:
  1538. with self.assertRaisesRegexp(Exception,
  1539. 'UserEmail has no emailsettings'):
  1540. user.emailsettings
  1541. self.add_subscribe_command(self.package.name, self.user_email_address)
  1542. self.control_process() # Must not raise anything
  1543. class UnsubscribeFromPackageTest(EmailControlTest):
  1544. """
  1545. Tests for the unsubscribe from package story.
  1546. """
  1547. def setUp(self):
  1548. super(UnsubscribeFromPackageTest, self).setUp()
  1549. self.user_email_address = 'dummy-user@domain.com'
  1550. self.set_header('From',
  1551. 'Dummy User <{user_email}>'.format(
  1552. user_email=self.user_email_address))
  1553. self.package = PackageName.objects.create(
  1554. source=True,
  1555. name='dummy-package')
  1556. self.other_package = PackageName.objects.create(name='other-package')
  1557. # The user is initially subscribed to the package
  1558. Subscription.objects.create_for(
  1559. package_name=self.package.name,
  1560. email=self.user_email_address)
  1561. self.other_user = 'another-user@domain.com'
  1562. Subscription.objects.create_for(
  1563. package_name=self.package.name,
  1564. email=self.other_user)
  1565. # Regular expression to extract the confirmation code from the body of
  1566. # the response mail
  1567. self.regexp = re.compile(r'^CONFIRM (.*)$', re.MULTILINE)
  1568. def user_subscribed(self, email_address):
  1569. """
  1570. Helper method checks whether the given email is subscribed to the
  1571. package.
  1572. """
  1573. return email_address in (
  1574. user_email.email
  1575. for user_email in self.package.subscriptions.all()
  1576. )
  1577. def assert_confirmation_sent_to(self, email_address):
  1578. """
  1579. Helper method checks whether a confirmation mail was sent to the
  1580. given email address.
  1581. """
  1582. self.assertIn(
  1583. True, (
  1584. extract_email_address_from_header(msg.to[0]) == email_address
  1585. for msg in mail.outbox[:-1]
  1586. )
  1587. )
  1588. def assert_not_subscribed_error_in_response(self, email):
  1589. self.assert_error_in_response(
  1590. "{email} is not subscribed, you can't unsubscribe.".format(
  1591. email=email))
  1592. def add_binary_package(self, source_package, binary_package):
  1593. """
  1594. Helper method which creates a binary package for the given source
  1595. package.
  1596. """
  1597. binary_pkg = BinaryPackageName.objects.create(
  1598. name=binary_package)
  1599. src_pkg_name = SourcePackageName.objects.get(name=source_package.name)
  1600. src_pkg, _ = SourcePackage.objects.get_or_create(
  1601. source_package_name=src_pkg_name, version='1.0.0')
  1602. src_pkg.binary_packages = [binary_pkg]
  1603. src_pkg.save()
  1604. def add_unsubscribe_command(self, package, email=None):
  1605. """
  1606. Helper method which adds a subscribe command to the command message.
  1607. """
  1608. if not email:
  1609. email = ''
  1610. payload = self.message.get_payload() or ''
  1611. commands = payload.splitlines()
  1612. commands.append('unsubscribe ' + package + ' ' + email)
  1613. self.set_input_lines(commands)
  1614. def test_unsubscribe_and_confirm_normal(self):
  1615. """
  1616. Tests that the user is unsubscribed from the pacakge after running
  1617. unsubscribe and confirm.
  1618. """
  1619. package_name = self.package.name
  1620. self.add_unsubscribe_command(package_name, self.user_email_address)
  1621. self.control_process()
  1622. self.assert_in_response(
  1623. 'A confirmation mail has been sent to {email}'.format(
  1624. email=self.user_email_address))
  1625. self.assert_confirmation_sent_to(self.user_email_address)
  1626. # User still not actually unsubscribed
  1627. self.assertTrue(self.user_subscribed(self.user_email_address))
  1628. # Check that the confirmation mail contains the confirmation code
  1629. match = self.regex_search_in_response(self.regexp)
  1630. self.assertIsNotNone(match)
  1631. # Extract the code and send a confirmation mail
  1632. self.reset_message()
  1633. self.reset_outbox()
  1634. self.set_input_lines([match.group(0)])
  1635. self.control_process()
  1636. self.assert_in_response(
  1637. '{email} has been unsubscribed from {package}'.format(
  1638. email=self.user_email_address,
  1639. package=package_name))
  1640. # User no longer subscribed
  1641. self.assertFalse(self.user_subscribed(self.user_email_address))
  1642. def test_unsubscribe_when_user_not_subscribed(self):
  1643. """
  1644. Tests the unsubscribe command when the user is not subscribed to the
  1645. given package.
  1646. """
  1647. self.add_unsubscribe_command(self.other_package.name,
  1648. self.user_email_address)
  1649. self.control_process()
  1650. self.assert_not_subscribed_error_in_response(self.user_email_address)
  1651. def test_unsubscribe_inactive_subscription(self):
  1652. """
  1653. Tests the unsubscribe command when the user's subscription is not
  1654. active.
  1655. """
  1656. Subscription.objects.create_for(
  1657. package_name=self.other_package.name,
  1658. email=self.user_email_address,
  1659. active=False)
  1660. self.add_unsubscribe_command(self.other_package.name,
  1661. self.user_email_address)
  1662. self.control_process()
  1663. self.assert_not_subscribed_error_in_response(self.user_email_address)
  1664. def test_unsubscribe_no_email_given(self):
  1665. """
  1666. Tests the unsubscribe command when there is no email address given.
  1667. """
  1668. self.add_unsubscribe_command(self.package.name)
  1669. self.control_process()
  1670. self.assert_confirmation_sent_to(self.user_email_address)
  1671. def test_unsubscribe_email_different_than_from(self):
  1672. """
  1673. Tests the unsubscribe command when the given email address is different
  1674. than the From address of the received message.
  1675. """
  1676. self.add_unsubscribe_command(self.package.name,
  1677. self.other_user)
  1678. self.control_process()
  1679. self.assert_cc_contains_address(self.other_user)
  1680. self.assert_confirmation_sent_to(self.other_user)
  1681. def test_unsubscribe_unexisting_source_package(self):
  1682. """
  1683. Tests the unsubscribe command when the given package is not an existing
  1684. source package.
  1685. """
  1686. binary_package = 'binary-package'
  1687. self.add_binary_package(self.package, binary_package)
  1688. self.add_unsubscribe_command(binary_package)
  1689. self.control_process()
  1690. self.assert_in_response(
  1691. 'Warning: {package} is not a source package.'.format(
  1692. package=binary_package))
  1693. self.assert_in_response(
  1694. '{package} is the source package '
  1695. 'for the {binary} binary package'.format(
  1696. package=self.package.name,
  1697. binary=binary_package))
  1698. def test_unsubscribe_unexisting_source_or_binary_package(self):
  1699. """
  1700. Tests the unsubscribe command when the given package is neither an
  1701. existing source nor an existing binary package.
  1702. """
  1703. binary_package = 'binary-package'
  1704. self.add_unsubscribe_command(binary_package)
  1705. self.control_process()
  1706. self.assert_warning_in_response(
  1707. '{package} is neither a source package '
  1708. 'nor a binary package.'.format(package=binary_package))
  1709. def test_unsubscribe_execute_once(self):
  1710. """
  1711. If the command message includes the same subscribe command multiple
  1712. times, it is executed only once.
  1713. """
  1714. self.add_unsubscribe_command(self.package.name)
  1715. self.add_unsubscribe_command(self.package.name, self.user_email_address)
  1716. self.control_process()
  1717. # Only one confirmation email required as the commands are equivalent
  1718. self.assert_response_sent(2)
  1719. class UnsubscribeallCommandTest(EmailControlTest):
  1720. """
  1721. Tests for the unsubscribeall command.
  1722. """
  1723. def setUp(self):
  1724. super(UnsubscribeallCommandTest, self).setUp()
  1725. self.user_email_address = 'dummy-user@domain.com'
  1726. self.set_header('From',
  1727. 'Dummy User <{user_email}>'.format(
  1728. user_email=self.user_email_address))
  1729. self.package = PackageName.objects.create(name='dummy-package')
  1730. self.other_package = PackageName.objects.create(name='other-package')
  1731. # The user is initially subscribed to the package
  1732. Subscription.objects.create_for(
  1733. package_name=self.package.name,
  1734. email=self.user_email_address)
  1735. Subscription.objects.create_for(
  1736. package_name=self.other_package.name,
  1737. email=self.user_email_address,
  1738. active=False)
  1739. self.user = UserEmail.objects.get(email=self.user_email_address)
  1740. # Regular expression to extract the confirmation code from the body of
  1741. # the response mail
  1742. self.regexp = re.compile(r'^CONFIRM (.*)$', re.MULTILINE)
  1743. def assert_confirmation_sent_to(self, email_address):
  1744. """
  1745. Helper method checks whether a confirmation mail was sent to the
  1746. given email address.
  1747. """
  1748. self.assertIn(
  1749. True, (
  1750. extract_email_address_from_header(msg.to[0]) == email_address
  1751. for msg in mail.outbox[:-1]
  1752. )
  1753. )
  1754. def test_unsubscribeall_and_confirm(self):
  1755. """
  1756. Tests the unsubscribeall command with the confirmation.
  1757. """
  1758. old_subscriptions = [pkg.name for pkg
  1759. in self.user.emailsettings.packagename_set.all()]
  1760. self.set_input_lines(['unsubscribeall ' + self.user.email])
  1761. self.control_process()
  1762. self.assert_in_response(
  1763. "A confirmation mail has been sent to " + self.user.email)
  1764. self.assert_confirmation_sent_to(self.user.email)
  1765. match = self.regex_search_in_response(self.regexp)
  1766. self.assertIsNotNone(match)
  1767. self.reset_message()
  1768. self.reset_outbox()
  1769. self.set_input_lines([match.group(0)])
  1770. self.control_process()
  1771. self.assert_in_response('All your subscriptions have been terminated')
  1772. self.assert_list_in_response(
  1773. '{email} has been unsubscribed from {pkg}@{fqdn}'.format(
  1774. email=self.user.email,
  1775. pkg=package,
  1776. fqdn=settings.DISTRO_TRACKER_FQDN)
  1777. for package in sorted(old_subscriptions)
  1778. )
  1779. def test_unsubscribeall_no_subscriptions(self):
  1780. """
  1781. Tests the unsubscribeall command when the user is not subscribed to any
  1782. packages.
  1783. """
  1784. self.user.emailsettings.subscription_set.all().delete()
  1785. self.set_input_lines(['unsubscribeall ' + self.user.email])
  1786. self.control_process()
  1787. self.assert_warning_in_response(
  1788. 'User {email} is not subscribed to any packages'.format(
  1789. email=self.user.email))
  1790. def test_unsubscribeall_email_different_than_from(self):
  1791. """
  1792. Tests the unsubscribeall when the email given in the command is
  1793. different than the one in the From header.
  1794. """
  1795. self.set_input_lines(['unsubscribeall ' + self.user.email])
  1796. self.set_header('From', 'other-email@domain.com')
  1797. self.control_process()
  1798. self.assert_cc_contains_address(self.user.email)
  1799. self.assert_confirmation_sent_to(self.user.email)
  1800. def test_unsubscribeall_no_email_given(self):
  1801. """
  1802. Tests the unsubscribeall command when no email is given in the message.
  1803. """
  1804. self.set_input_lines(['unsubscribeall'])
  1805. self.control_process()
  1806. self.assert_confirmation_sent_to(self.user.email)
  1807. class WhichCommandTest(EmailControlTest):
  1808. """
  1809. Tests for the which command.
  1810. """
  1811. def setUp(self):
  1812. super(WhichCommandTest, self).setUp()
  1813. self.packages = [
  1814. PackageName.objects.create(name='package' + str(i))
  1815. for i in range(10)
  1816. ]
  1817. self.user = UserEmail.objects.create(email='user@domain.com')
  1818. self.email_settings = EmailSettings.objects.create(user_email=self.user)
  1819. def assert_no_subscriptions_in_response(self):
  1820. self.assert_in_response('No subscriptions found')
  1821. def test_list_packages_subscribed_to(self):
  1822. """
  1823. Tests that the which command lists the right packages.
  1824. """
  1825. subscriptions = [
  1826. Subscription.objects.create(
  1827. package=self.packages[i],
  1828. email_settings=self.email_settings
  1829. )
  1830. for i in range(5)
  1831. ]
  1832. self.set_input_lines(['which ' + self.user.email])
  1833. self.control_process()
  1834. self.assert_list_in_response(sub.package.name for sub in subscriptions)
  1835. def test_list_packages_no_email_given(self):
  1836. """
  1837. Tests that the which command lists the right packages when no email is
  1838. given.
  1839. """
  1840. subscriptions = [
  1841. Subscription.objects.create(
  1842. package=self.packages[i],
  1843. email_settings=self.email_settings
  1844. )
  1845. for i in range(5)
  1846. ]
  1847. self.set_header('From', self.user.email)
  1848. self.set_input_lines(['which'])
  1849. self.control_process()
  1850. self.assert_list_in_response(sub.package.name for sub in subscriptions)
  1851. def test_list_packages_no_subscriptions(self):
  1852. """
  1853. Tests the which command when the user is not subscribed to any packages.
  1854. """
  1855. self.set_input_lines(['which ' + self.user.email])
  1856. self.control_process()
  1857. self.assert_no_subscriptions_in_response()
  1858. def test_list_packages_no_active_subscriptions(self):
  1859. """
  1860. Tests the which command when the user does not have any active
  1861. subscriptions.
  1862. """
  1863. Subscription.objects.create(
  1864. package=self.packages[0],
  1865. email_settings=self.email_settings,
  1866. active=False)
  1867. self.set_input_lines(['which ' + self.user.email])
  1868. self.control_process()
  1869. self.assert_no_subscriptions_in_response()
  1870. class WhoCommandTest(EmailControlTest):
  1871. """
  1872. Tests for the who command.
  1873. """
  1874. def setUp(self):
  1875. super(WhoCommandTest, self).setUp()
  1876. self.package = PackageName.objects.create(name='dummy-package')
  1877. self.users = [
  1878. UserEmail.objects.create(email='user@domain.com'),
  1879. UserEmail.objects.create(email='second-user@domain.com'),
  1880. ]
  1881. def get_command_message(self):
  1882. """
  1883. Helper function returns the message that the command should output
  1884. before the list of all packages.
  1885. """
  1886. return "Here's the list of subscribers to package {package}".format(
  1887. package=self.package)
  1888. def test_list_all_subscribers(self):
  1889. """
  1890. Tests that all subscribers are output.
  1891. """
  1892. # Subscribe users
  1893. for user in self.users:
  1894. email_settings, _ = \
  1895. EmailSettings.objects.get_or_create(user_email=user)
  1896. Subscription.objects.create(email_settings=email_settings,
  1897. package=self.package)
  1898. self.set_input_lines(['who ' + self.package.name])
  1899. self.control_process()
  1900. self.assert_in_response(self.get_command_message())
  1901. # Check that all users are in the response
  1902. for user in self.users:
  1903. self.assert_in_response(user.email.rsplit('@', 1)[0])
  1904. # Check that their exact addresses aren't
  1905. for user in self.users:
  1906. self.assert_not_in_response(user.email)
  1907. def test_package_does_not_exist(self):
  1908. """
  1909. Tests the who command when the given package does not exist.
  1910. """
  1911. self.set_input_lines(['who no-exist'])
  1912. self.control_process()
  1913. self.assert_in_response('Package no-exist does not exist')
  1914. def test_no_subscribers(self):
  1915. """
  1916. Tests the who command when the given package does not have any
  1917. subscribers.
  1918. """
  1919. self.set_input_lines(['who ' + self.package.name])
  1920. self.control_process()
  1921. self.assert_in_response(
  1922. 'Package {package} does not have any subscribers'.format(
  1923. package=self.package.name))
  1924. class TeamCommandsMixin(object):
  1925. def setUp(self):
  1926. super(TeamCommandsMixin, self).setUp()
  1927. self.password = 'asdf'
  1928. self.user = User.objects.create_user(
  1929. main_email='user@domain.com', password=self.password,
  1930. first_name='', last_name='')
  1931. self.team = Team.objects.create_with_slug(
  1932. owner=self.user, name="Team name")
  1933. self.package = PackageName.objects.create(name='dummy')
  1934. self.team.packages.add(self.package)
  1935. self.team.add_members(self.user.emails.all()[:1])
  1936. def get_confirmation_text(self, email):
  1937. return 'A confirmation mail has been sent to {}'.format(email)
  1938. def assert_confirmation_sent_to(self, email_address):
  1939. """
  1940. Asserts that a confirmation mail has been sent to the given email.
  1941. """
  1942. self.assertTrue(any(
  1943. msg.message()['Subject'].startswith('CONFIRM') and
  1944. email_address in msg.to
  1945. for msg in mail.outbox
  1946. ))
  1947. class JoinTeamCommandsTests(TeamCommandsMixin, EmailControlTest):
  1948. """
  1949. Tests for the join-team control command.
  1950. """
  1951. def setUp(self):
  1952. super(JoinTeamCommandsTests, self).setUp()
  1953. self.user_email = UserEmail.objects.create(email='other@domain.com')
  1954. self.set_header('From', self.user_email.email)
  1955. def get_join_command(self, team, email=''):
  1956. return 'join-team {} {}'.format(team, email)
  1957. def get_joined_message(self, team):
  1958. return 'You have successfully joined the team "{}"'.format(team.name)
  1959. def get_private_error(self, team):
  1960. return (
  1961. "The given team is not public. "
  1962. "Please contact {} if you wish to join".format(
  1963. team.owner.main_email)
  1964. )
  1965. def get_no_exist_error(self, team):
  1966. return 'Team with the slug "{}" does not exist.'.format(team)
  1967. def get_is_member_warning(self):
  1968. return 'You are already a member of the team.'
  1969. def test_join_public_team(self):
  1970. """
  1971. Tests that users can join a public team.
  1972. """
  1973. self.set_input_lines([self.get_join_command(self.team.slug)])
  1974. self.control_process()
  1975. # Confirmation mail sent
  1976. self.assert_confirmation_sent_to(self.user_email.email)
  1977. # The response to the original control message indicates that
  1978. self.assert_in_response(
  1979. self.get_confirmation_text(self.user_email.email))
  1980. # The user isn't a member of the team yet
  1981. self.assertNotIn(self.user_email, self.team.members.all())
  1982. # A confirmation instance is created
  1983. self.assertEqual(1, CommandConfirmation.objects.count())
  1984. confirmation = CommandConfirmation.objects.all()[0]
  1985. # Send the confirmation mail now
  1986. self.reset_outbox()
  1987. self.set_input_lines(['CONFIRM ' + confirmation.confirmation_key])
  1988. self.control_process()
  1989. # The response indicates that the user has joined the team
  1990. self.assert_in_response(self.get_joined_message(self.team))
  1991. # The user now really is in the team
  1992. self.assertIn(self.user_email, self.team.members.all())
  1993. def test_join_public_team_different_from(self):
  1994. """
  1995. Tests that a confirmation mail is sent to the user being added to the
  1996. team, not the user who sent the control command.
  1997. """
  1998. self.set_input_lines([self.get_join_command(self.team.slug,
  1999. self.user_email)])
  2000. self.set_header('From', 'different-user@domain.com')
  2001. self.control_process()
  2002. # The confirmation sent to the user being added to the team
  2003. self.assert_confirmation_sent_to(self.user_email.email)
  2004. def test_join_private_team(self):
  2005. """
  2006. Tests that trying to join a private team fails.
  2007. """
  2008. self.team.public = False
  2009. self.team.save()
  2010. self.set_input_lines([self.get_join_command(self.team.slug)])
  2011. self.control_process()
  2012. self.assert_error_in_response(self.get_private_error(self.team))
  2013. def test_join_non_existing_team(self):
  2014. """
  2015. Tests that trying to join a non-existing team fails.
  2016. """
  2017. team_slug = 'team-does-not-exist'
  2018. self.set_input_lines([self.get_join_command(team_slug)])
  2019. self.control_process()
  2020. self.assert_error_in_response(self.get_no_exist_error(team_slug))
  2021. def test_join_team_already_member(self):
  2022. """
  2023. Tests that a user gets a warning when trying to join a team he is
  2024. already a member of.
  2025. """
  2026. self.team.add_members([self.user_email])
  2027. self.set_input_lines([self.get_join_command(self.team.slug,
  2028. self.user_email)])
  2029. self.control_process()
  2030. self.assert_warning_in_response(self.get_is_member_warning())
  2031. class LeaveTeamCommandTests(TeamCommandsMixin, EmailControlTest):
  2032. def setUp(self):
  2033. super(LeaveTeamCommandTests, self).setUp()
  2034. self.user_email = UserEmail.objects.create(email='other@domain.com')
  2035. self.team.add_members([self.user_email])
  2036. self.set_header('From', self.user_email.email)
  2037. def get_leave_command(self, team, email=''):
  2038. return 'leave-team {} {}'.format(team, email)
  2039. def get_left_team_message(self, team):
  2040. return 'You have successfully left the team "{}" (slug: {})'.format(
  2041. team,
  2042. team.slug)
  2043. def get_is_not_member_warning(self):
  2044. return 'You are not a member of the team.'
  2045. def get_no_exist_error(self, team):
  2046. return 'Team with the slug "{}" does not exist.'.format(team)
  2047. def test_leave_team(self):
  2048. """
  2049. Tests the normal situation where the user leaves a team he is a
  2050. member of.
  2051. """
  2052. self.set_input_lines([self.get_leave_command(self.team.slug)])
  2053. self.control_process()
  2054. # A confirmation sent to the user
  2055. self.assert_confirmation_sent_to(self.user_email.email)
  2056. # Which is displayed in the response to the original message
  2057. self.assert_in_response(
  2058. self.get_confirmation_text(self.user_email.email))
  2059. # The user is still a member of the team
  2060. self.assertIn(self.user_email, self.team.members.all())
  2061. # A confirmation is created
  2062. self.assertEqual(1, CommandConfirmation.objects.count())
  2063. confirmation = CommandConfirmation.objects.all()[0]
  2064. # Now confirm the email
  2065. self.reset_outbox()
  2066. self.set_input_lines(['CONFIRM ' + confirmation.confirmation_key])
  2067. self.control_process()
  2068. # The user notified that he has left the team
  2069. self.assert_in_response(self.get_left_team_message(self.team))
  2070. # The user is no longer a member of the team
  2071. self.assertNotIn(self.user_email, self.team.members.all())
  2072. def test_leave_team_different_from(self):
  2073. """
  2074. Tests that a confirmation message is sent to the user being removed
  2075. from the team, not the one that sent the control message.
  2076. """
  2077. self.set_input_lines(
  2078. [self.get_leave_command(self.team.slug, self.user_email.email)])
  2079. self.set_header('From', 'some-other-user@domain.com')
  2080. self.control_process()
  2081. # Confirmation sent to the user being removed from the team
  2082. self.assert_confirmation_sent_to(self.user_email.email)
  2083. def test_leave_team_not_member(self):
  2084. """
  2085. Tests that a warning is returned when the user tries to leave a team
  2086. that he is not a member of.
  2087. """
  2088. self.team.remove_members([self.user_email])
  2089. self.set_input_lines([self.get_leave_command(self.team.slug)])
  2090. self.control_process()
  2091. self.assert_warning_in_response(self.get_is_not_member_warning())
  2092. def test_leave_team_does_not_exist(self):
  2093. """
  2094. Tests that an error is returned when the user tries to leave a team
  2095. that does not even exist.
  2096. """
  2097. team_slug = 'this-does-not-exist'
  2098. self.set_input_lines([self.get_leave_command(team_slug)])
  2099. self.control_process()
  2100. self.assert_error_in_response(self.get_no_exist_error(team_slug))
  2101. class ListTeamPackagesTests(TeamCommandsMixin, EmailControlTest):
  2102. def setUp(self):
  2103. super(ListTeamPackagesTests, self).setUp()
  2104. # Add some more packages to the team
  2105. self.team.packages.create(name='pkg1')
  2106. self.team.packages.create(name='pkg2')
  2107. def get_list_team_packages_command(self, team):
  2108. return 'list-team-packages {}'.format(team)
  2109. def get_private_error(self):
  2110. return (
  2111. "The team is private. "
  2112. "Only team members can see its packages.")
  2113. def test_get_public_team_packages(self):
  2114. """
  2115. Tests that a public team's packages can be obtained by any user.
  2116. """
  2117. self.set_input_lines([
  2118. self.get_list_team_packages_command(self.team.slug)
  2119. ])
  2120. self.control_process()
  2121. self.assert_list_in_response(
  2122. package.name
  2123. for package in self.team.packages.all().order_by('name'))
  2124. def test_get_private_team_packages_non_member(self):
  2125. """
  2126. Tests that getting a private team's packages is not possible by a
  2127. user that is not a member of the team.
  2128. """
  2129. self.team.public = False
  2130. self.team.save()
  2131. self.set_input_lines([
  2132. self.get_list_team_packages_command(self.team.slug)
  2133. ])
  2134. self.control_process()
  2135. self.assert_error_in_response(self.get_private_error())
  2136. def test_get_private_team_packages_member(self):
  2137. """
  2138. Tests that getting a private team's packages is possible by a
  2139. member of the team.
  2140. """
  2141. self.team.public = False
  2142. self.team.save()
  2143. # Add a member to the team
  2144. user_email = UserEmail.objects.create(email='member@domain.com')
  2145. self.team.add_members([user_email])
  2146. # Set the from field so that the member sends the control email
  2147. self.set_header('From', user_email.email)
  2148. self.set_input_lines([
  2149. self.get_list_team_packages_command(self.team.slug)
  2150. ])
  2151. self.control_process()
  2152. # The packages are output in the response
  2153. self.assert_list_in_response(
  2154. package.name
  2155. for package in self.team.packages.all().order_by('name'))
  2156. class WhichTeamsCommandTests(TeamCommandsMixin, EmailControlTest):
  2157. def setUp(self):
  2158. super(WhichTeamsCommandTests, self).setUp()
  2159. # Set up more teams
  2160. self.teams = [
  2161. self.team,
  2162. Team.objects.create_with_slug(name='Other team', owner=self.user),
  2163. Team.objects.create_with_slug(name='Some team', owner=self.user),
  2164. ]
  2165. self.user_email = UserEmail.objects.create(email='other@domain.com')
  2166. def get_which_teams_command(self, email=''):
  2167. return 'which-teams {}'.format(email)
  2168. def get_no_teams_warning(self, email):
  2169. return '{} is not a member of any team.'.format(email)
  2170. def test_user_member_of_teams(self):
  2171. """
  2172. Test that all the user's team memberships are output.
  2173. """
  2174. member_of = self.teams[:2]
  2175. not_member_of = self.teams[2:]
  2176. for team in member_of:
  2177. team.add_members([self.user_email])
  2178. self.set_input_lines([
  2179. self.get_which_teams_command(self.user_email.email)
  2180. ])
  2181. self.control_process()
  2182. # The teams that the user is subscribed too are output in the response
  2183. self.assert_list_in_response([
  2184. team.slug
  2185. for team in self.user_email.teams.all().order_by('name')
  2186. ])
  2187. # The teams the user is not subscribed to are not found in the response
  2188. for team in not_member_of:
  2189. self.assert_list_item_not_in_response(team.slug)
  2190. def test_user_not_member_of_any_team(self):
  2191. """
  2192. Tests the situation when the user is not a member of any teams.
  2193. """
  2194. self.set_input_lines([
  2195. self.get_which_teams_command(self.user_email.email)
  2196. ])
  2197. self.control_process()
  2198. self.assert_warning_in_response(
  2199. self.get_no_teams_warning(self.user_email.email))