Commit 771b6761 authored by Stephane Glondu's avatar Stephane Glondu

Merge branch 'swergas'

parents c2862989 98d4f739
Pipeline #61757 passed with stages
in 18 minutes and 36 seconds
Scenario 1: Simple vote, in "fully automated" mode
=================================
## Introduction and parameters
Protagonists to emulate: election administrator, `K` electors, an auditor.
Administrator uses only her browser.
Electors use their browser and read emails sent by the server.
`L` electors re-vote (with `L <= K`)
`M` electors ask administrator to re-generate their password, and vote with their re-generated password (with `M <= K`).
The auditor makes web requests, has a persistent state, and runs the commandline version of the Belenios tool.
Auditor makes web requests, has a persistent state, and runs the commandline version of the Belenios tool.
Authentication of administrator and electors are done using a login / password combination.
Examples of parameters sizes: `N` and `K` would be between 6 (quick test) and 1000 (load test)
## Note about verification
Verifications all along the process is done using command line tools `belenios-tool verify` and `belenios-tool verify-diff`:
- `belenios-tool verify` does a static verification (it verifies that vote data at current time is coherent)
- `belenios-tool verify-diff` does a dynamic verification (it verifies that current state of vote data is a possible/legitimate evolution of a vote data snapshot that has been saved during a previous step of the process)
## Detailed steps of the Test Scenario 1 process
- Starting setup of the election (action of the administrator)
- Creation of the draft election
- Alice has been given administrator rights on an online voting app called Belenios. She goes to check out its homepage and logs in.
- She clicks on the "Prepare a new election" link
- (She keeps default values on the form: Credential management is automatic (not manual), and Authentication method is Password, not CAS)
- She clicks on the "Proceed" button (this redirects to the "Preparation of election" page)
- She changes values of fields name and description of the election
- She clicks on the "Save changes button" (the one that is next to the election description field)
- Edition of election's questions
- She clicks on the "Edit questions" link, to write her own questions
- She arrives on the Questions page. She checks that the page title is correct
- She removes answer 3
- She clicks on the "Save changes" button (this redirects to the "Preparation of election" page)
- Setting election's voters
- She clicks on the "Edit voters" link, to then type the list of voters
- She types `N` e-mail addresses (the list of invited voters)
- She clicks on the "Add" button to submit changes
- She clicks on "Return to draft page" link
- She clicks on button "Generate on server"
- (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)
- 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.)
- She clicks on the "Proceed" link
- In "Authentication" section, she clicks on the "Generate and mail missing passwords" button
- She checks that the page contains expected confirmation text, instead of an error
- She clicks on the "Proceed" link
- Finalize creation of election
- In "Validate creation" section, she clicks on the "Create election" link
- (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
- (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 remembers the URL of the voting page, that is where the "Election home" link points to
- She checks that a "Close election" button is present (but she does not click on it)
- Log out and close the browser window
- Regenerating electors' lost passwords (for M electors) (action of the administrator)
- Alice has been contacted by some voters who say they lost their password. She wants to re-generate their passwords and have the platform send them by email. For this, she logs in as administrator.
- She remembers the list of voters who contacted her and said they lost their password.
- She selects the election that she wants to edit
- She arrives to the election administration page. For each voter of the M selected voters:
- She clicks on the "Regenerate and mail a password" link
- She types the e-mail address of the voter in the "Username" field
- She clicks on the "Submit" button
- She checks that the page shows a confirmation message similar to "A new password has been mailed to name@email.com"
- She clicks on the "Proceed" link (She arrives back to the election administration page)
- We do a sanity check that server has really tried to send these emails, and to these users only.
- She logs out and closes the browser window
- Verify election consistency (using command line tool `belenios_tool verify`)
- All voting electors cast their vote (`K` electors vote). We check vote data consistency for every batch of `X` votes (using `belenios_tool verify-diff` and a snapshot of election data copied in previous batch). For each batch of `X` voters:
- Create election data snapshot
- Current batch of electors vote. For each voter of this batch:
- Bob checks that he has received 2 emails containing an invitation to vote and all necessary credentials (election page URL, username, password). He goes to the election page URL.
- He clicks on the "Start" button
- A loading screen appears, then another screen appears. He clicks on the "Here" button
- A modal opens (it is an HTML modal created using Window.prompt()), with an input field. He types his credential.
- He fills his votes to each answer of the question (for each displayed checkbox, he decides to mark it or leave it empty)
- He clicks on the "Next" button
- He remembers the smart ballot tracker that is displayed
- He clicks on the "Continue" button
- He types his voter username and password, and submits the form
- He checks that the smart ballot tracker value that appears on screen is the same as the one he noted
- He clicks on the "I cast my vote" button
- He clicks on the "ballot box" link
- He checks that his smart ballot tracker appears in the list
- He closes the window (there is no log-out link, because user is not logged in: credentials are not remembered)
- He checks his mailbox to find a new email with confirmation of his vote, and verifies the value of the smart ballot tracker written in this email is the same as the one he noted.
- Verify election consistency (using `belenios_tool verify-diff`)
- Delete election data snapshot
- Verify election consistency (using command line tool `belenios_tool verify`)
- Create election data snapshot
- All electors who want to change their vote re-vote (`L` electors re-vote)
- We re-apply the same procedure as listed in previous step, except we use the set of `L` re-voters instead of the set of `K` voters
- Verify election consistency (using `belenios_tool verify-diff` and the snapshot created right before re-votes)
- Delete election data snapshot
- Verify election consistency (using command line tool `belenios_tool verify`)
- Administrator does tallying of the election
- Alice goes to the election page
- She clicks on the "Administer this election" link
- She logs in as administrator
- She clicks on the "Close election" button
- She clicks on the "Proceed to vote counting" button
- She checks consistency of the election result
- She checks that the number of accepted ballots is the same as the number of voters who voted
- For each available answer in the question, she checks that the total number of votes in favor of Answer X displayed in result page is the same as the sum of votes for Answer X in all votes of voters who voted that have been randomly generated in advance
- She checks that each ballot content corresponds to content that of this vote that has been randomly generated in advance
- Verify election consistency (using command line tool `belenios_tool verify`)
This diff is collapsed.
...@@ -85,6 +85,7 @@ let extractTemplate () = ...@@ -85,6 +85,7 @@ let extractTemplate () =
let rec createAnswer a = let rec createAnswer a =
let container = Dom_html.createDiv document in let container = Dom_html.createDiv document in
container##.className := Js.string "question_answer_item";
let t = document##createTextNode (Js.string "Answer: ") in let t = document##createTextNode (Js.string "Answer: ") in
let u = Dom_html.createInput document in let u = Dom_html.createInput document in
u##.className := Js.string "question_answer"; u##.className := Js.string "question_answer";
...@@ -100,6 +101,7 @@ let rec createAnswer a = ...@@ -100,6 +101,7 @@ let rec createAnswer a =
return () return ()
in in
btn##.onclick := handler f; btn##.onclick := handler f;
btn##.className := Js.string "btn_remove";
Dom.appendChild btn btn_text; Dom.appendChild btn btn_text;
Dom.appendChild container btn; Dom.appendChild container btn;
let insert_text = document##createTextNode (Js.string "Insert") in let insert_text = document##createTextNode (Js.string "Insert") in
...@@ -111,6 +113,7 @@ let rec createAnswer a = ...@@ -111,6 +113,7 @@ let rec createAnswer a =
return () return ()
in in
insert_btn##.onclick := handler f; insert_btn##.onclick := handler f;
insert_btn##.className := Js.string "btn_insert";
Dom.appendChild insert_btn insert_text; Dom.appendChild insert_btn insert_text;
Dom.appendChild container insert_btn; Dom.appendChild container insert_btn;
container container
......
...@@ -102,7 +102,7 @@ let base ~title ?login_box ~content ?(footer = div []) ?uuid () = ...@@ -102,7 +102,7 @@ let base ~title ?login_box ~content ?(footer = div []) ?uuid () =
| None -> | None ->
a ~service:admin [pcdata L.administer_elections] () a ~service:admin [pcdata L.administer_elections] ()
| Some uuid -> | Some uuid ->
a ~service:election_admin [pcdata L.administer_this_election] uuid a ~service:election_admin ~a:[a_id ("election_admin_" ^ (raw_string_of_uuid uuid))] [pcdata L.administer_this_election] uuid
in in
let login_box = match login_box with let login_box = match login_box with
| None -> | None ->
...@@ -182,12 +182,12 @@ let privacy_notice cont = ...@@ -182,12 +182,12 @@ let privacy_notice cont =
let format_election (uuid, name) = let format_election (uuid, name) =
li [ li [
a ~service:election_admin [pcdata name] uuid; a ~service:election_admin ~a:[a_id ("election_admin_" ^ (raw_string_of_uuid uuid))] [pcdata name] uuid;
] ]
let format_draft_election (uuid, name) = let format_draft_election (uuid, name) =
li [ li [
a ~service:election_draft [pcdata name] uuid; a ~service:election_draft ~a:[a_id ("election_draft_" ^ (raw_string_of_uuid uuid))] [pcdata name] uuid;
] ]
let admin ~elections () = let admin ~elections () =
...@@ -298,7 +298,7 @@ let generic_page ~title ?service message () = ...@@ -298,7 +298,7 @@ let generic_page ~title ?service message () =
| None -> pcdata "" | None -> pcdata ""
| Some service -> | Some service ->
div [ div [
a ~service [pcdata "Proceed"] (); a ~service ~a:[a_id "generic_proceed_link"] [pcdata "Proceed"] ();
] ]
in in
let content = [ let content = [
...@@ -2112,7 +2112,7 @@ let election_admin election metadata state get_tokens_decrypt () = ...@@ -2112,7 +2112,7 @@ let election_admin election metadata state get_tokens_decrypt () =
pcdata "" pcdata ""
else else
div [ div [
a ~service:election_regenpwd [pcdata "Regenerate and mail a password"] uuid; a ~a:[a_id "election_regenpwd"] ~service:election_regenpwd [pcdata "Regenerate and mail a password"] uuid;
] ]
in in
let content = [ let content = [
......
...@@ -160,7 +160,10 @@ class BeleniosTestElectionScenario2(BeleniosElectionTestBase): ...@@ -160,7 +160,10 @@ class BeleniosTestElectionScenario2(BeleniosElectionTestBase):
# She sends the remembered link to the credential authority by email (actually we don't need to send anything because we will act as the credential authority) # She sends the remembered link to the credential authority by email (actually we don't need to send anything because we will act as the credential authority)
# She closes the browser # Optionnaly, she logs out
# log_out(browser)
# She closes the browser window
browser.quit() browser.quit()
...@@ -185,7 +188,7 @@ class BeleniosTestElectionScenario2(BeleniosElectionTestBase): ...@@ -185,7 +188,7 @@ class BeleniosTestElectionScenario2(BeleniosElectionTestBase):
wait_a_bit() wait_a_bit()
# She clicks on the "private credentials" and "public credentials" links and downloads these files. Files are by default downloaded to /tmp using filenames creds.txt and public_creds.txt respectively, but we choose to name them using an unique identifier instead. # She clicks on the "private credentials" and "public credentials" links and downloads these files. Files are by default downloaded to /tmp using filenames `creds.txt` and `public_creds.txt` respectively, but we choose to name them using an unique identifier instead.
link_css_ids = ["creds", "public_creds"] link_css_ids = ["creds", "public_creds"]
file_labels = ["private credentials", "public credentials"] file_labels = ["private credentials", "public credentials"]
link_css_selectors = ["#" + el for el in link_css_ids] link_css_selectors = ["#" + el for el in link_css_ids]
...@@ -323,12 +326,15 @@ The election administrator.\ ...@@ -323,12 +326,15 @@ The election administrator.\
custom_content = content_format.format(link_for_trustee=self.links_for_trustees[idx]) custom_content = content_format.format(link_for_trustee=self.links_for_trustees[idx])
self.fake_sent_emails_manager.send_email(settings.ADMINISTRATOR_EMAIL_ADDRESS, trustee_email_address, subject, custom_content) self.fake_sent_emails_manager.send_email(settings.ADMINISTRATOR_EMAIL_ADDRESS, trustee_email_address, subject, custom_content)
# Optionnaly, she logs out
# log_out(browser)
# She closes the window # She closes the window
browser.quit() browser.quit()
def trustees_generate_election_private_keys(self): def trustees_generate_election_private_keys(self):
# Each trustee will do the following process # Each trustee (Tom and Taylor) will do the following process
for idx, trustee_email_address in enumerate(settings.TRUSTEES_EMAIL_ADDRESSES): for idx, trustee_email_address in enumerate(settings.TRUSTEES_EMAIL_ADDRESSES):
# Trustee opens link that has been sent to him by election administrator # Trustee opens link that has been sent to him by election administrator
link_for_this_trustee = self.links_for_trustees[idx] # TODO: Decide either not send trustee email at all or read trustee link from email content link_for_this_trustee = self.links_for_trustees[idx] # TODO: Decide either not send trustee email at all or read trustee link from email content
...@@ -348,7 +354,7 @@ The election administrator.\ ...@@ -348,7 +354,7 @@ The election administrator.\
generate_button_element = wait_for_element_exists_and_contains_expected_text(browser, generate_button_css_selector, generate_button_expected_label) generate_button_element = wait_for_element_exists_and_contains_expected_text(browser, generate_button_css_selector, generate_button_expected_label)
generate_button_element.click() generate_button_element.click()
# He clicks on the "private key" and "public key" links, to download the private key and the public key (files are respectively saved by default to private_key.json and public_key.json, but we decide to save them as a unique file name) # He clicks on the "private key" and "public key" links, to download the private key and the public key (files are respectively saved by default as `private_key.json` and `public_key.json`, but we decide to save them as a unique file name)
link_css_ids = ["private_key", "public_key"] link_css_ids = ["private_key", "public_key"]
link_expected_labels = ["private key", "public key"] link_expected_labels = ["private key", "public key"]
self.downloaded_files_paths_per_trustee[trustee_email_address] = dict() self.downloaded_files_paths_per_trustee[trustee_email_address] = dict()
...@@ -482,13 +488,17 @@ The election administrator.\ ...@@ -482,13 +488,17 @@ The election administrator.\
custom_content = content_format.format(link_for_trustee=self.closed_election_tally_links_for_trustees[idx]) custom_content = content_format.format(link_for_trustee=self.closed_election_tally_links_for_trustees[idx])
self.fake_sent_emails_manager.send_email(settings.ADMINISTRATOR_EMAIL_ADDRESS, trustee_email_address, subject, custom_content) self.fake_sent_emails_manager.send_email(settings.ADMINISTRATOR_EMAIL_ADDRESS, trustee_email_address, subject, custom_content)
# She logs out
log_out(browser)
# She closes the window # She closes the window
browser.quit() browser.quit()
def trustees_do_partial_decryption(self): def trustees_do_partial_decryption(self):
# Tom and Talyor are trustees and open the link that Alice, election administrator, has sent to them. # Each trustee (Tom and Taylor) will do the following process:
for idx, trustee_email_address in enumerate(settings.TRUSTEES_EMAIL_ADDRESSES): for idx, trustee_email_address in enumerate(settings.TRUSTEES_EMAIL_ADDRESSES):
# He opens the link that Alice (the election administrator) has sent to him
self.browser = initialize_browser_for_scenario_2() self.browser = initialize_browser_for_scenario_2()
browser = self.browser browser = self.browser
link_for_trustee = self.closed_election_tally_links_for_trustees[idx] link_for_trustee = self.closed_election_tally_links_for_trustees[idx]
...@@ -507,16 +517,23 @@ The election administrator.\ ...@@ -507,16 +517,23 @@ The election administrator.\
private_key_field_element = wait_for_element_exists(browser, private_key_field_css_selector) private_key_field_element = wait_for_element_exists(browser, private_key_field_css_selector)
assert private_key_field_element.get_attribute('value') == "" assert private_key_field_element.get_attribute('value') == ""
# One trustee uploads his private key file, the other copy-pastes its contents into the form field
private_key_file = self.downloaded_files_paths_per_trustee[trustee_email_address]["private key"]
if idx % 2 == 0:
# He clicks on the "Browse..." button and selects his private key file (initially downloaded as `private_key.json` by default) # He clicks on the "Browse..." button and selects his private key file (initially downloaded as `private_key.json` by default)
browse_button_css_selector = "input[id=private_key_file][type=file]" browse_button_css_selector = "input[id=private_key_file][type=file]"
browse_button_element = wait_for_element_exists(browser, browse_button_css_selector) browse_button_element = wait_for_element_exists(browser, browse_button_css_selector)
path_of_file_to_upload = self.downloaded_files_paths_per_trustee[trustee_email_address]["private key"] path_of_file_to_upload = private_key_file
browse_button_element.clear() browse_button_element.clear()
browse_button_element.send_keys(path_of_file_to_upload) browse_button_element.send_keys(path_of_file_to_upload)
# He waits until the "private key" input field (that has id "#private_key") becomes not empty anymore. This is because once the user has selected the file to upload, the Javascript code in the page detects that a file has been selected, reads it, and fills "private key" input field with file's contents. The computation triggered by click on the "Compute decryption factors" button will use the value of this field, not directly the uploaded file contents. # He waits until the "private key" input field (that has id "#private_key") becomes not empty anymore. This is because once the user has selected the file to upload, the Javascript code in the page detects that a file has been selected, reads it, and fills "private key" input field with file's contents. The computation triggered by click on the "Compute decryption factors" button will use the value of this field, not directly the uploaded file contents.
private_key_field_expected_non_empty_attribute = "value" private_key_field_expected_non_empty_attribute = "value"
wait_for_element_exists_and_has_non_empty_attribute(browser, private_key_field_css_selector, private_key_field_expected_non_empty_attribute) wait_for_element_exists_and_has_non_empty_attribute(browser, private_key_field_css_selector, private_key_field_expected_non_empty_attribute)
else:
with open(private_key_file) as myfile:
private_key_field_element.send_keys(myfile.read())
wait_a_bit()
# He clicks on the "Compute decryption factors" button # He clicks on the "Compute decryption factors" button
compute_button_css_selector = "button[id=compute]" compute_button_css_selector = "button[id=compute]"
......
...@@ -175,9 +175,8 @@ pris en compte. ...@@ -175,9 +175,8 @@ pris en compte.
# She clicks on the "Proceed" link # She clicks on the "Proceed" link
proceed_link_expected_label = "Proceed" proceed_link_css_selector = "#generic_proceed_link"
proceed_link_css_selector = "#main a" proceed_link_element = wait_for_element_exists(browser, proceed_link_css_selector, settings.EXPLICIT_WAIT_TIMEOUT)
proceed_link_element = wait_for_element_exists_and_contains_expected_text(browser, proceed_link_css_selector, proceed_link_expected_label, settings.EXPLICIT_WAIT_TIMEOUT)
proceed_link_element.click() proceed_link_element.click()
wait_a_bit() wait_a_bit()
...@@ -195,9 +194,8 @@ pris en compte. ...@@ -195,9 +194,8 @@ pris en compte.
wait_for_element_exists_and_contains_expected_text(browser, confirmation_sentence_css_selector, confirmation_sentence_expected_text, settings.EXPLICIT_WAIT_TIMEOUT) wait_for_element_exists_and_contains_expected_text(browser, confirmation_sentence_css_selector, confirmation_sentence_expected_text, settings.EXPLICIT_WAIT_TIMEOUT)
# She clicks on the "Proceed" link (this redirects to the "Preparation of election" page) # She clicks on the "Proceed" link (this redirects to the "Preparation of election" page)
proceed_link_expected_label = "Proceed" proceed_link_css_selector = "#generic_proceed_link"
proceed_link_css_selector = "#main a" proceed_link_element = wait_for_element_exists(browser, proceed_link_css_selector, settings.EXPLICIT_WAIT_TIMEOUT)
proceed_link_element = wait_for_element_exists_and_contains_expected_text(browser, proceed_link_css_selector, proceed_link_expected_label, settings.EXPLICIT_WAIT_TIMEOUT)
proceed_link_element.click() proceed_link_element.click()
wait_a_bit() wait_a_bit()
...@@ -221,7 +219,7 @@ pris en compte. ...@@ -221,7 +219,7 @@ pris en compte.
# She selects the election that she wants to edit # She selects the election that she wants to edit
browser = self.browser browser = self.browser
election_to_edit_css_selector = "#main li a[href^='election/admin?uuid=']" election_to_edit_css_selector = "#election_admin_" + str(self.election_id)
election_to_edit_elements = wait_for_elements_exist(browser, election_to_edit_css_selector, settings.EXPLICIT_WAIT_TIMEOUT) election_to_edit_elements = wait_for_elements_exist(browser, election_to_edit_css_selector, settings.EXPLICIT_WAIT_TIMEOUT)
assert len(election_to_edit_elements) > 0 assert len(election_to_edit_elements) > 0
election_to_edit_elements[0].click() election_to_edit_elements[0].click()
...@@ -231,7 +229,7 @@ pris en compte. ...@@ -231,7 +229,7 @@ pris en compte.
# She arrives to the election administration page. For each voter of the NUMBER_OF_REGENERATED_PASSWORD_VOTERS selected voters: # She arrives to the election administration page. For each voter of the NUMBER_OF_REGENERATED_PASSWORD_VOTERS selected voters:
for email_address in self.voters_email_addresses_who_have_lost_their_password: for email_address in self.voters_email_addresses_who_have_lost_their_password:
# She clicks on the "Regenerate and mail a password" link # She clicks on the "Regenerate and mail a password" link
regenerate_and_mail_a_password_link_css_selector = "#main a[href^='regenpwd?uuid=']" regenerate_and_mail_a_password_link_css_selector = "#election_regenpwd"
regenerate_and_mail_a_password_link_element = wait_for_element_exists(browser, regenerate_and_mail_a_password_link_css_selector, settings.EXPLICIT_WAIT_TIMEOUT) regenerate_and_mail_a_password_link_element = wait_for_element_exists(browser, regenerate_and_mail_a_password_link_css_selector, settings.EXPLICIT_WAIT_TIMEOUT)
regenerate_and_mail_a_password_link_element.click() regenerate_and_mail_a_password_link_element.click()
...@@ -257,9 +255,8 @@ pris en compte. ...@@ -257,9 +255,8 @@ pris en compte.
wait_for_element_exists_and_contains_expected_text(browser, confirmation_sentence_css_selector, confirmation_sentence_expected_text, settings.EXPLICIT_WAIT_TIMEOUT) wait_for_element_exists_and_contains_expected_text(browser, confirmation_sentence_css_selector, confirmation_sentence_expected_text, settings.EXPLICIT_WAIT_TIMEOUT)
# She clicks on the "Proceed" link # She clicks on the "Proceed" link
proceed_link_expected_label = "Proceed" proceed_link_css_selector = "#generic_proceed_link"
proceed_link_css_selector = "#main a" proceed_link_element = wait_for_element_exists(browser, proceed_link_css_selector, settings.EXPLICIT_WAIT_TIMEOUT)
proceed_link_element = wait_for_element_exists_and_contains_expected_text(browser, proceed_link_css_selector, proceed_link_expected_label, settings.EXPLICIT_WAIT_TIMEOUT)
proceed_link_element.click() proceed_link_element.click()
wait_a_bit() wait_a_bit()
......
...@@ -475,7 +475,7 @@ def administrator_edits_election_questions(browser): ...@@ -475,7 +475,7 @@ def administrator_edits_election_questions(browser):
# She removes answer 3 # She removes answer 3
question_to_remove = 3 question_to_remove = 3
remove_button_css_selector = ".question_answers > div:nth-child(" + str(question_to_remove) + ") button:nth-child(2)" remove_button_css_selector = ".question_answer_item:nth-child(" + str(question_to_remove) + ") .btn_remove"
remove_button_element = browser.find_element_by_css_selector(remove_button_css_selector) remove_button_element = browser.find_element_by_css_selector(remove_button_css_selector)
remove_button_element.click() remove_button_element.click()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment