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( url(
r"/requests/new", r"/requests/new",
RequestNew, 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", name="request_new",
), ),
url( url(
r"/requests/new/([0-9])", r"/requests/new/([0-9])",
RequestNew, 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", name="request_form_new",
), ),
url( url(
r"/requests/new/([0-9])/(\S+)", r"/requests/new/([0-9])/(\S+)",
RequestNew, 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", name="request_form_update",
), ),
url( url(
@ -128,6 +140,10 @@ def make_deps(config):
api_version="v1", api_version="v1",
validate_cert=validate_cert, validate_cert=validate_cert,
), ),
"fundz_client": ApiClient(
config["default"]["FUNDZ_BASE_URL"],
validate_cert=validate_cert,
),
"requests_client": ApiClient( "requests_client": ApiClient(
config["default"]["REQUESTS_QUEUE_BASE_URL"], config["default"]["REQUESTS_QUEUE_BASE_URL"],
api_version="v1", 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.html5 import EmailField
from wtforms.fields import StringField, SelectField from wtforms.fields import StringField, SelectField
from wtforms.form import Form
from wtforms.validators import Required, Email from wtforms.validators import Required, Email
from wtforms_tornado import Form
from .fields import NewlineListField 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_id = StringField(
"Task Order Number associated with this request.", validators=[Required()] "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.html5 import EmailField, TelField
from wtforms.fields import RadioField, StringField from wtforms.fields import RadioField, StringField
from wtforms.validators import Required, Email from wtforms.validators import Required, Email
from wtforms_tornado import Form
import pendulum import pendulum
from .fields import DateField from .fields import DateField
from .forms import ValidatedForm
from .validators import DateRange, PhoneNumber, Alphabet from .validators import DateRange, PhoneNumber, Alphabet
class OrgForm(Form): class OrgForm(ValidatedForm):
fname_request = StringField("First Name", validators=[Required(), Alphabet()]) fname_request = StringField("First Name", validators=[Required(), Alphabet()])
lname_request = StringField("Last 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.fields import StringField
from wtforms.validators import Required, Email, Length from wtforms.validators import Required, Email, Length
from wtforms_tornado import Form from .forms import ValidatedForm
from .validators import IsNumber, Alphabet from .validators import IsNumber, Alphabet
class POCForm(Form): class POCForm(ValidatedForm):
fname_poc = StringField("POC First Name", validators=[Required(), Alphabet()]) fname_poc = StringField("POC First Name", validators=[Required(), Alphabet()])
lname_poc = StringField("POC Last 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.html5 import IntegerField
from wtforms.fields import RadioField, StringField, TextAreaField from wtforms.fields import RadioField, StringField, TextAreaField
from wtforms.validators import NumberRange, InputRequired from wtforms.validators import NumberRange, InputRequired
from wtforms_tornado import Form
from .fields import DateField from .fields import DateField
from .forms import ValidatedForm
from .validators import DateRange from .validators import DateRange
import pendulum import pendulum
class RequestForm(Form): class RequestForm(ValidatedForm):
# Details of Use: Overall Request Details # Details of Use: Overall Request Details
dollar_value = IntegerField( dollar_value = IntegerField(

View File

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

View File

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

View File

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

View File

@ -1,5 +1,8 @@
import re import re
import pytest import pytest
import tornado
import urllib
from tests.mocks import MOCK_REQUEST, MOCK_VALID_PE_ID
ERROR_CLASS = "usa-input-error-message" ERROR_CLASS = "usa-input-error-message"
MOCK_USER = { MOCK_USER = {
@ -47,3 +50,82 @@ def test_submit_valid_request_form(monkeypatch, http_client, base_url):
body="meaning=42", body="meaning=42",
) )
assert "/requests/new/2" in response.effective_url 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 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): class MockRequestsClient(MockApiClient):
@tornado.gen.coroutine @tornado.gen.coroutine
def get(self, path, **kwargs): def get(self, path, **kwargs):
json = { return self._get_response("GET", path, 200, json=MOCK_REQUEST)
"id": "66b8ef71-86d3-48ef-abc2-51bfa1732b6b",
"creator": "49903ae7-da4a-49bf-a6dc-9dff5d004238",
"body": {},
"status": "incomplete",
}
return self._get_response("GET", path, 200, json=json)
@tornado.gen.coroutine @tornado.gen.coroutine
def post(self, path, **kwargs): def post(self, path, **kwargs):
json = { return self._get_response("POST", path, 202, json=MOCK_REQUEST)
"id": "66b8ef71-86d3-48ef-abc2-51bfa1732b6b",
"creator": "49903ae7-da4a-49bf-a6dc-9dff5d004238",
"body": {}, MOCK_VALID_PE_ID = "8675309U"
}
return self._get_response("POST", path, 202, json=json)
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): class MockAuthzClient(MockApiClient):