Repository: freedomofpress/securedrop

Issue:
CI / test failures on develop branch
## Description

After merging #3619 to develop, CI is failing on the following test:
```
=================================== FAILURES ===================================
___________________ test_authorized_user_gets_single_source ____________________

journalist_app = <Flask 'journalist_app'>
test_source = {'codename': '<snip>', 'filesystem_id': 'I5DGWCSOFACCI6H5NDJ3QNZ7XYY...ITGCCFDWNJCWAZLA2QOGA=', 'source': <Source u'diachronic incineration'>, 'uuid': '1ee09d73-1164-424a-8f35-21fb37daf5bd'}
journalist_api_token = '<snip>'

    def test_authorized_user_gets_single_source(journalist_app, test_source,
                                                journalist_api_token):
        with journalist_app.test_client() as app:
            uuid = test_source['source'].uuid
            response = app.get(url_for('api.single_source', source_uuid=uuid),
>                              headers=get_api_headers(journalist_api_token))

tests/test_journalist_api.py:263: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
/usr/local/lib/python2.7/dist-packages/werkzeug/test.py:791: in get
    return self.open(*args, **kw)
/usr/local/lib/python2.7/dist-packages/flask/testing.py:127: in open
    follow_redirects=follow_redirects)
/usr/local/lib/python2.7/dist-packages/werkzeug/test.py:764: in open
    response = self.run_wsgi_app(environ, buffered=buffered)
/usr/local/lib/python2.7/dist-packages/werkzeug/test.py:677: in run_wsgi_app
    rv = run_wsgi_app(self.application, environ, buffered=buffered)
/usr/local/lib/python2.7/dist-packages/werkzeug/test.py:884: in run_wsgi_app
    app_rv = app(environ, start_response)
/usr/local/lib/python2.7/dist-packages/flask/app.py:1997: in __call__
    return self.wsgi_app(environ, start_response)
/usr/local/lib/python2.7/dist-packages/flask/app.py:1985: in wsgi_app
    response = self.handle_exception(e)
/usr/local/lib/python2.7/dist-packages/flask/app.py:1540: in handle_exception
    reraise(exc_type, exc_value, tb)
/usr/local/lib/python2.7/dist-packages/flask/app.py:1982: in wsgi_app
    response = self.full_dispatch_request()
/usr/local/lib/python2.7/dist-packages/flask/app.py:1614: in full_dispatch_request
    rv = self.handle_user_exception(e)
/usr/local/lib/python2.7/dist-packages/flask/app.py:1517: in handle_user_exception
    reraise(exc_type, exc_value, tb)
/usr/local/lib/python2.7/dist-packages/flask/app.py:1612: in full_dispatch_request
    rv = self.dispatch_request()
/usr/local/lib/python2.7/dist-packages/flask/app.py:1598: in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
journalist_app/api.py:43: in decorated_function
    return f(*args, **kwargs)
journalist_app/api.py:113: in single_source
    return jsonify(source.to_json()), 200
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <Source u'diachronic incineration'>

    def to_json(self):
        docs_msg_count = self.documents_messages_count()
    
        json_source = {
            'uuid': self.uuid,
            'url': url_for('api.single_source', source_uuid=self.uuid),
            'journalist_designation': self.journalist_designation,
            'is_flagged': self.flagged,
            'is_starred': True if self.star else False,
>           'last_updated': self.last_updated.isoformat() + 'Z',
            'interaction_count': self.interaction_count,
            'key': {
              'type': 'PGP',
              'public': self.public_key
            },
            'number_of_documents': docs_msg_count['documents'],
            'number_of_messages': docs_msg_count['messages'],
            'submissions_url': url_for('api.all_source_submissions',
                                       source_uuid=self.uuid),
            'add_star_url': url_for('api.add_star', source_uuid=self.uuid),
            'remove_star_url': url_for('api.remove_star',
                                       source_uuid=self.uuid),
            'reply_url': url_for('api.post_reply', source_uuid=self.uuid)
            }
E       AttributeError: 'NoneType' object has no attribute 'isoformat'

models.py:127: AttributeError
---------------------------- Captured stdout setup -----------------------------
seq  name             file                                                      
---  ---------------  ----------------------------------------------------------
0    main             /tmp/pytest-of-www-data/pytest-0/test_authorized_user_gets
============== 1 failed, 261 passed, 2 xfailed in 1058.03 seconds ==============
``` 

Re-running the build triggers the same failure.

## Steps to Reproduce
`make test`
## Expected Behavior

All tests
## Actual Behavior

`test_authorized_user_gets_single_source` fails

## Comments

I can reproduce the test failure in a local dev environment on the `develop` branch. 
The test does *not* fail locally on `journalist-api-0.9.0` branch.


Discussion:
Failure is likely due to the changes introduced in https://github.com/freedomofpress/securedrop/commit/ee8e1bf7efba0e9bbf4fc6532a2653854298a11a, which was merged in between the moment the Journalist API PR was rebased and the moment it was merged. 

Resolving patch:
diff --git a/securedrop/models.py b/securedrop/models.py
--- a/securedrop/models.py
+++ b/securedrop/models.py
@@ -118,13 +118,18 @@ def public_key(self):
     def to_json(self):
         docs_msg_count = self.documents_messages_count()
 
+        if self.last_updated:
+            last_updated = self.last_updated.isoformat() + 'Z'
+        else:
+            last_updated = datetime.datetime.utcnow().isoformat() + 'Z'
+
         json_source = {
             'uuid': self.uuid,
             'url': url_for('api.single_source', source_uuid=self.uuid),
             'journalist_designation': self.journalist_designation,
             'is_flagged': self.flagged,
             'is_starred': True if self.star else False,
-            'last_updated': self.last_updated.isoformat() + 'Z',
+            'last_updated': last_updated,
             'interaction_count': self.interaction_count,
             'key': {
               'type': 'PGP',


Test patch: