Merge remote-tracking branch 'origin/monolith' into vue

This commit is contained in:
Patrick Smith 2018-08-02 16:36:22 -04:00
commit 165a5bf374
41 changed files with 478 additions and 510 deletions

View File

@ -17,18 +17,17 @@ flask = "*"
flask-sqlalchemy = "*"
flask-assets = "*"
flask-session = "*"
flask-wtf = "*"
[dev-packages]
bandit = "*"
pytest = "*"
pytest-tornado = "*"
ipython = "*"
ipdb = "*"
pylint = "*"
black = "*"
pytest-watch = "*"
factory-boy = "*"
pytest-flask = "*"
[requires]
python_version = "3.6"

76
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "e04e11d9bd5c1dcc725de48b20902f5c416417e73774e557e45af7bd0c147ff5"
"sha256": "9f17530cb96833c424369b9cac305cb43a817cdf19605aaedeb2d98566302857"
},
"pipfile-spec": 6,
"requires": {
@ -62,14 +62,6 @@
"index": "pypi",
"version": "==2.3.2"
},
"flask-wtf": {
"hashes": [
"sha256:5d14d55cfd35f613d99ee7cba0fc3fbbe63ba02f544d349158c14ca15561cc36",
"sha256:d9a9e366b32dcbb98ef17228e76be15702cd2600675668bca23f63a7947fd5ac"
],
"index": "pypi",
"version": "==0.14.2"
},
"itsdangerous": {
"hashes": [
"sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519"
@ -168,7 +160,6 @@
"sha256:1d936da41ee06216d89fdc7ead1ee9a5da2811a8787515a976b646e110c3f622",
"sha256:e4ef42e82b0b493c5849eed98b5ab49d6767caf982127e9a33167f1153b36cc5"
],
"markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.3.*'",
"version": "==2018.5"
},
"redis": {
@ -350,9 +341,16 @@
"sha256:0e9a1227a3a0f3297a485715e72ee6eb77081b17b629367042b586e38c03c867",
"sha256:b4840807a94a3bad0217d6ed3f9b65a1cc6e1db1c99e1184673056ae2c0a4c4d"
],
"markers": "python_version != '3.1.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.0.*'",
"version": "==0.8.17"
},
"flask": {
"hashes": [
"sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48",
"sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05"
],
"index": "pypi",
"version": "==1.0.2"
},
"gitdb2": {
"hashes": [
"sha256:87783b7f4a8f6b71c7fe81d32179b3c8781c1a7d6fa0c69bff2f315b00aff4f8",
@ -395,9 +393,14 @@
"sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8",
"sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497"
],
"markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.2.*'",
"version": "==4.3.4"
},
"itsdangerous": {
"hashes": [
"sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519"
],
"version": "==0.24"
},
"jedi": {
"hashes": [
"sha256:b409ed0f6913a701ed474a614a3bb46e6953639033e31f769ca7581da5bd1ec1",
@ -405,6 +408,13 @@
],
"version": "==0.12.1"
},
"jinja2": {
"hashes": [
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
],
"version": "==2.10"
},
"lazy-object-proxy": {
"hashes": [
"sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33",
@ -439,6 +449,12 @@
],
"version": "==1.3.1"
},
"markupsafe": {
"hashes": [
"sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"
],
"version": "==1.0"
},
"mccabe": {
"hashes": [
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
@ -494,7 +510,6 @@
"sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1",
"sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1"
],
"markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.2.*'",
"version": "==0.7.1"
},
"prompt-toolkit": {
@ -517,7 +532,6 @@
"sha256:3fd59af7435864e1a243790d322d763925431213b6b8529c6ca71081ace3bbf7",
"sha256:e31fb2767eb657cbde86c454f02e99cb846d3cd9d61b318525140214fdc0e98e"
],
"markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.2.*'",
"version": "==1.5.4"
},
"pygments": {
@ -543,13 +557,13 @@
"index": "pypi",
"version": "==3.7.0"
},
"pytest-tornado": {
"pytest-flask": {
"hashes": [
"sha256:214fc59d06fb81696fce3028b56dff522168ac1cfc784cfc0077b7b1e425b4cd",
"sha256:687c1f9c0f5bda7808c1e53c14bbebfe4fb9452e34cc95b440e598d4724265e0"
"sha256:2c5a36f9033ef8b6f85ddbefaebdd4f89197fc283f94b20dfe1a1beba4b77f03",
"sha256:657c7de386215ab0230bee4d76ace0339ae82fcbb34e134e17a29f65032eef03"
],
"index": "pypi",
"version": "==0.5.0"
"version": "==0.10.0"
},
"pytest-watch": {
"hashes": [
@ -567,11 +581,15 @@
},
"pyyaml": {
"hashes": [
"sha256:1cbc199009e78f92d9edf554be4fe40fb7b0bef71ba688602a00e97a51909110",
"sha256:254bf6fda2b7c651837acb2c718e213df29d531eebf00edb54743d10bcb694eb",
"sha256:3108529b78577327d15eec243f0ff348a0640b0c3478d67ad7f5648f93bac3e2",
"sha256:3c17fb92c8ba2f525e4b5f7941d850e7a48c3a59b32d331e2502a3cdc6648e76",
"sha256:6f89b5c95e93945b597776163403d47af72d243f366bf4622ff08bdfd1c950b7",
"sha256:8d6d96001aa7f0a6a4a95e8143225b5d06e41b1131044913fecb8f85a125714b",
"sha256:c8a88edd93ee29ede719080b2be6cb2333dfee1dccba213b422a9c8e97f2967b"
"sha256:be622cc81696e24d0836ba71f6272a2b5767669b0d79fdcf0295d51ac2e156c8",
"sha256:c8a88edd93ee29ede719080b2be6cb2333dfee1dccba213b422a9c8e97f2967b",
"sha256:f39411e380e2182ad33be039e8ee5770a5d9efe01a2bfb7ae58d9ba31c4a2a9d"
],
"version": "==4.2b4"
},
@ -615,19 +633,6 @@
],
"version": "==0.9.4"
},
"tornado": {
"hashes": [
"sha256:1c0816fc32b7d31b98781bd8ebc7a9726d7dce67407dc353a2e66e697e138448",
"sha256:4f66a2172cb947387193ca4c2c3e19131f1c70fa8be470ddbbd9317fd0801582",
"sha256:5327ba1a6c694e0149e7d9126426b3704b1d9d520852a3e4aa9fc8fe989e4046",
"sha256:6a7e8657618268bb007646b9eae7661d0b57f13efc94faa33cd2588eae5912c9",
"sha256:a9b14804783a1d77c0bd6c66f7a9b1196cbddfbdf8bceb64683c5ae60bd1ec6f",
"sha256:c58757e37c4a3172949c99099d4d5106e4d7b63aa0617f9bb24bfbff712c7866",
"sha256:d8984742ce86c0855cccecd5c6f54a9f7532c983947cff06f3a0e2115b47f85c"
],
"index": "pypi",
"version": "==5.1"
},
"traitlets": {
"hashes": [
"sha256:9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835",
@ -684,6 +689,13 @@
],
"version": "==0.1.7"
},
"werkzeug": {
"hashes": [
"sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c",
"sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b"
],
"version": "==0.14.1"
},
"wrapt": {
"hashes": [
"sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6"

View File

@ -72,6 +72,12 @@ To log in as one of them, navigate to `/login-dev?username=<lowercase name>`. Fo
## Testing
Tests require a test database:
```
createdb atat_test
```
To run lint, static analysis, and unit tests:
script/test

View File

@ -12,7 +12,7 @@ from atst.routes.workspaces import bp as workspace_routes
from atst.routes.requests import requests_bp
ENV = os.getenv("TORNADO_ENV", "dev")
ENV = os.getenv("FLASK_ENV", "dev")
def make_app(config):
@ -40,7 +40,7 @@ def make_app(config):
def make_flask_callbacks(app):
@app.before_request
def set_globals():
def _set_globals():
g.navigationContext = (
"workspace"
if re.match("\/workspaces\/[A-Za-z0-9]*", request.url)
@ -84,6 +84,10 @@ def make_config():
config = ConfigParser()
config.optionxform = str
config_files = [BASE_CONFIG_FILENAME, ENV_CONFIG_FILENAME]
if OVERRIDE_CONFIG_FILENAME:
config_files.append(OVERRIDE_CONFIG_FILENAME)
config_files = [BASE_CONFIG_FILENAME, ENV_CONFIG_FILENAME]
if OVERRIDE_CONFIG_FILENAME:
config_files.append(OVERRIDE_CONFIG_FILENAME)

View File

@ -1,5 +1,4 @@
from flask_assets import Environment, Bundle
from atst.home import home
environment = Environment()

View File

@ -1,24 +1,25 @@
from sqlalchemy.dialects.postgresql import insert
from atst.database import db
from atst.models.pe_number import PENumber
from .exceptions import NotFoundError
class PENumbers(object):
def __init__(self, db_session):
self.db_session = db_session
def get(self, number):
pe_number = self.db_session.query(PENumber).get(number)
@classmethod
def get(cls, number):
pe_number = db.session.query(PENumber).get(number)
if not pe_number:
raise NotFoundError("pe_number")
return pe_number
def create_many(self, list_of_pe_numbers):
@classmethod
def create_many(cls, list_of_pe_numbers):
stmt = insert(PENumber).values(list_of_pe_numbers)
do_update = stmt.on_conflict_do_update(
index_elements=["number"], set_=dict(description=stmt.excluded.description)
)
self.db_session.execute(do_update)
self.db_session.commit()
db.session.execute(do_update)
db.session.commit()

View File

@ -1,4 +1,3 @@
import tornado.gen
from sqlalchemy import exists, and_
from sqlalchemy.orm.exc import NoResultFound
from sqlalchemy.orm.attributes import flag_modified

View File

@ -1,12 +1,11 @@
import re
import tornado
from tornado.gen import Return
from wtforms.fields.html5 import EmailField
from wtforms.fields import StringField, SelectField
from wtforms.form import Form
from wtforms.validators import Required, Email
from atst.domain.exceptions import NotFoundError
from atst.domain.pe_numbers import PENumbers
from .fields import NewlineListField
from .forms import ValidatedForm
@ -40,9 +39,9 @@ def suggest_pe_id(pe_id):
return None
def validate_pe_id(field, existing_request, pe_numbers_repo):
def validate_pe_id(field, existing_request):
try:
pe_number = pe_numbers_repo.get(field.data)
pe_number = PENumbers.get(field.data)
except NotFoundError:
suggestion = suggest_pe_id(field.data)
error_str = (
@ -50,17 +49,17 @@ def validate_pe_id(field, existing_request, pe_numbers_repo):
"If you have double checked it you can submit anyway. "
"Your request will need to go through a manual review."
).format('Did you mean "{}"? '.format(suggestion) if suggestion else "")
field.errors.append(error_str)
field.errors += (error_str,)
return False
return True
class FinancialForm(ValidatedForm):
def perform_extra_validation(self, existing_request, pe_numbers_repo):
def perform_extra_validation(self, existing_request):
valid = True
if not existing_request or existing_request.get("pe_id") != self.pe_id.data:
valid = yield validate_pe_id(self.pe_id, existing_request, pe_numbers_repo)
valid = validate_pe_id(self.pe_id, existing_request)
return valid
task_order_id = StringField(

View File

@ -47,6 +47,7 @@ class RequestFinancialVerification(BaseHandler):
if form.validate():
yield self.update_request(request_id, form.data)
# pylint: disable=E1121
valid = yield form.perform_extra_validation(
existing_request.body.get("financial_verification"),
self.pe_numbers_repo,

View File

@ -7,6 +7,11 @@ bp = Blueprint("atst", __name__)
@bp.route("/")
def root():
return render_template("root.html")
@bp.route("/home")
def home():
return render_template("home.html")
@ -14,3 +19,8 @@ def home():
@bp.route("/styleguide")
def styleguide():
return render_template("styleguide.html")
@bp.route('/<path:path>')
def catch_all(path):
return render_template("{}.html".format(path))

View File

@ -1,4 +1,5 @@
from flask import render_template, redirect, url_for
from flask import request as http_request
from . import requests_bp
from atst.domain.requests import Requests
@ -29,7 +30,7 @@ def update_financial_verification(request_id):
existing_request.body.get("financial_verification")
)
if valid:
redirect(url_for("requests.financial_verification_submitted"))
return redirect(url_for("requests.financial_verification_submitted"))
else:
return render_template(
"requests/financial_verification.html", **rerender_args

View File

@ -1,7 +1,6 @@
from collections import defaultdict
from atst.domain.requests import Requests
from atst.forms.financial import FinancialForm
from atst.forms.request import RequestForm
from atst.forms.org import OrgForm
from atst.forms.poc import POCForm

View File

@ -1,7 +1,7 @@
from flask import Blueprint, render_template
from atst.domain.workspaces import Projects, Members
from atst.database import db
bp = Blueprint("workspaces", __name__)

View File

@ -11,7 +11,7 @@ def navigationContext(self):
def dev(self):
return os.getenv("TORNADO_ENV", "dev") == "dev"
return os.getenv("FLASK_ENV", "dev") == "dev"
def matchesPath(self, href):

2
config/test.ini Normal file
View File

@ -0,0 +1,2 @@
[default]
PGDATABASE = atat_test

View File

@ -4,6 +4,8 @@
source "$(dirname "${0}")"/../script/include/global_header.inc.sh
export FLASK_ENV=test
# Define all relevant python files and directories for this app
PYTHON_FILES="./app.py ./atst ./config"

View File

@ -1,4 +1,4 @@
{% extends "base.html.to" %}
{% extends "base.html" %}
{% block content %}
@ -8,5 +8,5 @@
</main>
{% end %}
{% endblock %}

View File

@ -16,40 +16,39 @@
level="info"
) %}
<div class='panel member-card'>
<div class='member-card__header'>
<h1 class='member-card__heading'>{{ member_name }}</h1>
<dl><dt>Workspace Role</dt> <dd><span class='label label--info'>{{member_workspace_role}}</span></dd></dl>
</div>
<div class='member-card__details'>
<dl>
<div>
<dt>DOD ID:</dt>
<dd>{{ member_id }}</dd>
</div>
<div>
<dt>Email:</dt>
<dd>{{ member_email }}</dd>
</div>
</dl>
<a href='#' class='icon-link'>edit account details</a>
<div class='panel'>
<div class='panel__heading'>
<h1 class='h2'>
{% if is_new_member %}
Add new member
{% else %}
{{ member_name }}
{% end %}
</h1>
<div>Workspace Role</span> <span class="label">{{member_workspace_role}}</div>
</div>
</div>
<form class='search-bar'>
<div class='usa-input search-input'>
<label for='project-search'>Search by project name</label>
<input type='search' id='project-search' name='project-search' placeholder="Search by project name"/>
<button type="submit">
<span class="hide">Search</span>
</button>
<div class='panel panel__actions'>
<div class='row'>
<div class='col col--grow'>
<form class="usa-search usa-search-small">
<label class="usa-sr-only" for="search-field-small">Search small</label>
<input id="search-field-small" type="search" name="search" placeholder="Search by project name">
<button type="submit">
<span class="usa-sr-only">Search</span>
</button>
</form>
</div>
</div>
</form>
</div>
<div class='block-list project-list-item'>
<header class='block-list__header'>
<h2 class='block-list__title'>{% module Icon('arrow-down') %} Code.mil</h2>
<span><a href="#" class="icon-link icon-link--danger">revoke all access</a></span>
<h2 class='block-list__title'>Code.mil</h2>
<a class="block-list__header__link icon-link icon-link--danger">revoke all access</a>
</header>
<ul>
<li class='block-list__item project-list-item__environment'>
@ -57,15 +56,7 @@
Development
</span>
<div class='project-list-item__environment__actions'>
<span class="label">no access </span><a href="#" class="icon-link">set role</a>
</div>
</li>
<li class='block-list__item project-list-item__environment'>
<span class='project-list-item__environment'>
Sandbox
</span>
<div class='project-list-item__environment__actions'>
<span class="label">no access</span><a href="#" class="icon-link">set role</a>
<span>no access</span><a href="#" class="icon-link">set role</a>
</div>
</li>
<li class='block-list__item project-list-item__environment'>
@ -73,17 +64,11 @@
Production
</span>
<div class='project-list-item__environment__actions'>
<span class="label label--success">Billing</span><a href="#" class="icon-link">set role</a>
<span>Billing</span><a href="#" class="icon-link">set role</a>
</div>
</li>
</ul>
</div>
<div class='block-list project-list-item'>
<header class='block-list__header'>
<h2 class='block-list__title'>{% module Icon('arrow-right') %} Digital Dojo</h2>
<span class="label">no access</span>
</header>
</ul>
</div>
<div class='action-group'>

View File

@ -1,44 +1,42 @@
{% from "components.html" import SidenavItem %}
<nav class='sidenav workspace-navigation'>
<ul>
{{ SidenavItem(
{% module SidenavItem(
"Projects",
href=url_for("workspaces.workspace_projects", workspace_id="123456"),
active=g.matchesPath('\/workspaces\/[A-Za-z0-9]*\/projects'),
href=reverse_url('workspace_projects', '123456'),
active=matchesPath('\/workspaces\/[A-Za-z0-9]*\/projects'),
subnav=[
{
"label": "Add New Project",
"href":"/",
"active": g.matchesPath('workspaces/projects/new'),
"active": matchesPath('workspaces/projects/new'),
"icon": "plus"
}
]
)}}
)%}
{{ SidenavItem(
{% module SidenavItem(
"Members",
href="/workspaces/{}/members".format('123456'),
active=g.matchesPath('\/workspaces\/[A-Za-z0-9]*\/members'),
href=reverse_url('workspace_members', '123456'),
active=matchesPath('\/workspaces\/[A-Za-z0-9]*\/members'),
subnav=[
{
"label": "Add New Member",
"href": "",
"active": g.matchesPath('/workspaces/members/new'),
"active": matchesPath('/workspaces/members/new'),
"icon": "plus"
},
{
"label": "Editing Member",
"href": "",
"active": g.matchesPath('/workspaces/123456/members/789/edit')
"active": matchesPath('/workspaces/123456/members/789/edit')
}
]
)}}
)%}
{{ SidenavItem(
{% module SidenavItem(
"Funding & Reports",
href=url_for("workspaces.workspace_projects", workspace_id="123456"),
active=g.matchesPath('\/workspaces\/[A-Za-z0-9]*\/reports')
)}}
href=reverse_url('workspace_projects', '123456'),
active=matchesPath('\/workspaces\/[A-Za-z0-9]*\/reports')
)%}
</ul>
</nav>

View File

@ -1,4 +1,4 @@
{% extends "base.html.to" %}
{% extends "base.html" %}
{% block content %}
@ -8,5 +8,5 @@
</main>
{% end %}
{% endblock %}

View File

@ -3,10 +3,10 @@
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>{% block title %}JEDI{% end %}</title>
{% for url in assets['css'].urls() %}
<link rel="stylesheet" href="{{ url }}" type="text/css">
{% end %}
<title>{% block title %}JEDI{% endblock %}</title>
{% assets "css" %}
<link rel="stylesheet" href="{{ ASSET_URL }}" type="text/css">
{% endassets %}
<link rel="icon" type="image/x-icon" href="/static/img/favicon.ico">
</head>
<body>
@ -17,11 +17,11 @@
<h1 class="usa-display">JEDI</h1>
<a class="usa-button" href='{{ config['default'].get('cac_url','https://cac.atat.codes') }}'><span>Sign In with CAC</span></a>
<a class="usa-button" href='{{ config.get('cac_url','https://cac.atat.codes') }}'><span>Sign In with CAC</span></a>
<button class="usa-button" disabled>Sign In via MFA</button>
{% if dev() %}
{% if g.dev %}
<a class="usa-button usa-button-secondary" href='/login-dev'><span>DEV Login</span></a>
{% end %}
{% endif %}
</main>

View File

@ -1,4 +1,4 @@
{% extends "base.html.to" %}
{% extends "base.html" %}
{% block content %}
@ -8,5 +8,5 @@
</main>
{% end %}
{% endblock %}

View File

@ -1,17 +1,15 @@
{% from "components.html" import EmptyState %}
{% extends "base_workspace.html" %}
{% extends "base_workspace.html.to" %}
{% block workspace_content %}
{% if not members %}
{{ EmptyState(
{% module EmptyState(
'There are currently no members in this Workspace.',
actionLabel='Invite a new Member',
actionHref='/members/new',
icon='avatar'
)}}
)%}
{% else %}
@ -61,17 +59,17 @@
{% for m in members %}
<tr>
<td><a href="/workspaces/123456/members/789/edit" class="icon-link icon-link--large">{{ m['first_name'] }} {{ m['last_name'] }}</a></td>
<td class='table-cell--shrink'>{% if m['num_projects'] == '0' %} <span class="label label--info">No Project Access</span> {% endif %}</td>
<td class='table-cell--shrink'>{% if m['num_projects'] == '0' %} <span class="label label--info">No Project Access</span> {% end %}</td>
<td>{{ m['status'] }}</a></td>
<td>{{ m['workspace_role'] }}</a></td>
</tr>
{% endfor %}
{% end %}
</tbody>
</table>
</div>
{% endif %}
{% end %}
{% endblock %}
{% end %}

View File

@ -1,44 +1,72 @@
import os
import pytest
import alembic.config
import alembic.command
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, scoped_session
from atst.app import make_app, make_deps, make_config
from atst.database import make_db
from atst.app import make_app, make_config
from tests.mocks import MockApiClient
from atst.sessions import DictSessions
from atst.models import Base
from atst.database import db as _db
@pytest.fixture
def app(db):
TEST_DEPS = {
"authnid_client": MockApiClient("authnid"),
"sessions": DictSessions(),
"db_session": db
}
@pytest.fixture(scope='session')
def app(request):
config = make_config()
deps = make_deps(config)
deps.update(TEST_DEPS)
return make_app(config, deps)
_app = make_app(config)
ctx = _app.app_context()
ctx.push()
def teardown():
ctx.pop()
return _app
@pytest.fixture(scope='function')
def db():
def apply_migrations():
"""Applies all alembic migrations."""
alembic_config = os.path.join(os.path.dirname(__file__), "../", "alembic.ini")
config = alembic.config.Config(alembic_config)
app_config = make_config()
config.set_main_option('sqlalchemy.url', app_config["DATABASE_URI"])
alembic.command.upgrade(config, 'head')
# Override db with a new SQLAlchemy session so that we can rollback
# each test's transaction.
# Inspiration: https://docs.sqlalchemy.org/en/latest/orm/session_transaction.html#session-external-transaction
config = make_config()
database = make_db(config)
connection = database.get_bind().connect()
@pytest.fixture(scope='session')
def db(app, request):
def teardown():
_db.drop_all()
_db.app = app
apply_migrations()
yield _db
_db.drop_all()
@pytest.fixture(scope='function', autouse=True)
def session(db, request):
"""Creates a new database session for a test."""
connection = db.engine.connect()
transaction = connection.begin()
db = scoped_session(sessionmaker(bind=connection))
yield db
options = dict(bind=connection, binds={})
session = db.create_scoped_session(options=options)
db.session = session
yield session
db.close()
transaction.rollback()
connection.close()
session.remove()
class DummyForm(dict):

View File

@ -6,36 +6,32 @@ from atst.domain.pe_numbers import PENumbers
from tests.factories import PENumberFactory
@pytest.fixture()
def pe_numbers(db):
return PENumbers(db)
@pytest.fixture(scope="function")
def new_pe_number(db):
def new_pe_number(session):
def make_pe_number(**kwargs):
pen = PENumberFactory.create(**kwargs)
db.add(pen)
db.commit()
session.add(pen)
session.commit()
return pen
return make_pe_number
def test_can_get_pe_number(pe_numbers, new_pe_number):
def test_can_get_pe_number(new_pe_number):
new_pen = new_pe_number(number="0701367F", description="Combat Support - Offensive")
pen = pe_numbers.get(new_pen.number)
pen = PENumbers.get(new_pen.number)
assert pen.number == new_pen.number
def test_nonexistent_pe_number_raises(pe_numbers):
def test_nonexistent_pe_number_raises():
with pytest.raises(NotFoundError):
pe_numbers.get("some fake number")
PENumbers.get("some fake number")
def test_create_many(pe_numbers):
def test_create_many():
pen_list = [['123456', 'Land Speeder'], ['7891011', 'Lightsaber']]
pe_numbers.create_many(pen_list)
PENumbers.create_many(pen_list)
assert pe_numbers.get(pen_list[0][0])
assert pe_numbers.get(pen_list[1][0])
assert PENumbers.get(pen_list[0][0])
assert PENumbers.get(pen_list[1][0])

View File

@ -8,14 +8,14 @@ from tests.factories import RequestFactory
@pytest.fixture()
def requests(db):
return Requests(db)
def requests(session):
return Requests()
@pytest.fixture(scope="function")
def new_request(db):
def new_request(session):
created_request = RequestFactory.create()
db.add(created_request)
db.commit()
session.add(created_request)
session.commit()
return created_request
@ -31,25 +31,22 @@ def test_nonexistent_request_raises(requests):
requests.get(uuid4())
@pytest.mark.gen_test
def test_auto_approve_less_than_1m(requests, new_request):
new_request.body = {"details_of_use": {"dollar_value": 999999}}
request = yield requests.submit(new_request)
request = requests.submit(new_request)
assert request.status == 'approved'
@pytest.mark.gen_test
def test_dont_auto_approve_if_dollar_value_is_1m_or_above(requests, new_request):
new_request.body = {"details_of_use": {"dollar_value": 1000000}}
request = yield requests.submit(new_request)
request = requests.submit(new_request)
assert request.status == 'submitted'
@pytest.mark.gen_test
def test_dont_auto_approve_if_no_dollar_value_specified(requests, new_request):
new_request.body = {"details_of_use": {}}
request = yield requests.submit(new_request)
request = requests.submit(new_request)
assert request.status == 'submitted'

View File

@ -4,8 +4,8 @@ from atst.domain.exceptions import NotFoundError
@pytest.fixture()
def roles_repo(db):
return Roles(db)
def roles_repo(session):
return Roles(session)
def test_get_all_roles(roles_repo):

View File

@ -7,15 +7,15 @@ from tests.factories import TaskOrderFactory
@pytest.fixture()
def task_orders(db):
return TaskOrders(db)
def task_orders(session):
return TaskOrders(session)
@pytest.fixture(scope="function")
def new_task_order(db):
def new_task_order(session):
def make_task_order(**kwargs):
to = TaskOrderFactory.create(**kwargs)
db.add(to)
db.commit()
session.add(to)
session.commit()
return to

View File

@ -6,8 +6,8 @@ from atst.domain.exceptions import NotFoundError, AlreadyExistsError
@pytest.fixture()
def users_repo(db):
return Users(db)
def users_repo(session):
return Users(session)
@pytest.fixture(scope="function")

View File

@ -6,13 +6,13 @@ from atst.domain.users import Users
@pytest.fixture()
def users_repo(db):
return Users(db)
def users_repo(session):
return Users(session)
@pytest.fixture()
def workspace_users_repo(db):
return WorkspaceUsers(db)
def workspace_users_repo(session):
return WorkspaceUsers(session)
def test_can_create_new_workspace_user(users_repo, workspace_users_repo):

View File

@ -1,89 +0,0 @@
import re
import pytest
import tornado
import urllib
from tests.mocks import MOCK_REQUEST, MOCK_USER
from tests.factories import PENumberFactory
class TestPENumberInForm:
required_data = {
"pe_id": "123",
"task_order_id": "1234567899C0001",
"fname_co": "Contracting",
"lname_co": "Officer",
"email_co": "jane@mail.mil",
"office_co": "WHS",
"fname_cor": "Officer",
"lname_cor": "Representative",
"email_cor": "jane@mail.mil",
"office_cor": "WHS",
"funding_type": "RDTE",
"funding_type_other": "other",
"clin_0001": "50,000",
"clin_0003": "13,000",
"clin_1001": "30,000",
"clin_1003": "7,000",
"clin_2001": "30,000",
"clin_2003": "7,000",
}
def _set_monkeypatches(self, monkeypatch):
monkeypatch.setattr(
"atst.handlers.request_financial_verification.RequestFinancialVerification.get_current_user", lambda s: MOCK_USER
)
monkeypatch.setattr(
"atst.handlers.request_financial_verification.RequestFinancialVerification.check_xsrf_cookie", lambda s: True
)
monkeypatch.setattr("atst.forms.request.RequestForm.validate", lambda s: True)
monkeypatch.setattr("atst.domain.requests.Requests.get", lambda s, i: MOCK_REQUEST)
@tornado.gen.coroutine
def submit_data(self, http_client, base_url, data):
response = yield http_client.fetch(
base_url + "/requests/verify/{}".format(MOCK_REQUEST.id),
method="POST",
headers={"Content-Type": "application/x-www-form-urlencoded"},
body=urllib.parse.urlencode(data),
follow_redirects=False,
raise_error=False,
)
return response
@pytest.mark.gen_test
def test_submit_request_form_with_invalid_pe_id(self, monkeypatch, http_client, base_url):
self._set_monkeypatches(monkeypatch)
response = yield self.submit_data(http_client, base_url, self.required_data)
assert "We couldn\'t find that PE number" in response.body.decode()
assert response.code == 200
assert "/requests/verify" in response.effective_url
@pytest.mark.gen_test
def test_submit_request_form_with_unchanged_pe_id(self, monkeypatch, http_client, base_url):
self._set_monkeypatches(monkeypatch)
data = dict(self.required_data)
data['pe_id'] = MOCK_REQUEST.body['financial_verification']['pe_id']
response = yield self.submit_data(http_client, base_url, data)
assert response.code == 302
assert response.headers.get("Location") == "/requests/financial_verification_submitted"
@pytest.mark.gen_test
def test_submit_request_form_with_new_valid_pe_id(self, db, monkeypatch, http_client, base_url):
self._set_monkeypatches(monkeypatch)
pe = PENumberFactory.create(number="8675309U", description="sample PE number")
db.add(pe)
db.commit()
data = dict(self.required_data)
data['pe_id'] = pe.number
response = yield self.submit_data(http_client, base_url, data)
assert response.code == 302
assert response.headers.get("Location") == "/requests/financial_verification_submitted"

View File

@ -1,54 +0,0 @@
import re
import pytest
import tornado
import urllib
from tests.mocks import MOCK_USER
from tests.factories import RequestFactory
ERROR_CLASS = "alert--error"
MOCK_REQUEST = RequestFactory.create(
creator=MOCK_USER["id"],
body={
"financial_verification": {
"pe_id": "0203752A",
},
}
)
@pytest.mark.gen_test
def test_submit_invalid_request_form(monkeypatch, http_client, base_url):
monkeypatch.setattr(
"atst.handlers.request_new.RequestNew.get_current_user", lambda s: MOCK_USER
)
monkeypatch.setattr(
"atst.handlers.request_new.RequestNew.check_xsrf_cookie", lambda s: True
)
# this just needs to send a known invalid form value
response = yield http_client.fetch(
base_url + "/requests/new",
method="POST",
headers={"Content-Type": "application/x-www-form-urlencoded"},
body="total_ram=5",
)
assert response.effective_url == base_url + "/requests/new"
assert re.search(ERROR_CLASS, response.body.decode())
@pytest.mark.gen_test
def test_submit_valid_request_form(monkeypatch, http_client, base_url):
monkeypatch.setattr(
"atst.handlers.request_new.RequestNew.get_current_user", lambda s: MOCK_USER
)
monkeypatch.setattr(
"atst.handlers.request_new.RequestNew.check_xsrf_cookie", lambda s: True
)
monkeypatch.setattr("atst.forms.request.RequestForm.validate", lambda s: True)
# this just needs to send a known invalid form value
response = yield http_client.fetch(
base_url + "/requests/new",
method="POST",
headers={"Content-Type": "application/x-www-form-urlencoded"},
body="meaning=42",
)
assert "/requests/new/2" in response.effective_url

View File

@ -1,57 +0,0 @@
import pytest
import tornado
from tests.mocks import MOCK_USER
from tests.factories import RequestFactory
@tornado.gen.coroutine
def _mock_func(*args, **kwargs):
return RequestFactory.create()
@pytest.mark.gen_test
def test_submit_reviewed_request(monkeypatch, http_client, base_url):
monkeypatch.setattr(
"atst.handlers.request_submit.RequestsSubmit.get_current_user",
lambda s: MOCK_USER,
)
monkeypatch.setattr(
"atst.handlers.request_submit.RequestsSubmit.check_xsrf_cookie", lambda s: True
)
monkeypatch.setattr("atst.domain.requests.Requests.get", _mock_func)
monkeypatch.setattr("atst.domain.requests.Requests.submit", _mock_func)
monkeypatch.setattr("atst.models.request.Request.status", "pending")
# this just needs to send a known invalid form value
response = yield http_client.fetch(
base_url + "/requests/submit/1",
method="POST",
headers={"Content-Type": "application/x-www-form-urlencoded"},
body="",
raise_error=False,
follow_redirects=False,
)
assert response.headers["Location"] == "/requests"
@pytest.mark.gen_test
def test_submit_autoapproved_reviewed_request(monkeypatch, http_client, base_url):
monkeypatch.setattr(
"atst.handlers.request_submit.RequestsSubmit.get_current_user",
lambda s: MOCK_USER,
)
monkeypatch.setattr(
"atst.handlers.request_submit.RequestsSubmit.check_xsrf_cookie", lambda s: True
)
monkeypatch.setattr("atst.domain.requests.Requests.get", _mock_func)
monkeypatch.setattr("atst.domain.requests.Requests.submit", _mock_func)
monkeypatch.setattr("atst.models.request.Request.status", "approved")
# this just needs to send a known invalid form value
response = yield http_client.fetch(
base_url + "/requests/submit/1",
method="POST",
headers={"Content-Type": "application/x-www-form-urlencoded"},
body="",
raise_error=False,
follow_redirects=False,
)
assert response.headers["Location"] == "/requests?modal=True"

View File

@ -0,0 +1,76 @@
import re
import pytest
import tornado
import urllib
from tests.mocks import MOCK_REQUEST, MOCK_USER
from tests.factories import PENumberFactory
class TestPENumberInForm:
required_data = {
"pe_id": "123",
"task_order_id": "1234567899C0001",
"fname_co": "Contracting",
"lname_co": "Officer",
"email_co": "jane@mail.mil",
"office_co": "WHS",
"fname_cor": "Officer",
"lname_cor": "Representative",
"email_cor": "jane@mail.mil",
"office_cor": "WHS",
"funding_type": "RDTE",
"funding_type_other": "other",
"clin_0001": "50,000",
"clin_0003": "13,000",
"clin_1001": "30,000",
"clin_1003": "7,000",
"clin_2001": "30,000",
"clin_2003": "7,000",
}
def _set_monkeypatches(self, monkeypatch):
monkeypatch.setattr("atst.forms.financial.FinancialForm.validate", lambda s: True)
monkeypatch.setattr("atst.domain.requests.Requests.get", lambda i: MOCK_REQUEST)
def submit_data(self, client, data):
response = client.post(
"/requests/verify/{}".format(MOCK_REQUEST.id),
headers={"Content-Type": "application/x-www-form-urlencoded"},
data=urllib.parse.urlencode(data),
follow_redirects=False,
)
return response
def test_submit_request_form_with_invalid_pe_id(self, monkeypatch, client):
self._set_monkeypatches(monkeypatch)
response = self.submit_data(client, self.required_data)
assert "We couldn\'t find that PE number" in response.data.decode()
assert response.status_code == 200
def test_submit_request_form_with_unchanged_pe_id(self, monkeypatch, client):
self._set_monkeypatches(monkeypatch)
data = dict(self.required_data)
data['pe_id'] = MOCK_REQUEST.body['financial_verification']['pe_id']
response = self.submit_data(client, data)
assert response.status_code == 302
assert "/requests/financial_verification_submitted" in response.headers.get("Location")
def test_submit_request_form_with_new_valid_pe_id(self, session, monkeypatch, client):
self._set_monkeypatches(monkeypatch)
pe = PENumberFactory.create(number="8675309U", description="sample PE number")
session.add(pe)
session.commit()
data = dict(self.required_data)
data['pe_id'] = pe.number
response = self.submit_data(client, data)
assert response.status_code == 302
assert "/requests/financial_verification_submitted" in response.headers.get("Location")

View File

@ -0,0 +1,35 @@
import re
import pytest
import urllib
from tests.mocks import MOCK_USER
from tests.factories import RequestFactory
ERROR_CLASS = "alert--error"
MOCK_REQUEST = RequestFactory.create(
creator=MOCK_USER["id"],
body={
"financial_verification": {
"pe_id": "0203752A",
},
}
)
def test_submit_invalid_request_form(monkeypatch, client):
response = client.post(
"/requests/new/1",
headers={"Content-Type": "application/x-www-form-urlencoded"},
data="total_ram=5",
)
assert re.search(ERROR_CLASS, response.data.decode())
def test_submit_valid_request_form(monkeypatch, client):
monkeypatch.setattr("atst.forms.request.RequestForm.validate", lambda s: True)
response = client.post(
"/requests/new/1",
headers={"Content-Type": "application/x-www-form-urlencoded"},
data="meaning=42",
)
assert "/requests/new/2" in response.headers.get("Location")

View File

@ -0,0 +1,37 @@
import pytest
import tornado
from tests.mocks import MOCK_USER
from tests.factories import RequestFactory
def _mock_func(*args, **kwargs):
return RequestFactory.create()
def test_submit_reviewed_request(monkeypatch, client):
monkeypatch.setattr("atst.domain.requests.Requests.get", _mock_func)
monkeypatch.setattr("atst.domain.requests.Requests.submit", _mock_func)
monkeypatch.setattr("atst.models.request.Request.status", "pending")
# this just needs to send a known invalid form value
response = client.post(
"/requests/submit/1",
headers={"Content-Type": "application/x-www-form-urlencoded"},
data="",
follow_redirects=False,
)
assert "/requests" in response.headers["Location"]
assert "modal" not in response.headers["Location"]
def test_submit_autoapproved_reviewed_request(monkeypatch, client):
monkeypatch.setattr("atst.domain.requests.Requests.get", _mock_func)
monkeypatch.setattr("atst.domain.requests.Requests.submit", _mock_func)
monkeypatch.setattr("atst.models.request.Request.status", "approved")
# this just needs to send a known invalid form value
response = client.post(
"/requests/submit/1",
headers={"Content-Type": "application/x-www-form-urlencoded"},
data="",
follow_redirects=False,
)
assert "/requests?modal=True" in response.headers["Location"]

View File

@ -1,10 +0,0 @@
import pytest
from atst.api_client import ApiClient
@pytest.mark.gen_test
def test_api_client(http_client, base_url):
client = ApiClient(base_url)
response = yield client.get("")
assert response.code == 200

View File

@ -1,81 +1,78 @@
import re
import pytest
import tornado.web
import tornado.gen
MOCK_USER = {"id": "438567dd-25fa-4d83-a8cc-8aa8366cb24a"}
@tornado.gen.coroutine
def _fetch_user_info(c, t):
return MOCK_USER
@pytest.mark.gen_test
def test_redirects_when_not_logged_in(http_client, base_url):
response = yield http_client.fetch(
base_url + "/home", raise_error=False, follow_redirects=False
)
location = response.headers["Location"]
assert response.code == 302
assert response.error
assert re.match("/\??", location)
@pytest.mark.skip
def test_redirects_when_not_logged_in():
pass
# response = yield http_client.fetch(
# base_url + "/home", raise_error=False, follow_redirects=False
# )
# location = response.headers["Location"]
# assert response.code == 302
# assert response.error
# assert re.match("/\??", location)
@pytest.mark.gen_test
def test_redirects_when_session_does_not_exist(monkeypatch, http_client, base_url):
monkeypatch.setattr("atst.handlers.main.Main.get_secure_cookie", lambda s,c: 'stale cookie!')
response = yield http_client.fetch(
base_url + "/home", raise_error=False, follow_redirects=False
)
location = response.headers["Location"]
cookie = response.headers._dict.get('Set-Cookie')
# should clear session cookie
assert 'atat=""' in cookie
assert response.code == 302
assert response.error
assert re.match("/\??", location)
# @pytest.mark.skip
# def test_redirects_when_session_does_not_exist():
# monkeypatch.setattr("atst.handlers.main.Main.get_secure_cookie", lambda s,c: 'stale cookie!')
# response = yield http_client.fetch(
# base_url + "/home", raise_error=False, follow_redirects=False
# )
# location = response.headers["Location"]
# cookie = response.headers._dict.get('Set-Cookie')
# # should clear session cookie
# assert 'atat=""' in cookie
# assert response.code == 302
# assert response.error
# assert re.match("/\??", location)
@pytest.mark.gen_test
def test_login_with_valid_bearer_token(app, monkeypatch, http_client, base_url):
monkeypatch.setattr("atst.handlers.login_redirect.LoginRedirect._fetch_user_info", _fetch_user_info)
response = yield http_client.fetch(
base_url + "/login-redirect?bearer-token=abc-123",
follow_redirects=False,
raise_error=False,
)
assert response.headers["Set-Cookie"].startswith("atat")
assert response.headers["Location"] == "/home"
assert response.code == 302
@pytest.mark.gen_test
def test_login_via_dev_endpoint(app, http_client, base_url):
response = yield http_client.fetch(
base_url + "/login-dev", raise_error=False, follow_redirects=False
)
assert response.headers["Set-Cookie"].startswith("atat")
assert response.code == 302
assert response.headers["Location"] == "/home"
@pytest.mark.gen_test
@pytest.mark.skip(reason="need to work out auth error user paths")
def test_login_with_invalid_bearer_token(http_client, base_url):
_response = yield http_client.fetch(
base_url + "/home",
raise_error=False,
headers={"Cookie": "bearer-token=anything"},
)
@pytest.mark.gen_test
def test_valid_login_creates_session(app, monkeypatch, http_client, base_url):
monkeypatch.setattr("atst.handlers.login_redirect.LoginRedirect._fetch_user_info", _fetch_user_info)
assert len(app.sessions.sessions) == 0
yield http_client.fetch(
base_url + "/login-redirect?bearer-token=abc-123",
follow_redirects=False,
raise_error=False,
)
assert len(app.sessions.sessions) == 1
session = list(app.sessions.sessions.values())[0]
assert "atat_permissions" in session["user"]
assert isinstance(session["user"]["atat_permissions"], list)
# @pytest.mark.skip
# def test_login_with_valid_bearer_token():
# monkeypatch.setattr("atst.handlers.login_redirect.LoginRedirect._fetch_user_info", _fetch_user_info)
# response = client.fetch(
# base_url + "/login-redirect?bearer-token=abc-123",
# follow_redirects=False,
# raise_error=False,
# )
# assert response.headers["Set-Cookie"].startswith("atat")
# assert response.headers["Location"] == "/home"
# assert response.code == 302
#
#
# @pytest.mark.skip
# def test_login_via_dev_endpoint():
# response = yield http_client.fetch(
# base_url + "/login-dev", raise_error=False, follow_redirects=False
# )
# assert response.headers["Set-Cookie"].startswith("atat")
# assert response.code == 302
# assert response.headers["Location"] == "/home"
#
#
# @pytest.mark.skip
# def test_login_with_invalid_bearer_token():
# _response = yield http_client.fetch(
# base_url + "/home",
# raise_error=False,
# headers={"Cookie": "bearer-token=anything"},
# )
#
# @pytest.mark.skip
# def test_valid_login_creates_session():
# monkeypatch.setattr("atst.handlers.login_redirect.LoginRedirect._fetch_user_info", _fetch_user_info)
# assert len(app.sessions.sessions) == 0
# yield http_client.fetch(
# base_url + "/login-redirect?bearer-token=abc-123",
# follow_redirects=False,
# raise_error=False,
# )
# assert len(app.sessions.sessions) == 1
# session = list(app.sessions.sessions.values())[0]
# assert "atat_permissions" in session["user"]
# assert isinstance(session["user"]["atat_permissions"], list)

View File

@ -1,7 +1,3 @@
import pytest
@pytest.mark.gen_test
def test_hello_world(http_client, base_url):
response = yield http_client.fetch(base_url)
assert response.code == 200
def test_hello_world(client):
response = client.get("/")
assert response.status_code == 200

View File

@ -1,13 +1,14 @@
import pytest
import tornado
from tests.mocks import MOCK_USER
from atst.handlers.request_new import JEDIRequestFlow
from atst.routes.requests.jedi_request_flow import JEDIRequestFlow
SCREENS = JEDIRequestFlow(None, None, 3).screens
@pytest.fixture
def screens(app):
return JEDIRequestFlow(3).screens
@pytest.mark.gen_test
def test_stepthrough_request_form(monkeypatch, http_client, base_url):
@pytest.mark.skip()
def test_stepthrough_request_form(monkeypatch, screens, client):
monkeypatch.setattr(
"atst.handlers.request_new.RequestNew.get_current_user", lambda s: MOCK_USER
)
@ -18,29 +19,28 @@ def test_stepthrough_request_form(monkeypatch, http_client, base_url):
"atst.handlers.request_new.JEDIRequestFlow.validate", lambda s: True
)
@tornado.gen.coroutine
def take_a_step(inc, req=None):
req_url = base_url + "/requests/new/{}".format(inc)
req_url = "/requests/new/{}".format(inc)
if req:
req_url += "/" + req
response = yield http_client.fetch(
response = client.post(
req_url,
method="POST",
headers={"Content-Type": "application/x-www-form-urlencoded"},
body="meaning=42",
data="meaning=42",
)
return response
# GET the initial form
response = yield http_client.fetch(base_url + "/requests/new", method="GET")
assert SCREENS[0]["title"] in response.body.decode()
response = client.get("/requests/new")
assert screens[0]["title"] in response.data.decode()
# POST to each of the form pages up until review and submit
req_id = None
for i in range(1, len(SCREENS)):
resp = yield take_a_step(i, req=req_id)
for i in range(1, len(screens)):
resp = take_a_step(i, req=req_id)
__import__('ipdb').set_trace()
req_id = resp.effective_url.split("/")[-1]
screen_title = SCREENS[i]["title"].replace("&", "&amp;")
screen_title = screens[i]["title"].replace("&", "&amp;")
assert "/requests/new/{}/{}".format(i + 1, req_id) in resp.effective_url
assert screen_title in resp.body.decode()
assert screen_title in resp.data.decode()

View File

@ -1,18 +1,19 @@
import pytest
@pytest.mark.gen_test
def test_routes(http_client, base_url):
def test_routes(client):
for path in (
"/",
"/home",
"/workspaces",
"/requests",
"/requests/new",
"/requests/new/1",
"/requests/new/2",
"/users",
"/reports",
"/calculator",
):
response = yield http_client.fetch(base_url + path)
assert response.code == 200
response = client.get(path)
if response.status_code == 404:
__import__('ipdb').set_trace()
assert response.status_code == 200