<commit_before># doctest: +ELLIPSIS
# doctest: +NORMALIZE_WHITESPACE

User Registration and Login
===========================

Prerequisites
-------------

Some imports to work with rest api calls::

    >>> from pprint import pprint

Start Adhocracy testapp::

    >>> from webtest import TestApp
    >>> app = getfixture('app_sample')
    >>> websocket = getfixture('websocket')
    >>> testapp = TestApp(app)
    >>> rest_url = 'http://localhost'


Test that the relevant resources and sheets exist:

    >>> resp_data = testapp.get("/meta_api/").json
    >>> 'adhocracy.sheets.versions.IVersions' in resp_data['sheets']
    True
    >>> 'adhocracy.sheets.user.IUserBasic' in resp_data['sheets']
    True
    >>> 'adhocracy.sheets.user.IPasswordAuthentication' in resp_data['sheets']
    True

User Creation (Registration)
----------------------------

A new user is registered by creating a user object under the
``/principals/users`` pool. On success, the response contains the
path of the new user::

    >>> prop = {'content_type': 'adhocracy.resources.principal.IUser',
    ...         'data': {
    ...              'adhocracy.sheets.user.IUserBasic': {
    ...                  'name': '<NAME>',
    ...                  'email': '<EMAIL>'},
    ...              'adhocracy.sheets.user.IPasswordAuthentication': {
    ...                  'password': '<PASSWORD>'}}}
    >>> resp_data = testapp.post_json(rest_url + "/principals/users", prop).json
    >>> resp_data["content_type"]
    'adhocracy.resources.principal.IUser'
    >>> user_path = resp_data["path"]
    >>> user_path
    '.../principals/users/00...

Like every resource the user has a metadata sheet with creation information::
    >>> resp_data = testapp.get(user_path).json
    >>> resp_metadata = resp_data['data']['adhocracy.sheets.metadata.IMetadata']

Even though he is not logged in yet, the creator points to his user resource::

    >>> resp_metadata['creator']
    '.../principals/users/00...

The "name" field in the "IUserBasic" schema is a non-empty string that
can contain any characters except '@' (to make user names distinguishable
from email addresses). The username must not contain any whitespace except
single spaces, preceded and followed by non-whitespace (no whitespace at
begin or end, multiple subsequent spaces are forbidden,
tabs and newlines are forbidden).

The "email" field must be a valid email address.

Creating a new user will not automatically log them in. First, the backend
will send a registration message to the specified email address. Once the user
has clicked on the activation link in the message, the user account is ready
to be used (see "Account Activation" below).

On failure, the backend responds with status code 400 and an error message.
E.g. when we try to register a user with an empty password::

    >>> prop = {'content_type': 'adhocracy.resources.principal.IUser',
    ...         'data': {
    ...              'adhocracy.sheets.user.IUserBasic': {
    ...                  'name': 'Other User',
    ...                  'email': '<EMAIL>'},
    ...              'adhocracy.sheets.user.IPasswordAuthentication': {
    ...                  'password': ''}}}
    >>> resp_data = testapp.post_json(rest_url + "/principals/users", prop,
    ...                               status=400).json
    >>> pprint(resp_data)
    {'errors': [{'description': 'Required',
                 'location': 'body',
                 'name': 'data.adhocracy.sheets.user.IPasswordAuthentication.password'}],
     'status': 'error'}

