Repository: freedomofpress/securedrop

Issue:
Transient testing errors for source login
# Bug

## Description

```
____________________________________________________________ TestSourceApp.test_login_and_logout _____________________________________________________________

self = <tests.test_source.TestSourceApp testMethod=test_login_and_logout>

    def test_login_and_logout(self):
        resp = self.client.get('/login')
        self.assertEqual(resp.status_code, 200)
        self.assertIn("Enter Codename", resp.data)
    
        codename = self._new_codename()
        with self.client as c:
            resp = c.post('/login', data=dict(codename=codename),
                          follow_redirects=True)
            self.assertEqual(resp.status_code, 200)
>           self.assertIn("Submit Materials", resp.data)
E           AssertionError: 'Submit Materials' not found in '<!DOCTYPE html>\n<html lang="en" dir="ltr">\n  <head>\n    <meta charset="utf-8">\n    <meta name="viewport" content="width=device-width, initial-scale=1">\n    <title>SecureDrop | Protecting Journalists and Sources</title>\n\n    <link rel="stylesheet" href="/static/css/source.css">\n    <link rel="icon" type="image/png" href="/static/i/favicon.png">\n    \n  </head>\n  <body>\n    \n\n  \n\n\n    <div class="content">\n      \n      <div id="header">\n        <a href="/lookup">\n          <img src="/static/i/logo.png" class="logo small" alt="SecureDrop" width="250px">\n        </a>\n        \n        \n      </div>\n      \n\n      <div class="panel selected">\n      \n        <a href="/logout" class="sd-button btn pull-right" id="logout">LOG OUT</a>\n      \n        <hr class="no-line">\n\n        \n\n<h1>Enter Codename</h1>\n\n\n\n\n\n<form method="post" action="/login" autocomplete="off">\n<input name="csrf_token" type="hidden" value="IjBkMTg0N2ZmYzU4YzExZjgzOGJiYzJjZTFkYjdkOWZlNmI2OWFhN2Ui.DLa79A.Y7lYbUepqRHo6eF63pCNX1nViJI">\n\n<p class="center">\n  <input autocomplete="off" autofocus class="codename-box" id="login-with-existing-codename" name="codename" placeholder="Enter your codename" type="password" value="">\n  <br>\n  \n    <span class="form-validation-error">Invalid input.</span>\n  \n</p>\n\n<div class="pull-right">\n  <a href="/" class="sd-button btn secondary" id="cancel">CANCEL</a>\n  <button type="submit" class="sd-button btn primary" id="login">\n    <img class="icon off-hover" src="/static/i/font-awesome/fa-arrow-circle-o-right-white.png" width="18px" height="18px">\n    <img class="icon on-hover" src="/static/i/font-awesome/fa-arrow-circle-o-right-blue.png" width="18px" height="18px">\n    CONTINUE\n  </button>\n</div>\n\n</form>\n\n\n      </div>\n\n      \n      <footer>\n        Like all software, SecureDrop may contain security bugs. Use at your own risk. Powered by SecureDrop 0.4.3.\n      </footer>\n      \n    </div>\n  </body>\n</html>'

tests/test_source.py:151: AssertionError
```

## Steps to Reproduce

I can't reproduce this, but it pops up on occasion and has been fixed by some combination of `vagrant reload` or wiping the translations directory. Though I'm not sure if either of those actually affect it. :/


Discussion:
The relevant error in the resp.data string is
<pre>
<span class="form-validation-error">Invalid input.</span>
</pre>
which comes from source_app/forms.py and is rejecting **-** which shows in four words. The regexp
<pre>
(\s|[^\W_])+$
</pre>
could be simplified as:
<pre>
[\sA-Za-z0-9-]+$
</pre>
It would be useful to add a test verifying it validates all words in the wordlist to avoid similar regressions in the future.

Resolving patch:
diff --git a/securedrop/source_app/forms.py b/securedrop/source_app/forms.py
--- a/securedrop/source_app/forms.py
+++ b/securedrop/source_app/forms.py
@@ -13,7 +13,6 @@ class LoginForm(FlaskForm):
                message=gettext('Field must be between 1 and '
                                '{max_codename_len} characters long. '.format(
                                    max_codename_len=Source.MAX_CODENAME_LEN))),
-        # The regex here allows either whitespace (\s) or
-        # alphanumeric characters (\W) except underscore (_)
-        Regexp(r'(\s|[^\W_])+$', message=gettext('Invalid input.'))
+        # Make sure to allow dashes since some words in the wordlist have them
+        Regexp(r'[\sA-Za-z0-9-]+$', message=gettext('Invalid input.'))
     ])


Test patch:
diff --git a/securedrop/tests/test_source.py b/securedrop/tests/test_source.py
--- a/securedrop/tests/test_source.py
+++ b/securedrop/tests/test_source.py
@@ -44,6 +44,25 @@ def test_index(self):
         self.assertIn("Submit documents for the first time", response.data)
         self.assertIn("Already submitted something?", response.data)
 
+    def test_all_words_in_wordlist_validate(self):
+        """Verify that all words in the wordlist are allowed by the form
+        validation. Otherwise a source will have a codename and be unable to
+        return."""
+
+        wordlist_en = crypto_util._get_wordlist('en')
+
+        for word in wordlist_en:
+            with self.client as c:
+                resp = c.post('/login', data=dict(codename=word),
+                              follow_redirects=True)
+                self.assertEqual(resp.status_code, 200)
+                # If the word does not validate, then it will show
+                # 'Invalid input'. If it does validate, it should show that
+                # it isn't a recognized codename.
+                self.assertIn('Sorry, that is not a recognized codename.',
+                              resp.data)
+                self.assertNotIn('logged_in', session)
+
     def _find_codename(self, html):
         """Find a source codename (diceware passphrase) in HTML"""
         # Codenames may contain HTML escape characters, and the wordlist