test_scenario_1.py 18.7 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427
import unittest
import time
import string
import random
import os
import shutil
import subprocess
from selenium import webdriver
from selenium.webdriver.common.keys import Keys


SERVER_EXECUTABLE_FILE_PATH_RELATIVE_TO_GIT_REPOSITORY = "demo/run-server.sh"
SERVER_URL = "http://localhost:8001"
DATABASE_FOLDER_PATH_RELATIVE_TO_GIT_REPOSITORY = "_run/spool"
SENDMAIL_EXECUTABLE_FILE_ABSOLUTE_PATH = "/usr/lib/sendmail"
FAKE_SENDMAIL_EXECUTABLE_FILE_PATH_RELATIVE_TO_GIT_REPOSITORY = "tests/tools/sendmail_fake.sh"
SENT_EMAILS_TEXT_FILE_ABSOLUTE_PATH = "/tmp/sendmail_fake"
USE_HEADLESS_BROWSER = True
WAIT_TIME_BETWEEN_EACH_STEP = 0.05 # In seconds (float)


THIS_FILE_ABSOLUTE_PATH = os.path.abspath(__file__)
GIT_REPOSITORY_ABSOLUTE_PATH = os.path.dirname(os.path.dirname(THIS_FILE_ABSOLUTE_PATH))


def random_email_addresses_generator(size=20):
    res = []
    for x in range(size):
        res.append(random_email_address_generator())
    return res


def random_email_address_generator():
    return random_generator() + "@mailinator.com"


def random_generator(size=20, chars=string.ascii_uppercase + string.digits):
    return ''.join(random.choice(chars) for x in range(size))


def find_in_sent_emails(text):
    with open(SENT_EMAILS_TEXT_FILE_ABSOLUTE_PATH) as fl:
        return text in fl.read()


def remove_database_folder():
    shutil.rmtree(os.path.join(GIT_REPOSITORY_ABSOLUTE_PATH, DATABASE_FOLDER_PATH_RELATIVE_TO_GIT_REPOSITORY), ignore_errors=True)


def wait_a_bit():
    time.sleep(WAIT_TIME_BETWEEN_EACH_STEP)

def install_fake_sendmail():
    subprocess.run(["sudo", "mv", SENDMAIL_EXECUTABLE_FILE_ABSOLUTE_PATH, SENDMAIL_EXECUTABLE_FILE_ABSOLUTE_PATH + "_original"])
    fake_sendmail_absolute_path = os.path.join(GIT_REPOSITORY_ABSOLUTE_PATH, FAKE_SENDMAIL_EXECUTABLE_FILE_PATH_RELATIVE_TO_GIT_REPOSITORY)
    subprocess.run(["sudo", "ln", "--symbolic", fake_sendmail_absolute_path, SENDMAIL_EXECUTABLE_FILE_ABSOLUTE_PATH])

def uninstall_fake_sendmail():
    subprocess.run(["sudo", "rm", SENDMAIL_EXECUTABLE_FILE_ABSOLUTE_PATH])
    subprocess.run(["sudo", "mv", SENDMAIL_EXECUTABLE_FILE_ABSOLUTE_PATH + "_original", SENDMAIL_EXECUTABLE_FILE_ABSOLUTE_PATH])
    subprocess.run(["sudo", "rm", "-f", SENT_EMAILS_TEXT_FILE_ABSOLUTE_PATH])


class BeleniosTestElectionScenario1(unittest.TestCase):
  def setUp(self):
    install_fake_sendmail()

    remove_database_folder()

    server_path = os.path.join(GIT_REPOSITORY_ABSOLUTE_PATH, SERVER_EXECUTABLE_FILE_PATH_RELATIVE_TO_GIT_REPOSITORY)
    self.server = subprocess.Popen([server_path], stdout=subprocess.PIPE)

    if USE_HEADLESS_BROWSER:
        from selenium.webdriver.firefox.options import Options
        options = Options()
        options.add_argument("--headless")
        options.log.level = "trace"
        self.driver = webdriver.Firefox(options=options)
    else:
        self.driver = webdriver.Firefox()


  def tearDown(self):
    self.driver.quit()

    self.server.kill()

    remove_database_folder()

    uninstall_fake_sendmail()


  def administrator_creates_election(self):
    number_of_voters = 20 # This is K in description of Scenario 1. K is between 6 (quick test) and 1000 (load testing). FIXME: Is this the same as N?
    number_of_revoting_voters = 10 # This is L in description of Scenario 1. L <= K
    number_of_regenerated_password_voters = 5 # This is M in description of Scenario 1. M <= K



    # # Setting up a new election (action of the administrator)

    # Edith has been given administrator rights on an online voting app called Belenios. She goes
    # to check out its homepage
    browser = self.driver
    browser.get(SERVER_URL)

    # She notices the page title mentions an election
    assert 'Election Server' in browser.title, "Browser title was: " + browser.title

    # She clicks on the "Accept" button to accept the personal data policy
    accept_button_css_selector = '#main form input[type=submit]'
    button_elements = browser.find_elements_by_css_selector(accept_button_css_selector)
    assert len(button_elements) is 1
    button_elements
    button_elements[0].click()

    wait_a_bit()

    # She clicks on "local" to go to the login page
    login_link_id = "login_local"
    login_element = browser.find_element_by_id(login_link_id)
    login_element.click()

    # She enters her identifier and password and submits the form to log in
    login_form_username_value = "user1"
    login_form_password_value = "phiexoey" # This is the 4th column of file demo/password_db.csv

    login_form_username_css_selector = '#main form input[name=username]'
    login_form_password_css_selector = '#main form input[name=password]'

    login_form_username_element = browser.find_element_by_css_selector(login_form_username_css_selector)
    login_form_password_element = browser.find_element_by_css_selector(login_form_password_css_selector)

    login_form_username_element.send_keys(login_form_username_value)
    login_form_password_element.send_keys(login_form_password_value)
    login_form_password_element.submit()

    wait_a_bit()

    # She verifies that she arrived on the administration page (instead of any login error page)
    page_visible_title_element = browser.find_element_by_css_selector("#header h1")
    page_title_expected_content = "Administration"
    page_title_real_content = page_visible_title_element.get_attribute('innerText')
    assert page_title_expected_content in page_title_real_content, "Page title was: " + page_title_real_content

    wait_a_bit()

    # She clicks on the "Prepare a new election" link
    create_election_link_text = "Prepare a new election"
    links_css_selector = "#main a"
    links_elements = browser.find_elements_by_css_selector(links_css_selector)
    assert len(links_elements)
    create_election_link_element = links_elements[0]
    link_real_content = create_election_link_element.get_attribute('innerText')
    assert create_election_link_text in link_real_content, "Link text was: " + link_real_content
    create_election_link_element.click()

    wait_a_bit()

    # She clicks on the "Proceed" button (this redirects to the "Preparation of election" page)
    proceed_button_css_selector = "#main form input[type=submit]"
    proceed_button_element = browser.find_element_by_css_selector(proceed_button_css_selector)
    proceed_button_element.click()

    wait_a_bit()

    # She changes values of fields name and description of the election
    election_name_field_css_selector = "#main form input[name=__co_eliom_name]"
    election_name_field_element = browser.find_element_by_css_selector(election_name_field_css_selector)
    election_name_field_value = "My test election for Scenario 1"
    election_name_field_element.clear()
    election_name_field_element.send_keys(election_name_field_value)

    wait_a_bit()

    election_description_field_css_selector = "#main form textarea[name=__co_eliom_description]"
    election_description_field_element = browser.find_element_by_css_selector(election_description_field_css_selector)
    election_description_field_value = "This is the description of my test election for Scenario 1"
    election_description_field_element.clear()
    election_description_field_element.send_keys(election_description_field_value)

    wait_a_bit()

    # She clicks on the "Save changes button" (the one that is next to the election description field)
    save_changes_button_css_selector = "#main > div:nth-child(1) form input[type=submit]" # Warning: form:nth-child(1) selects another form
    save_changes_button_element = browser.find_element_by_css_selector(save_changes_button_css_selector)
    save_changes_button_element.click()

    wait_a_bit()

    # She clicks on the "Edit questions" link, to write her own questions
    edit_questions_link_css_selector = "#edit_questions"
    edit_questions_link_element = browser.find_element_by_css_selector(edit_questions_link_css_selector)
    edit_questions_link_element.click()

    wait_a_bit()

    # She arrives on the Questions page. She checks that the page title is correct
    page_visible_title_element = browser.find_element_by_css_selector("#header h1")
    page_title_expected_content = "Questions for"
    page_title_real_content = page_visible_title_element.get_attribute('innerText')
    assert page_title_expected_content in page_title_real_content, "Page title was: " + page_title_real_content

    # She removes answer 3
    question_to_remove = 3
    remove_button_css_selector = ".question_answers > div:nth-child("+str(question_to_remove)+") button:nth-child(2)"
    remove_button_element = browser.find_element_by_css_selector(remove_button_css_selector)
    remove_button_element.click()

    wait_a_bit()

    # She clicks on the "Save changes" button (this redirects to the "Preparation of election" page)
    save_changes_button_expected_label = "Save changes"
    button_elements = browser.find_elements_by_css_selector("button")
    assert len(button_elements)
    save_changes_button_element = button_elements[-1]
    save_changes_button_real_content = save_changes_button_element.get_attribute('innerText')
    assert save_changes_button_expected_label in save_changes_button_real_content, "Save button label was: " + save_changes_button_real_content
    save_changes_button_element.click()

    wait_a_bit()

    # She clicks on the "Edit voters" link, to then type the list of voters
    edit_voters_link_css_selector = "#edit_voters"
    edit_voters_link_element = browser.find_element_by_css_selector(edit_voters_link_css_selector)
    edit_voters_link_element.click()

    wait_a_bit()

    # She types N e-mail addresses (the list of voters)
    email_addresses = random_email_addresses_generator(number_of_voters)
    voters_list_field_css_selector = "#main form textarea"
    voters_list_field_element = browser.find_element_by_css_selector(voters_list_field_css_selector)
    voters_list_field_element.clear()
    is_first = True
    for email_address in email_addresses:
        if is_first:
            is_first = False
        else:
            voters_list_field_element.send_keys(Keys.ENTER)
        voters_list_field_element.send_keys(email_address)

    wait_a_bit()

    # She clicks on the "Add" button to submit changes
    add_button_css_selector = "#main form input[type=submit]"
    add_button_element = browser.find_element_by_css_selector(add_button_css_selector)
    add_button_element.click()

    wait_a_bit()

    # She clicks on "Return to draft page" link
    return_link_label = "Return to draft page"
    browser.find_element_by_partial_link_text(return_link_label).click()

    wait_a_bit()

    # She clicks on button "Generate on server"
    generate_on_server_button_label = "Generate on server"
    generate_on_server_button_css_selector = "#main form input[value='" + generate_on_server_button_label + "']"
    generate_on_server_button_element = browser.find_element_by_css_selector(generate_on_server_button_css_selector)
    generate_on_server_button_element.click()

    wait_a_bit()

    # (Server sends emails to voters.) She checks that server does not show any error that would happen when trying to send these emails (this can happen if sendmail is not configured)
    confirmation_sentence_expected_text = "Credentials have been generated and mailed!"
    confirmation_sentence_css_selector = "#main p"
    confirmation_sentence_element = browser.find_element_by_css_selector(confirmation_sentence_css_selector)
    confirmation_sentence_real_content = confirmation_sentence_element.get_attribute('innerText')
    assert confirmation_sentence_expected_text in confirmation_sentence_real_content, "Confirmation sentence was: " + confirmation_sentence_real_content

    # Now we do a sanity check that server has really tried to send emails. For this, we look for email addresses in the temporary file where our fake sendmail executable redirects its inputs to.

    """
    An email sent by Belenios (using sendmail or using the fake sendmail) to a voter looks like this:

    Content-type: text/plain; charset="UTF-8"
Content-transfer-encoding: quoted-printable
From: Belenios public server <noreply@example.org>
To: "820E7G83JBY0F4Z3DY2Y@mailinator.com"
 <820E7G83JBY0F4Z3DY2Y@mailinator.com>
Subject: Your credential for election My test election for Scenario 1
MIME-Version: 1.0
X-Mailer: OcamlNet (ocamlnet.sourceforge.net)
Date: Wed, 31 Oct 2018 15:22:27 +0100

You are listed as a voter for the election

  My test election for Scenario 1

You will find below your credential.  To cast a vote, you will also
need a password, sent in a separate email.  Be careful, passwords and
credentials look similar but play different roles.  You will be asked
to enter your credential before entering the voting booth.  Login and
passwords are required once your ballot is ready to be cast.

Credential: yQVDQaKSAQVjdZq
Page of the election: http://localhost:8001/elections/AFFNDEPnpy21bw/

Note that you are allowed to vote several times.  Only the last vote
counts.

----------

Vous =C3=AAtes enregistr=C3=A9(e) en tant qu=27=C3=A9lecteur(trice) pour=20=
l=27=C3=A9lection

  My test election for Scenario 1

Veuillez trouver ci-dessous votre code de vote. Pour soumettre un
bulletin, vous aurez =C3=A9galement besoin d=27un mot de passe, envoy=C3=A9=
 dans
un e-mail s=C3=A9par=C3=A9. Soyez attentif(ve), le mot de passe et le cod=
e de
vote se ressemblent mais jouent des r=C3=B4les diff=C3=A9rents. Le syst=C3=
=A8me vous
demandera votre code de vote d=C3=A8s l=27entr=C3=A9e dans l=27isoloir=20=
virtuel. Le
nom d=27utilisateur et le mot de passe sont n=C3=A9cessaires lorsque votr=
e
bulletin est pr=C3=AAt =C3=A0 =C3=AAtre soumis.

Code de vote=C2=A0: yQVDQaKSAQVjdZq
Page de l=27=C3=A9lection=C2=A0: http://localhost:8001/elections/AFFNDEPn=
py21bw/

Notez que vous pouvez voter plusieurs fois. Seul le dernier vote est
pris en compte.

--=20
    """

    email_address_to_look_for = email_addresses[0]
    text_to_look_for = 'To: "' + email_address_to_look_for + '"'
    email_address_found = find_in_sent_emails(text_to_look_for)
    assert email_address_found, "Text '" + email_address_to_look_for + "'' not found in fake sendmail log file"


    # She clicks on the "Proceed" link
    proceed_link_expected_label = "Proceed"
    proceed_link_css_selector = "#main a"
    proceed_link_element = browser.find_element_by_css_selector(proceed_link_css_selector)
    proceed_link_element_real_label = proceed_link_element.get_attribute('innerText')
    assert proceed_link_expected_label in proceed_link_element_real_label, "Proceed link label was: " + proceed_link_element_real_label
    proceed_link_element.click()

    wait_a_bit()

    # In "Authentication" section, she clicks on the "Generate and mail missing passwords" button
    generate_and_mail_missing_passwords_button_label = "Generate and mail missing passwords"
    generate_and_mail_missing_passwords_button_css_selector = "#main form input[value='" + generate_and_mail_missing_passwords_button_label + "']"
    generate_and_mail_missing_passwords_button_element = browser.find_element_by_css_selector(generate_and_mail_missing_passwords_button_css_selector)
    generate_and_mail_missing_passwords_button_element.click()

    wait_a_bit()

    # She checks that the page contains expected confirmation text, instead of an error (TODO: explain in which case an error can happen, and check that it does not show)
    confirmation_sentence_expected_text = "Passwords have been generated and mailed!"
    confirmation_sentence_css_selector = "#main p"
    confirmation_sentence_element = browser.find_element_by_css_selector(confirmation_sentence_css_selector)
    confirmation_sentence_real_content = confirmation_sentence_element.get_attribute('innerText')
    assert confirmation_sentence_expected_text in confirmation_sentence_real_content, "Confirmation sentence was: " + confirmation_sentence_real_content

    # She clicks on the "Proceed" link
    proceed_link_expected_label = "Proceed"
    proceed_link_css_selector = "#main a"
    proceed_link_element = browser.find_element_by_css_selector(proceed_link_css_selector)
    proceed_link_element_real_label = proceed_link_element.get_attribute('innerText')
    assert proceed_link_expected_label in proceed_link_element_real_label, "Proceed link label was: " + proceed_link_element_real_label
    proceed_link_element.click()

    wait_a_bit()

    # In "Validate creation" section, she clicks on the "Create election" link
    create_election_link_label = "Create election"
    browser.find_element_by_partial_link_text(create_election_link_label).click()

    wait_a_bit()

    # She arrives on the "Checklist" page, that lists all main parameters of the election for review, and that flags incoherent or misconfigured parameters. For example, in this test scenario, it displays 2 warnings: "Warning: No trustees were set. This means that the server will manage the election key by itself.", and "Warning: No contact was set!"

    # In the "Validate creation" section, she clicks on the "Create election" button
    create_election_button_label = "Create election"
    create_election_button_css_selector = "#main form input[value='" + create_election_button_label + "']"
    create_election_button_element = browser.find_element_by_css_selector(create_election_button_css_selector)
    create_election_button_element.click()

    wait_a_bit()

    # She arrives back on the "My test election for Scenario 1 — Administration" page. Its contents have changed. There is now a text saying "The election is open. Voters can vote.", and there are now buttons "Close election", "Archive election", "Delete election"

    # She checks that a "Close election" button is present (but she does not click on it)
    close_election_button_label = "Close election"
    close_election_button_css_selector = "#main form input[value='" + close_election_button_label + "']"
    close_election_button_elements = browser.find_elements_by_css_selector(close_election_button_css_selector)
    assert len(close_election_button_elements)

    # In the header of the page, she clicks on the "Log out" link
    logout_link_css_id = "logout"
    logout_element = browser.find_element_by_id(logout_link_css_id)
    logout_element.click()

    # She arrives back on the logged out home page. She checks that a login link is present
    login_link_id = "login_local"
    login_element = browser.find_element_by_id(login_link_id)


  def administrator_regenerates_passwords_for_some_voters(self):
    # TODO: implement this step
    pass


  def test_scenario_1_simple_vote(self):
    print("### Starting step: administrator_creates_election")
    self.administrator_creates_election()
    print("### Step complete: administrator_creates_election")

    print("### Starting step: administrator_regenerates_passwords_for_some_voters")
    self.administrator_regenerates_passwords_for_some_voters()
    print("### Step complete: administrator_regenerates_passwords_for_some_voters")

    # TODO: implement next steps


if __name__ == "__main__":
  unittest.main()