Merge pull request #80 from dod-ccpo/158874759-validate-pe-number

Validate PE number
This commit is contained in:
patricksmithdds 2018-07-20 14:11:21 -04:00 committed by GitHub
commit ef1f2224e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 243 additions and 44 deletions

View File

@ -58,19 +58,31 @@ def make_app(config, deps, **kwargs):
url(
r"/requests/new",
RequestNew,
{"page": "requests_new", "requests_client": deps["requests_client"]},
{
"page": "requests_new",
"requests_client": deps["requests_client"],
"fundz_client": deps["fundz_client"],
},
name="request_new",
),
url(
r"/requests/new/([0-9])",
RequestNew,
{"page": "requests_new", "requests_client": deps["requests_client"]},
{
"page": "requests_new",
"requests_client": deps["requests_client"],
"fundz_client": deps["fundz_client"],
},
name="request_form_new",
),
url(
r"/requests/new/([0-9])/(\S+)",
RequestNew,
{"page": "requests_new", "requests_client": deps["requests_client"]},
{
"page": "requests_new",
"requests_client": deps["requests_client"],
"fundz_client": deps["fundz_client"],
},
name="request_form_update",
),
url(
@ -128,6 +140,10 @@ def make_deps(config):
api_version="v1",
validate_cert=validate_cert,
),
"fundz_client": ApiClient(
config["default"]["FUNDZ_BASE_URL"],
validate_cert=validate_cert,
),
"requests_client": ApiClient(
config["default"]["REQUESTS_QUEUE_BASE_URL"],
api_version="v1",

View File

@ -1,12 +1,40 @@
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 wtforms_tornado import Form
from .fields import NewlineListField
from .forms import ValidatedForm
class FinancialForm(Form):
@tornado.gen.coroutine
def validate_pe_id(field, existing_request, fundz_client):
response = yield fundz_client.get(
"/pe-number/{}".format(field.data),
raise_error=False,
)
if not response.ok:
field.errors.append(
"We couldn't find that PE number, but if you have double checked "
"it you can submit anyway. Your request will need to go through a "
"manual review."
)
return False
return True
class FinancialForm(ValidatedForm):
@tornado.gen.coroutine
def perform_extra_validation(self, existing_request, fundz_client):
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, fundz_client)
raise Return(valid)
task_order_id = StringField(
"Task Order Number associated with this request.", validators=[Required()]
)

12
atst/forms/forms.py Normal file
View File

@ -0,0 +1,12 @@
import tornado
from tornado.gen import Return
from wtforms_tornado import Form
class ValidatedForm(Form):
@tornado.gen.coroutine
def perform_extra_validation(self, *args, **kwargs):
"""A coroutine that performs any applicable extra validation. Must
return True if the form is valid or False otherwise."""
raise Return(True)

View File

@ -1,13 +1,13 @@
from wtforms.fields.html5 import EmailField, TelField
from wtforms.fields import RadioField, StringField
from wtforms.validators import Required, Email
from wtforms_tornado import Form
import pendulum
from .fields import DateField
from .forms import ValidatedForm
from .validators import DateRange, PhoneNumber, Alphabet
class OrgForm(Form):
class OrgForm(ValidatedForm):
fname_request = StringField("First Name", validators=[Required(), Alphabet()])
lname_request = StringField("Last Name", validators=[Required(), Alphabet()])

View File

@ -1,10 +1,10 @@
from wtforms.fields import StringField
from wtforms.validators import Required, Email, Length
from wtforms_tornado import Form
from .forms import ValidatedForm
from .validators import IsNumber, Alphabet
class POCForm(Form):
class POCForm(ValidatedForm):
fname_poc = StringField("POC First Name", validators=[Required(), Alphabet()])
lname_poc = StringField("POC Last Name", validators=[Required(), Alphabet()])

View File