<errors> is a list of errors. The above error indicates that a required
field (the password field) is missing or empty. The following other error
conditions can occur:

  * username does already exist
  * email does already exist
  * email is invalid (doesn't look like an email address)
  * couldn't send a registration mail to the email address
  * password is too short (less than 6 chars)
  * password is too long (more than 100 chars)
  * internal error: something went wrong in the backend

For example, if we try to register a user whose email address is already
registered:

    >>> prop = {'content_type': 'adhocracy.resources.principal.IUser',
    ...         'data': {
    ...              'adhocracy.sheets.user.IUserBasic': {
    ...                  'name': 'New user with old password',
    ...                  'email': '<EMAIL>'},
    ...              'adhocracy.sheets.user.IPasswordAuthentication': {
    ...                  'password': '<PASSWORD>'}}}
    >>> resp_data = testapp.post_json(rest_url + "/principals/users", prop,
    ...                               status=400).json
    >>> pprint(resp_data)
    {'errors': [{'description': 'The user login email is not unique',
                 'location': 'body',
                 'name': 'data.adhocracy.sheets.user.IUserBasic.email'}],
     'status': 'error'}

*Note:* in the future, the registration request may contain additional
personal data for the user. This data will probably be collected in one or
several additional sheets, e.g.::

    'data': {
        'adhocracy.sheets.user.IUserBasic': {
            'name': '<NAME>',
            'email': '<EMAIL>'},
        'adhocracy.sheets.user.IPasswordAuthentication': {
            'password': '...'},
        'adhocracy.sheets.user.IUserDetails': {
          'forename': '...',
          'surname': '...',
          'day_of_birth': '...',
          'street': '...',
          'town': '...',
          'postcode': '...',
          'gender': '...'
        }
     }


Account Activation
------------------

On user registration, the backend sends a mail with an activation link to the
specified email address and sends a 2xx HTTP response to the frontend,
so the frontend can tell the user to expect an email.
The user has to click on the activation link to
activate their account. The *path* component of all such links starts with
``/activate/``. Once the frontend receives a click on such a link, it must post
a JSON request containing the path to the ``activate_account`` endpoint of the
backend::

    >> prop = {'path': '/activate/blahblah'}
    >> resp_data = testapp.post_json('/activate_account', prop).json
    >> pprint(resp_data)
    {'details': 'unknown_path',
     'status': 'error'}

FIXME Make the above a real test once that endpoint exists.

The backend responds with either 'status': 'success' and 'user_path' and
'user_token', just like after a successful login request (see next section).
This means that the user account has been activated and the user is now
logged in.

Or it responds with 'status': 'error' and a 'details' field that contains
one of the following values:

* 'unknown_path' if the activation path is unknown to the backend
* 'expired_path' if the activation path has expired since it was generated more
  than 7 days ago. In this case, user activation is no longer possible for
  security reasons and the user has to call support or register again,
  using a different email. (More user-friendly options are planned but haven't
  been implemented yet!)

Note that activation links are deleted from the backend once the account has
been successfully activated. (In the future, they may also be deleted if the
user didn't click on them within 7 days.) 'unknown_path' can therefore mean
two things: either the activation link was was never valid (the user
mistyped it or just tried to guess one), or it used to be valid but has been
deleted. There is no way to distinguish between these cases.

FIXME How to test this without actually sending an email?

User Login
----------

To log-in an existing and activated user via password, the frontend posts a<commit_msg>Clarify that response code reflects error condition.<commit_after># doctest: +ELLIPSIS
# doctest: +NORMALIZE_WHITESPACE

User Registration and Login
===========================

Prerequisites
-------------

Some imports to work with rest api calls::

    >>> from pprint import pprint

Start Adhocracy testapp::

    >>> from webtest import TestApp
    >>> app = getfixture('app_sample')
    >>> websocket = getfixture('websocket')
    >>> testapp = TestApp(app)
    >>> rest_url = 'http://localhost'


Test that the relevant resources and sheets exist:

    >>> resp_data = testapp.get("/meta_api/").json
    >>> 'adhocracy.sheets.versions.IVersions' in resp_data['sheets']
    True
    >>> 'adhocracy.sheets.user.IUserBasic' in resp_data['sheets']
    True
    >>> 'adhocracy.sheets.user.IPasswordAuthentication' in resp_data['sheets']
    True

User Creation (Registration)
----------------------------

A new user is registered by creating a user object under the
``/principals/users`` pool. On success, the response contains the
path of the new user::

    >>> prop = {'content_type': 'adhocracy.resources.principal.IUser',
    ...         'data': {
    ...              'adhocracy.sheets.user.IUserBasic': {
    ...                  'name': '<NAME>',
    ...                  'email': '<EMAIL>'},
    ...              'adhocracy.sheets.user.IPasswordAuthentication': {
    ...                  'password': '<PASSWORD>'}}}
    >>> resp_data = testapp.post_json(rest_url + "/principals/users", prop).json
    >>> resp_data["content_type"]
    'adhocracy.resources.principal.IUser'
    >>> user_path = resp_data["path"]
    >>> user_path
    '.../principals/users/00...

Like every resource the user has a metadata sheet with creation information::
    >>> resp_data = testapp.get(user_path).json
    >>> resp_metadata = resp_data['data']['adhocracy.sheets.metadata.IMetadata']

Even though he is not logged in yet, the creator points to his user resource::

    >>> resp_metadata['creator']
    '.../principals/users/00...

The "name" field in the "IUserBasic" schema is a non-empty string that
can contain any characters except '@' (to make user names distinguishable
from email addresses). The username must not contain any whitespace except
single spaces, preceded and followed by non-whitespace (no whitespace at
begin or end, multiple subsequent spaces are forbidden,
tabs and newlines are forbidden).

The "email" field must be a valid email address.

Creating a new user will not automatically log them in. First, the backend
will send a registration message to the specified email address. Once the user
has clicked on the activation link in the message, the user account is ready
to be used (see "Account Activation" below).

On failure, the backend responds with status code 400 and an error message.
E.g. when we try to register a user with an empty password::

    >>> prop = {'content_type': 'adhocracy.resources.principal.IUser',
    ...         'data': {
    ...              'adhocracy.sheets.user.IUserBasic': {
    ...                  'name': 'Other User',
    ...                  'email': '<EMAIL>'},
    ...              'adhocracy.sheets.user.IPasswordAuthentication': {
    ...                  'password': ''}}}
    >>> resp_data = testapp.post_json(rest_url + "/principals/users", prop,
    ...                               status=400).json
    >>> pprint(resp_data)
    {'errors': [{'description': 'Required',
                 'location': 'body',
                 'name': 'data.adhocracy.sheets.user.IPasswordAuthentication.password'}],
     'status': 'error'}

<errors> is a list of errors. The above error indicates that a required
field (the password field) is missing or empty. The following other error
conditions can occur:

  * username does already exist
  * email does already exist
  * email is invalid (doesn't look like an email address)
  * couldn't send a registration mail to the email address
  * password is too short (less than 6 chars)
  * password is too long (more than 100 chars)
  * internal error: something went wrong in the backend

For example, if we try to register a user whose email address is already
registered:

    >>> prop = {'content_type': 'adhocracy.resources.principal.IUser',
    ...         'data': {
    ...              'adhocracy.sheets.user.IUserBasic': {
    ...                  'name': 'New user with old password',
    ...                  'email': '<EMAIL>'},
    ...              'adhocracy.sheets.user.IPasswordAuthentication': {
    ...                  'password': '<PASSWORD>'}}}
    >>> resp_data = testapp.post_json(rest_url + "/principals/users", prop,
    ...                               status=400).json
    >>> pprint(resp_data)
    {'errors': [{'description': 'The user login email is not unique',
                 'location': 'body',
                 'name': 'data.adhocracy.sheets.user.IUserBasic.email'}],
     'status': 'error'}

*Note:* in the future, the registration request may contain additional
personal data for the user. This data will probably be collected in one or
several additional sheets, e.g.::

    'data': {
        'adhocracy.sheets.user.IUserBasic': {
            'name': '<NAME>',
            'email': '<EMAIL>'},
        'adhocracy.sheets.user.IPasswordAuthentication': {
            'password': '...'},
        'adhocracy.sheets.user.IUserDetails': {
          'forename': '...',
          'surname': '...',
          'day_of_birth': '...',
          'street': '...',
          'town': '...',
          'postcode': '...',
          'gender': '...'
        }
     }


Account Activation
------------------

On user registration, the backend sends a mail with an activation link to the
specified email address and sends a 2xx HTTP response to the frontend,
so the frontend can tell the user to expect an email.
The user has to click on the activation link to
activate their account. The *path* component of all such links starts with
``/activate/``. Once the frontend receives a click on such a link, it must post
a JSON request containing the path to the ``activate_account`` endpoint of the
backend::

    >> prop = {'path': '/activate/blahblah'}
    >> resp_data = testapp.post_json('/activate_account', prop).json
    >> pprint(resp_data)
    {'details': 'unknown_path',
     'status': 'error'}

FIXME Make the above a real test once that endpoint exists.

The backend responds with either 2xx response code and
'status': 'success' and 'user_path' and
'user_token', just like after a successful login request (see next section).
This means that the user account has been activated and the user is now
logged in.

Or it responds with 4xx response code and
'status': 'error' and a 'details' field that contains
one of the following values:

* 'unknown_path' if the activation path is unknown to the backend
* 'expired_path' if the activation path has expired since it was generated more
  than 7 days ago. In this case, user activation is no longer possible for
  security reasons and the user has to call support or register again,
  using a different email. (More user-friendly options are planned but haven't
  been implemented yet!)

Note that activation links are deleted from the backend once the account has
been successfully activated. (In the future, they may also be deleted if the
user didn't click on them within 7 days.) 'unknown_path' can therefore mean
two things: either the activation link was was never valid (the user
mistyped it or just tried to guess one), or it used to be valid but has been
deleted. There is no way to distinguish between these cases.

FIXME How to test this without actually sending an email?

User Login
----------

To log-in an existing and activated user via password, the frontend posts a
{'fraction_non_alphanumeric': 0.122966228041745, 'fraction_numerical': 0.0027224995138393727, 'mean_word_length': 2.7776689520078355, 'pattern_counts': {'":': 0, '<': 21, '<?xml version=': 0, '>': 177, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6535962', 'n_tokens_mistral': 4299, 'n_tokens_neox': 4088, 'n_words': 1629}