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-sqlalchemy = "*"
flask-assets = "*" flask-assets = "*"
flask-session = "*" flask-session = "*"
flask-wtf = "*"
[dev-packages] [dev-packages]
bandit = "*" bandit = "*"
pytest = "*" pytest = "*"
pytest-tornado = "*"
ipython = "*" ipython = "*"
ipdb = "*" ipdb = "*"
pylint = "*" pylint = "*"
black = "*" black = "*"
pytest-watch = "*" pytest-watch = "*"
factory-boy = "*" factory-boy = "*"
pytest-flask = "*"
[requires] [requires]
python_version = "3.6" python_version = "3.6"

76
Pipfile.lock generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,6 +7,11 @@ bp = Blueprint("atst", __name__)
@bp.route("/") @bp.route("/")
def root():
return render_template("root.html")
@bp.route("/home")
def home(): def home():
return render_template("home.html") return render_template("home.html")
@ -14,3 +19,8 @@ def home():
@bp.route("/styleguide") @bp.route("/styleguide")
def styleguide(): def styleguide():
return render_template("styleguide.html") 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 render_template, redirect, url_for
from flask import request as http_request
from . import requests_bp from . import requests_bp
from atst.domain.requests import Requests from atst.domain.requests import Requests
@ -29,7 +30,7 @@ def update_financial_verification(request_id):
existing_request.body.get("financial_verification") existing_request.body.get("financial_verification")
) )
if valid: if valid:
redirect(url_for("requests.financial_verification_submitted")) return redirect(url_for("requests.financial_verification_submitted"))
else: else:
return render_template( return render_template(
"requests/financial_verification.html", **rerender_args "requests/financial_verification.html", **rerender_args

View File

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

View File

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

View File

@ -11,7 +11,7 @@ def navigationContext(self):
def dev(self): def dev(self):
return os.getenv("TORNADO_ENV", "dev") == "dev" return os.getenv("FLASK_ENV", "dev") == "dev"
def matchesPath(self, href): 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 source "$(dirname "${0}")"/../script/include/global_header.inc.sh
export FLASK_ENV=test
# Define all relevant python files and directories for this app # Define all relevant python files and directories for this app
PYTHON_FILES="./app.py ./atst ./config" PYTHON_FILES="./app.py ./atst ./config"

View File

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

View File

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

View File

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

View File

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

View File

@ -3,10 +3,10 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>{% block title %}JEDI{% end %}</title> <title>{% block title %}JEDI{% endblock %}</title>
{% for url in assets['css'].urls() %} {% assets "css" %}
<link rel="stylesheet" href="{{ url }}" type="text/css"> <link rel="stylesheet" href="{{ ASSET_URL }}" type="text/css">
{% end %} {% endassets %}
<link rel="icon" type="image/x-icon" href="/static/img/favicon.ico"> <link rel="icon" type="image/x-icon" href="/static/img/favicon.ico">
</head> </head>
<body> <body>
@ -17,11 +17,11 @@
<h1 class="usa-display">JEDI</h1> <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> <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> <a class="usa-button usa-button-secondary" href='/login-dev'><span>DEV Login</span></a>
{% end %} {% endif %}
</main> </main>

View File

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

View File

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

View File

@ -1,44 +1,72 @@
import os
import pytest import pytest
import alembic.config
import alembic.command
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, scoped_session from sqlalchemy.orm import sessionmaker, scoped_session
from atst.app import make_app, make_deps, make_config from atst.app import make_app, make_config
from atst.database import make_db
from tests.mocks import MockApiClient from tests.mocks import MockApiClient
from atst.sessions import DictSessions from atst.sessions import DictSessions
from atst.models import Base
from atst.database import db as _db
@pytest.fixture @pytest.fixture(scope='session')
def app(db): def app(request):
TEST_DEPS = {
"authnid_client": MockApiClient("authnid"),
"sessions": DictSessions(),
"db_session": db
}
config = make_config() 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 apply_migrations():
def db(): """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. @pytest.fixture(scope='session')
# Inspiration: https://docs.sqlalchemy.org/en/latest/orm/session_transaction.html#session-external-transaction def db(app, request):
config = make_config()
database = make_db(config) def teardown():
connection = database.get_bind().connect() _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() 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() transaction.rollback()
connection.close() connection.close()
session.remove()
class DummyForm(dict): class DummyForm(dict):

View File

@ -6,36 +6,32 @@ from atst.domain.pe_numbers import PENumbers
from tests.factories import PENumberFactory from tests.factories import PENumberFactory
@pytest.fixture()
def pe_numbers(db):
return PENumbers(db)
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def new_pe_number(db): def new_pe_number(session):
def make_pe_number(**kwargs): def make_pe_number(**kwargs):
pen = PENumberFactory.create(**kwargs) pen = PENumberFactory.create(**kwargs)
db.add(pen) session.add(pen)
db.commit() session.commit()
return pen return pen
return make_pe_number 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") 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 assert pen.number == new_pen.number
def test_nonexistent_pe_number_raises(pe_numbers): def test_nonexistent_pe_number_raises():
with pytest.raises(NotFoundError): 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']] 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 PENumbers.get(pen_list[0][0])
assert pe_numbers.get(pen_list[1][0]) assert PENumbers.get(pen_list[1][0])

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,14 @@
import pytest import pytest
import tornado
from tests.mocks import MOCK_USER 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 @pytest.mark.skip()
def test_stepthrough_request_form(monkeypatch, http_client, base_url): def test_stepthrough_request_form(monkeypatch, screens, client):
monkeypatch.setattr( monkeypatch.setattr(
"atst.handlers.request_new.RequestNew.get_current_user", lambda s: MOCK_USER "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 "atst.handlers.request_new.JEDIRequestFlow.validate", lambda s: True
) )
@tornado.gen.coroutine
def take_a_step(inc, req=None): def take_a_step(inc, req=None):
req_url = base_url + "/requests/new/{}".format(inc) req_url = "/requests/new/{}".format(inc)
if req: if req:
req_url += "/" + req req_url += "/" + req
response = yield http_client.fetch( response = client.post(
req_url, req_url,
method="POST",
headers={"Content-Type": "application/x-www-form-urlencoded"}, headers={"Content-Type": "application/x-www-form-urlencoded"},
body="meaning=42", data="meaning=42",
) )
return response return response
# GET the initial form # GET the initial form
response = yield http_client.fetch(base_url + "/requests/new", method="GET") response = client.get("/requests/new")
assert SCREENS[0]["title"] in response.body.decode() assert screens[0]["title"] in response.data.decode()
# POST to each of the form pages up until review and submit # POST to each of the form pages up until review and submit
req_id = None req_id = None
for i in range(1, len(SCREENS)): for i in range(1, len(screens)):
resp = yield take_a_step(i, req=req_id) resp = take_a_step(i, req=req_id)
__import__('ipdb').set_trace()
req_id = resp.effective_url.split("/")[-1] 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 "/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 import pytest
@pytest.mark.gen_test def test_routes(client):
def test_routes(http_client, base_url):
for path in ( for path in (
"/", "/",
"/home", "/home",
"/workspaces", "/workspaces",
"/requests", "/requests",
"/requests/new", "/requests/new",
"/requests/new/1", "/requests/new/2",
"/users", "/users",
"/reports", "/reports",
"/calculator", "/calculator",
): ):
response = yield http_client.fetch(base_url + path) response = client.get(path)
assert response.code == 200 if response.status_code == 404:
__import__('ipdb').set_trace()
assert response.status_code == 200