@ -1,13 +1,13 @@
from wtforms.fields.html5 import IntegerField
from wtforms.fields import RadioField, StringField, TextAreaField
from wtforms.validators import NumberRange, InputRequired
from wtforms_tornado import Form
from .fields import DateField
from .forms import ValidatedForm
from .validators import DateRange
import pendulum
class RequestForm(Form):
class RequestForm(ValidatedForm):
# Details of Use: Overall Request Details
dollar_value = IntegerField(

View File

@ -1,6 +1,7 @@
from wtforms.fields import BooleanField
from wtforms_tornado import Form
from .forms import ValidatedForm
class ReviewForm(Form):
class ReviewForm(ValidatedForm):
reviewed = BooleanField("I have reviewed this data and it is correct.")

View File

@ -10,9 +10,17 @@ from atst.forms.financial import FinancialForm
class RequestNew(BaseHandler):
def initialize(self, page, requests_client):
def initialize(self, page, requests_client, fundz_client):
self.page = page
self.requests_client = requests_client
self.fundz_client = fundz_client
@tornado.gen.coroutine
def get_existing_request(self, request_id):
if request_id is None:
return {}
request = yield self.requests_client.get("/requests/{}".format(request_id))
return request.json
@tornado.web.authenticated
@tornado.gen.coroutine
@ -20,29 +28,51 @@ class RequestNew(BaseHandler):
self.check_xsrf_cookie()
screen = int(screen)
post_data = self.request.arguments
current_user = self.get_current_user()
existing_request = yield self.get_existing_request(request_id)
jedi_flow = JEDIRequestFlow(
self.requests_client, screen, post_data=post_data, request_id=request_id
self.requests_client,
self.fundz_client,
screen,
post_data=post_data,
request_id=request_id,
current_user=current_user,
existing_request=existing_request,
)
rerender_args = dict(
f=jedi_flow.form,
data=post_data,
page=self.page,
screens=jedi_flow.screens,
current=screen,
next_screen=jedi_flow.next_screen,
request_id=jedi_flow.request_id,
)
if jedi_flow.validate():
response = yield jedi_flow.create_or_update_request(self.get_current_user())
response = yield jedi_flow.create_or_update_request()
if response.ok:
where = self.application.default_router.reverse_url(
"request_form_update", str(screen + 1), jedi_flow.request_id
)
self.redirect(where)
valid = yield jedi_flow.validate_warnings()
if valid:
if jedi_flow.next_screen >= len(jedi_flow.screens):
where = "/requests"
else:
where = self.application.default_router.reverse_url(
"request_form_update", jedi_flow.next_screen, jedi_flow.request_id
)
self.redirect(where)
else:
self.render(
"requests/screen-%d.html.to" % int(screen),
**rerender_args
)
else:
self.set_status(response.code)
else:
self.render(
"requests/screen-%d.html.to" % int(screen),
f=jedi_flow.form,
data=post_data,
page=self.page,
screens=jedi_flow.screens,
current=screen,
next_screen=jedi_flow.next_screen,
request_id=jedi_flow.request_id,
**rerender_args
)
@tornado.web.authenticated
@ -60,7 +90,7 @@ class RequestNew(BaseHandler):
request = response.json
jedi_flow = JEDIRequestFlow(
self.requests_client, screen, request, request_id=request_id
self.requests_client, self.fundz_client, screen, request, request_id=request_id
)
self.render(
@ -80,12 +110,16 @@ class JEDIRequestFlow(object):
def __init__(
self,
requests_client,
fundz_client,
current_step,
request=None,
post_data=None,
request_id=None,
current_user=None,
existing_request=None,
):
self.requests_client = requests_client
self.fundz_client = fundz_client
self.current_step = current_step
self.request = request
@ -96,6 +130,9 @@ class JEDIRequestFlow(object):
self.request_id = request_id
self.form = self._form()
self.current_user = current_user
self.existing_request = existing_request
def _form(self):
if self.is_post:
return self.form_class()(self.post_data)
@ -107,6 +144,14 @@ class JEDIRequestFlow(object):
def validate(self):
return self.form.validate()
@tornado.gen.coroutine
def validate_warnings(self):
valid = yield self.form.perform_extra_validation(
self.existing_request.get('body', {}).get(self.form_section),
self.fundz_client,
)
return valid
@property
def current_screen(self):
return self.screens[self.current_step - 1]
@ -185,9 +230,9 @@ class JEDIRequestFlow(object):
]
@tornado.gen.coroutine
def create_or_update_request(self, user):
def create_or_update_request(self):
request_data = {
"creator_id": user["id"],
"creator_id": self.current_user["id"],
"request": {self.form_section: self.form.data},
}
if self.request_id:

View File

@ -4,6 +4,7 @@ ENVIRONMENT = dev
DEBUG = true
AUTHZ_BASE_URL = http://localhost:8002
AUTHNID_BASE_URL= https://localhost:8001
FUNDZ_BASE_URL= http://localhost:8004
COOKIE_SECRET = some-secret-please-replace
SECRET = change_me_into_something_secret
CAC_URL = https://localhost:8001

View File

@ -1,7 +1,7 @@
import pytest
from atst.app import make_app, make_deps, make_config
from tests.mocks import MockApiClient, MockRequestsClient, MockAuthzClient
from tests.mocks import MockApiClient, MockFundzClient, MockRequestsClient, MockAuthzClient
from atst.sessions import DictSessions
@ -11,6 +11,7 @@ def app():
"authz_client": MockAuthzClient("authz"),
"requests_client": MockRequestsClient("requests"),
"authnid_client": MockApiClient("authnid"),
"fundz_client": MockFundzClient("fundz"),
"sessions": DictSessions(),
}

