<file_sep>"""Module containing steps for sending POST requests to the /login endpoint."""
from uuid import uuid4

from aloe import step, world
from requests import get, delete

from features.helper import *
from features.helper.accounts_api import create_random_account


@step(r"I try to delete a user's account")
def delete_user_account(self):
    create_random_account()
    world.response = delete(url(f"/accounts/{world.username}"))


@step(r"I try to delete an account that doesn't exist")
def delete_non_existant_account(self):
    world.username = uuid4()
    world.response = delete(url(f"/accounts/{world.username}"))


@step(r"I try to delete all users' accounts")
def delete_all_accounts(self):
    create_random_account()
    world.initial_accounts = get(url("/accounts")).json()
    world.response = delete(url("/accounts"))


@step(r"the user's account should be deleted")
def check_user_account_deleted(self):
    expect(get(url(f"/accounts/{world.response}")).status_code).to(equal(404))


@step(r"no accounts should be deleted")
def check_response_login_status(self):
    accounts = get(url("/accounts")).json()
    expect(accounts).to(contain(*world.initial_accounts))
<file_sep>"""Module containing steps for sending POSt requests to the /login endpoint."""
from aloe import step, world
from requests import post

from features.helper import *
from features.helper.accounts_api import create_random_account


@step(r"I attempt to login with an? (in)?valid (\w+) and password combination")
def attempt_login(self, invalid, login):
    json = create_random_account()
    json = {key: json[key] for key in (login, "password")}
    if invalid:
        json["password"] = "<PASSWORD>"
    world.response = post(url("/login"), json=json)


@step(r"I attempt to login without providing a password")
def attempt_no_password_login(self):
    create_random_account()
    world.response = post(url("/login"), json={"username": world.username})


@step(r"I provide an extra argument whilst attempting to login")
def attempt_login_with_extra_arg(self):
    json = create_random_account()
    del json["email"]
    world.unexpected_argument = {"you": "can't login with this"}
    json.update(world.unexpected_argument)
    world.response = post(url("/login"), json=json)


@step(r"the response should contain a key suggesting that login was (un)?successful")
def check_response_login_status(self, unsuccessful):
    success = str(not bool(unsuccessful)).lower()
    expect(world.response.json()).to(equal({"success": success}))
<file_sep>"""Module containing code for the /login endpoint."""
from flask_restful import Resource, abort
from flask import request
from werkzeug.security import check_password_hash

from lib.accounts import find_account


class Login(Resource):
    """Class representing the /login endpoint in the API."""

    def post(self):
        """Find out whether the given json credentials are valid."""
        json = request.get_json()

        login = json.pop("username", None) or json.pop("email", None)
        if login is None:
            abort(400, message="One of the following arguments are required: 'username', 'email'")
        try:
            password = json.pop("password")
        except KeyError:
            abort(400, message="The 'password' argument is required.")

        if json:
            abort(400, message=f"Unexpected arguments: {json}")

        account = find_account(login)

        if account is None:
            abort(404, message=f"No account exists with username '{username}'.")

        success = check_password_hash(account["password"], password)

        json = {"success": str(success).lower()}
        return json, 200 if success else 401
<file_sep>"""Module containing generic API response checking steps."""
from aloe import step, world
from expects import *


@step(r"I should get a (\d+) response")
def check_response(self, expected_response_code):
    expect(world.response.status_code).to(equal(int(expected_response_code)))
<file_sep>from aloe import step, world

from features.helper import *


EXPECTED_MESSAGES = {
    "account creation was successful":
        "An account with username '{username}' was successfully created.",

    "the username is already in use":
        "An account already exists with username '{username}'.",

    "usernames cannot contain @ symbols":
        "The value of the 'username' field must not contain an '@' symbol.",

    "a required argument was missing":
        "The 'username', '<PASSWORD>' and 'email' arguments are required.",

    "an unexpected argument was given":
        "Unexpected arguments: {unexpected_argument}",

    "the user does not exist":
        "No account exists with username '{username}'.",

    "the password argument was missing":
        "The 'password' argument is required.",
}


@step("the response message should indicate that ((?:[\w@] ?)+)")
def check_message(self, message_purpose):
    expected_message = EXPECTED_MESSAGES[message_purpose]
    expected_message = expected_message.format(**world.__dict__)
    expect(world.response.json()["message"]).to(equal(expected_message))
<file_sep>"""Module containing functions for accessing the database for the REST API."""
import sqlite3 as _sqlite


DATABASE_FILE = "api.db"

def sql(query):
    """Execute the given SQL query on the database."""
    with _sqlite.connect(DATABASE_FILE) as connection:
        return connection.cursor().execute(query)


def find_account(login):
    """Return the account with the given login or None if it doesn't exist."""
    login = login.casefold()
    login_type = "email" if "@" in login else "username"
    sql("SELECT email, username, password"
        "FROM accounts"
        f"WHERE {login_type} = {login}")


def add_account(username, password, email):
    """Add the given account information to the database."""
    sql("INSERT INTO accounts VALUES({username}, {password}, {email})")
<file_sep>Flask>=0.12.2
Flask-RESTful>=0.3.6
Werkzeug>=0.12.2
<file_sep>"""Module containing test execution steps for calling the accounts API."""
from requests import get, post
from aloe import step, world

from features.helper import *   # pylint: disable=wildcard-import,unused-wildcard-import
from features.helper.accounts_api import post_account, generate_random_account_json


@step(r"I try to create an account ((?:[\w@] ?)+)")
def create_account_valid_request(self, condition):
    json = generate_random_account_json()

    if condition.startswith("without providing a"):
        argument_name = condition.split()[-1]
        del json[argument_name]

    elif condition == "using a username with an @ symbol in it":
        json["username"] += "@"

    elif condition == "whilst providing an extra argument":
        world.unexpected_argument = {"well,": "this is unexpected"}
        json.update(world.unexpected_argument)

    elif condition == "using a used username":
        post_account(json)  # Post account an extra time

    post_account(json)


@step("an account with that username should be created")
def confirm_account_created(self):
    expect(get(url(f"/accounts/{world.username}")).status_code).to(equal(200))
<file_sep>"""Module containing helper methods for accessing the /accounts endpoint of the API."""
from uuid import uuid4

from aloe import world
from requests import post

from features.helper import url


def generate_random_account_json():
    """Generate random credentials for creating an account with."""
    world.username = str(uuid4())
    world.password = str(<PASSWORD>())
    world.email = f"{<EMAIL>"
    return {"username": world.username, "password": <PASSWORD>, "email": world.email}


def post_account(json):
    """Send a POST request to the /accounts endpoint with the given json payload."""
    world.response = post(url("/accounts"), json=json)


def create_random_account():
    """Create a random account and return the credentials."""
    json = generate_random_account_json()
    post_account(json)
    return json
<file_sep># Proposals

* Make code comply with `pylint`.

* Use a database instead of the `ACCOUNTS` list.

* Try using `return {"message": message}, code` instead of `abort(code, message=message)`

* Use `before` hooks to create accounts for tests that need them,
or add a `Given` step to create them.
<file_sep>"""Module containing steps for sending GET requests to the /accounts endpoint."""
from uuid import uuid4

from aloe import step, world
from requests import get

from features.helper import *
from features.helper.accounts_api import create_random_account


@step(r"I try to get the details of an existing account")
def get_exisiting_account_details(self):
    create_random_account()
    world.response = get(url(f"/accounts/{world.username}"))


@step(r"the response should contain the user's public details")
def check_response_contains_user_details(self):
    expect(world.response.json()).to(equal({"username": world.username}))


@step(r"I try to get the details of an account that doesn't exist")
def get_non_existant_account(self):
    world.username = uuid4()
    world.response = get(url(f"/accounts/{world.username}"))

@step(r"I try to get all the usernames that are currently in use")
def get_all_usernames(self):
    world.usernames = [create_random_account()["username"] for _ in range(3)]
    world.response = get(url("/accounts"))

@step(r"the response should contain a list of all used usernames")
def check_response_contains_existing_usernames(self):
    expect(world.response.json()).to(contain(*world.usernames))
<file_sep>aloe>=0.1.15
expects>=0.8.0
requests>=2.18.4
<file_sep>from expects import *

from features.config import BASE_URL


# Helper functions
def url(path="/"):
    """Return the full url to the given path."""
    return BASE_URL + path
<file_sep># Account Service RESTful API

Written using the Flask framework for Python.
<file_sep>"""Main file for running the accounts service API."""
from flask import Flask
from flask_restful import Api

from lib.accounts import Accounts, AccountsList
from lib.login import Login


app = Flask(__name__)
api = Api(app)


api.add_resource(AccountsList, "/accounts")
api.add_resource(Accounts, "/accounts/<username>")

api.add_resource(Login, "/login")


if __name__ == "__main__":
    app.run(debug=True)
<file_sep>"""Module containing code for the /accounts endpoint."""
from flask_restful import Resource, abort
from flask_restful.reqparse import RequestParser
from flask import request
from werkzeug.security import generate_password_hash


ACCOUNTS = []   # TODO: replace with a database (SQL Alchemy?)

def find_account(login):
    """Return the account with the given login or None if it doesn't exist."""
    login = login.casefold()
    login_type = "email" if "@" in login else "username"
    for account in ACCOUNTS:
        if account[login_type].casefold() == login:
            return account.copy()
    return None


class Accounts(Resource):
    """Class representing the /accounts/{username} endpoint in the REST API."""

    def get(self, username):
        """Get the details of the account with the given `username` parameter."""
        account = find_account(username)
        if account is None:
            abort(404, message=f"No account exists with username '{username}'.")

        # Remove email and password from response
        account.pop("email")
        account.pop("password")

        return account

    def delete(self, username):
        """Delete the account with the given username."""
        account = find_account(username)
        if account is None:
            abort(404, message=f"No account exists with username '{username}'.")

        ACCOUNTS.remove(account)


class AccountsList(Resource):
    """Class representing the base /accounts endpoint in the REST API."""

    def get(self):
        """Get all existing account usernames."""
        return [account["username"] for account in ACCOUNTS]

    def post(self):
        """Add a new account."""
        json = request.get_json()
        try:
            username = json.pop("username")
            password = json.pop("<PASSWORD>")
            email = json.pop("email")
        except KeyError:
            abort(400, message="The 'username', 'password' and 'email' arguments are required.")

        if json:
            abort(400, message=f"Unexpected arguments: {json}")

        if "@" in username:
            abort(422, message="The value of the 'username' field must not contain an '@' symbol.")

        if find_account(username) is not None:
            abort(409, message=f"An account already exists with username '{username}'.")

        ACCOUNTS.append({
            "username": username,
            "password": <PASSWORD>_<PASSWORD>_<PASSWORD>(password),
            "email": email,
        })

        json = {"message": f"An account with username '{username}' was successfully created."}
        return json, 201