View File

@ -1,5 +1,8 @@
import re
import pytest
import tornado
import urllib
from tests.mocks import MOCK_REQUEST, MOCK_VALID_PE_ID
ERROR_CLASS = "usa-input-error-message"
MOCK_USER = {
@ -47,3 +50,82 @@ def test_submit_valid_request_form(monkeypatch, http_client, base_url):
body="meaning=42",
)
assert "/requests/new/2" in response.effective_url
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_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)
@tornado.gen.coroutine
def submit_data(self, http_client, base_url, data):
response = yield http_client.fetch(
base_url + "/requests/new/5/{}".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/new/5" 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"
@pytest.mark.gen_test
def test_submit_request_form_with_new_valid_pe_id(self, monkeypatch, http_client, base_url):
self._set_monkeypatches(monkeypatch)
data = dict(self.required_data)
data['pe_id'] = MOCK_VALID_PE_ID
response = yield self.submit_data(http_client, base_url, data)
assert response.code == 302
assert response.headers.get("Location") == "/requests"

View File

@ -43,26 +43,39 @@ class MockApiClient(ApiClient):
return response
MOCK_REQUEST = {
"id": "66b8ef71-86d3-48ef-abc2-51bfa1732b6b",
"creator": "49903ae7-da4a-49bf-a6dc-9dff5d004238",
"body": {
"financial_verification": {
"pe_id": "0203752A",
},
},
"status": "incomplete"
}
class MockRequestsClient(MockApiClient):
@tornado.gen.coroutine
def get(self, path, **kwargs):
json = {
"id": "66b8ef71-86d3-48ef-abc2-51bfa1732b6b",
"creator": "49903ae7-da4a-49bf-a6dc-9dff5d004238",
"body": {},
"status": "incomplete",
}
return self._get_response("GET", path, 200, json=json)
return self._get_response("GET", path, 200, json=MOCK_REQUEST)
@tornado.gen.coroutine
def post(self, path, **kwargs):
json = {
"id": "66b8ef71-86d3-48ef-abc2-51bfa1732b6b",
"creator": "49903ae7-da4a-49bf-a6dc-9dff5d004238",
"body": {},
}
return self._get_response("POST", path, 202, json=json)
return self._get_response("POST", path, 202, json=MOCK_REQUEST)
MOCK_VALID_PE_ID = "8675309U"
class MockFundzClient(MockApiClient):
@tornado.gen.coroutine
def get(self, path, **kwargs):
if path.endswith(MOCK_VALID_PE_ID):
return self._get_response("GET", path, 200)
else:
return self._get_response("GET", path, 404)
class MockAuthzClient(MockApiClient):