def test_missingRequiredParameter(self): # type: () -> None """ If required fields are missing, a default error form is presented and the form's handler is not called. """ mem = MemorySessionStore() session = self.successResultOf( mem.newSession(True, SessionMechanism.Header)) to = TestObject(mem) stub = StubTreq(to.router.resource()) response = self.successResultOf( stub.post( "https://localhost/handle", data=dict(), headers={b"X-Test-Session": session.identifier}, )) self.assertEqual(response.code, 400) self.assertIn( b"a value was required but none was supplied", self.successResultOf(content(response)), ) self.assertEqual(to.calls, [])
def test_numberConstraints(self): # type: () -> None """ Number parameters have minimum and maximum validations and the object will not be called when the values exceed them. """ mem = MemorySessionStore() session = self.successResultOf( mem.newSession(True, SessionMechanism.Header)) to = TestObject(mem) stub = StubTreq(to.router.resource()) tooLow = self.successResultOf( stub.post('https://localhost/constrained', data=dict(goldilocks='1'), headers={b'X-Test-Session': session.identifier})) tooHigh = self.successResultOf( stub.post('https://localhost/constrained', data=dict(goldilocks='20'), headers={b'X-Test-Session': session.identifier})) justRight = self.successResultOf( stub.post('https://localhost/constrained', data=dict(goldilocks='7'), headers={b'X-Test-Session': session.identifier})) self.assertEqual(tooHigh.code, 400) self.assertEqual(tooLow.code, 400) self.assertEqual(justRight.code, 200) self.assertEqual(self.successResultOf(content(justRight)), b'got it') self.assertEqual(to.calls, [(u'constrained', 7)])
def test_missingOptionalParameterJSON(self): # type: () -> None """ If a required Field is missing from the JSON body, its default value is used. """ mem = MemorySessionStore() session = self.successResultOf( mem.newSession(True, SessionMechanism.Header)) to = TestObject(mem) stub = StubTreq(to.router.resource()) response = self.successResultOf( stub.post( "https://localhost/notrequired", json=dict(name="one"), headers={b"X-Test-Session": session.identifier}, )) response2 = self.successResultOf( stub.post( "https://localhost/notrequired", json=dict(name="two", value=2), headers={b"X-Test-Session": session.identifier}, )) self.assertEqual(response.code, 200) self.assertEqual(response2.code, 200) self.assertEqual(self.successResultOf(content(response)), b"okay") self.assertEqual(self.successResultOf(content(response2)), b"okay") self.assertEqual(to.calls, [("one", 7.0), ("two", 2.0)])
def test_renderingFormGlue(self): # type: () -> None """ When a form renderer renders just the glue, none of the rest of the form is included. """ mem = MemorySessionStore() session = self.successResultOf( mem.newSession(True, SessionMechanism.Header)) stub = StubTreq(TestObject(mem).router.resource()) response = self.successResultOf( stub.get( "https://localhost/render-custom", headers={b"X-Test-Session": session.identifier}, )) self.assertEqual(response.code, 200) self.assertIn( response.headers.getRawHeaders(b"content-type")[0], b"text/html") responseDom = ElementTree.fromstring( self.successResultOf(content(response))) submitButton = responseDom.findall(".//*[@type='submit']") self.assertEqual(len(submitButton), 0) protectionField = responseDom.findall( ".//*[@name='__csrf_protection__']") self.assertEqual(protectionField[0].attrib["value"], session.identifier)
def test_rendering(self): # type: () -> None """ When a route requires form fields, it renders a form with those fields. """ mem = MemorySessionStore() session = self.successResultOf( mem.newSession(True, SessionMechanism.Header)) stub = StubTreq(TestObject(mem).router.resource()) response = self.successResultOf( stub.get( "https://localhost/render", headers={b"X-Test-Session": session.identifier}, )) self.assertEqual(response.code, 200) self.assertIn( response.headers.getRawHeaders(b"content-type")[0], b"text/html") responseDom = ElementTree.fromstring( self.successResultOf(content(response))) submitButton = responseDom.findall(".//*[@type='submit']") self.assertEqual(len(submitButton), 1) self.assertEqual(submitButton[0].attrib["name"], "__klein_auto_submit__")
def test_renderingEmptyForm(self): # type: () -> None """ When a form renderer specifies a submit button, no automatic submit button is rendered. """ mem = MemorySessionStore() session = self.successResultOf( mem.newSession(True, SessionMechanism.Header)) stub = StubTreq(TestObject(mem).router.resource()) response = self.successResultOf( stub.get( "https://localhost/render-empty", headers={b"X-Test-Session": session.identifier}, )) self.assertEqual(response.code, 200) self.assertIn( response.headers.getRawHeaders(b"content-type")[0], b"text/html") responseDom = ElementTree.fromstring( self.successResultOf(content(response))) submitButton = responseDom.findall(".//*[@type='submit']") self.assertEqual(len(submitButton), 1) self.assertEqual(submitButton[0].attrib["name"], "__klein_auto_submit__") protectionField = responseDom.findall( ".//*[@name='__csrf_protection__']") self.assertEqual(protectionField[0].attrib["value"], session.identifier)
def test_customValidationHandling(self): # type: () -> None """ L{Form.onValidationFailureFor} handles form validation failures by handing its thing a renderable form. """ mem = MemorySessionStore() session = self.successResultOf( mem.newSession(True, SessionMechanism.Header)) testobj = TestObject(mem) stub = StubTreq(testobj.router.resource()) response = self.successResultOf( stub.post( "https://localhost/handle-validation", headers={b"X-Test-Session": session.identifier}, json={"value": 300}, )) self.assertEqual(response.code, 200) self.assertIn( response.headers.getRawHeaders(b"content-type")[0], b"text/html") responseText = self.successResultOf(content(response)) self.assertEqual(responseText, b"~special~") self.assertEqual( [(k.pythonArgumentName, v) for k, v in testobj.calls[-1][1].prevalidationValues.items()], [("value", 300)], )
def test_cookieWithToken(self): # type: () -> None """ A cookie-authenticated, CRSF-protected form will call the form as expected. """ mem = MemorySessionStore() session = self.successResultOf( mem.newSession(True, SessionMechanism.Cookie)) to = TestObject(mem) stub = StubTreq(to.router.resource()) response = self.successResultOf( stub.post( "https://localhost/handle", data=dict( name="hello", value="1234", ignoreme="extraneous", __csrf_protection__=session.identifier, ), cookies={ "Klein-Secure-Session": nativeString(session.identifier) }, )) self.assertEqual(to.calls, [("hello", 1234)]) self.assertEqual(response.code, 200) self.assertIn(b"yay", self.successResultOf(content(response)))
def test_noAuthorizers(self) -> None: """ By default, L{MemorySessionStore} contains no authorizers and the sessions it returns will authorize any supplied interfaces as None. """ store = MemorySessionStore() session = self.successResultOf( store.newSession(True, SessionMechanism.Header)) self.assertEqual(self.successResultOf(session.authorize([IFoo, IBar])), {})
def test_interfaceCompliance(self): # type: () -> None """ Verify that the session store complies with the relevant interfaces. """ store = MemorySessionStore() verifyObject(ISessionStore, store) # type: ignore[misc] verifyObject( ISession, self.successResultOf( # type: ignore[misc] store.newSession(True, SessionMechanism.Header)))
def test_simpleAuthorization(self): # type: () -> None """ L{MemorySessionStore.fromAuthorizers} takes a set of functions decorated with L{declareMemoryAuthorizer} and constructs a session store that can authorize for those interfaces. """ @declareMemoryAuthorizer(IFoo) def fooMe(interface, session, componentized): # type: (Any, Any, Any) -> int return 1 @declareMemoryAuthorizer(IBar) def barMe(interface, session, componentized): # type: (Any, Any, Any) -> int return 2 store = MemorySessionStore.fromAuthorizers([fooMe, barMe]) session = self.successResultOf( store.newSession(False, SessionMechanism.Cookie)) self.assertEqual(self.successResultOf(session.authorize([IBar, IFoo])), { IFoo: 1, IBar: 2 })
def test_procurementSecurity(self) -> None: """ Once a session is negotiated, it should be the identical object to avoid duplicate work - unless we are using forceInsecure to retrieve the insecure session from a secure request, in which case the result should not be cached. """ sessions = [] mss = MemorySessionStore() router = Klein() @router.route("/") @inlineCallbacks def route(request: IRequest) -> Deferred: sproc = SessionProcurer(mss) sessions.append((yield sproc.procureSession(request))) sessions.append((yield sproc.procureSession(request))) sessions.append((yield sproc.procureSession(request, forceInsecure=True))) returnValue(b"sessioned") treq = StubTreq(router.resource()) self.successResultOf(treq.get("http://unittest.example.com/")) self.assertIs(sessions[0], sessions[1]) self.assertIs(sessions[0], sessions[2]) self.successResultOf(treq.get("https://unittest.example.com/")) self.assertIs(sessions[3], sessions[4]) self.assertIsNot(sessions[3], sessions[5])
def test_cookieNoToken(self): # type: () -> None """ A cookie-authenticated, CSRF-protected form will return a 403 Forbidden status code when a CSRF protection token is not supplied. """ mem = MemorySessionStore() session = self.successResultOf( mem.newSession(True, SessionMechanism.Cookie) ) to = TestObject(mem) stub = StubTreq(to.router.resource()) response = self.successResultOf(stub.post( 'https://localhost/handle', data=dict(name='hello', value='1234', ignoreme='extraneous'), cookies={"Klein-Secure-Session": nativeString(session.identifier)} )) self.assertEqual(to.calls, []) self.assertEqual(response.code, 403) self.assertIn(b'CSRF', self.successResultOf(content(response)))
def test_noName(self): # type: () -> None """ A handler for a Form with a Field that doesn't have a name will return an error explaining the problem. """ mem = MemorySessionStore() session = self.successResultOf( mem.newSession(True, SessionMechanism.Header)) to = TestObject(mem) stub = StubTreq(to.router.resource()) response = self.successResultOf( stub.post('https://localhost/dangling-param', data=dict(), headers={b'X-Test-Session': session.identifier})) self.assertEqual(response.code, 500) errors = self.flushLoggedErrors(ValueError) self.assertEqual(len(errors), 1) self.assertIn(str(errors[0].value), "Cannot extract unnamed form field.")
def test_renderLookupError(self): # type: () -> None """ RenderableForm raises L{MissingRenderMethod} if anything attempst to look up a render method on it. """ mem = MemorySessionStore() session = self.successResultOf( mem.newSession(True, SessionMechanism.Header)) stub = StubTreq(TestObject(mem).router.resource()) response = self.successResultOf( stub.get('https://localhost/render-cascade', headers={b'X-Test-Session': session.identifier})) self.assertEqual(response.code, 200) # print(self.successResultOf(response.content()).decode('utf-8')) failures = self.flushLoggedErrors() self.assertEqual(len(failures), 1) self.assertIn("MissingRenderMethod", str(failures[0]))
def test_handlingPassword(self): # type: () -> None """ From the perspective of form handling, passwords are handled like strings. """ mem = MemorySessionStore() session = self.successResultOf( mem.newSession(True, SessionMechanism.Header)) to = TestObject(mem) stub = StubTreq(to.router.resource()) response = self.successResultOf( stub.post('https://localhost/password-field', data=dict(pw='asdfjkl;'), headers={b'X-Test-Session': session.identifier})) self.assertEqual(response.code, 200) self.assertEqual(self.successResultOf(content(response)), b'password received') self.assertEqual(to.calls, [(u'password', u'asdfjkl;')])
def test_handlingJSON(self) -> None: """ A handler for a form with Fields receives those fields as input, as passed by an HTTP client that submits a JSON POST body. """ mem = MemorySessionStore() session = self.successResultOf( mem.newSession(True, SessionMechanism.Header)) to = TestObject(mem) stub = StubTreq(to.router.resource()) response = self.successResultOf( stub.post( "https://localhost/handle", json=dict(name="hello", value="1234", ignoreme="extraneous"), headers={"X-Test-Session": session.identifier}, )) self.assertEqual(response.code, 200) self.assertEqual(self.successResultOf(content(response)), b"yay") self.assertEqual(to.calls, [("hello", 1234)])
def test_renderingWithNoSessionYet(self) -> None: """ When a route is rendered with no session, it sets a cookie to establish a new session. """ mem = MemorySessionStore() stub = StubTreq(TestObject(mem).router.resource()) response = self.successResultOf(stub.get("https://localhost/render")) self.assertEqual(response.code, 200) setCookie = response.cookies()["Klein-Secure-Session"] expected = f'value="{setCookie}"' actual = self.successResultOf(content(response)).decode("utf-8") self.assertIn(expected, actual)
def test_handling(self): # type: () -> None """ A handler for a Form with Fields receives those fields as input, as passed by an HTTP client. """ mem = MemorySessionStore() session = self.successResultOf( mem.newSession(True, SessionMechanism.Header)) to = TestObject(mem) stub = StubTreq(to.router.resource()) response = self.successResultOf( stub.post('https://localhost/handle', data=dict(name='hello', value='1234', ignoreme='extraneous'), headers={b'X-Test-Session': session.identifier})) self.assertEqual(response.code, 200) self.assertEqual(self.successResultOf(content(response)), b'yay') self.assertEqual(to.calls, [(u'hello', 1234)])
def test_renderingExplicitSubmit(self): # type: () -> None """ When a form renderer specifies a submit button, no automatic submit button is rendered. """ mem = MemorySessionStore() session = self.successResultOf( mem.newSession(True, SessionMechanism.Header)) stub = StubTreq(TestObject(mem).router.resource()) response = self.successResultOf( stub.get('https://localhost/render-submit', headers={b'X-Test-Session': session.identifier})) self.assertEqual(response.code, 200) self.assertIn( response.headers.getRawHeaders(b"content-type")[0], b"text/html") responseDom = ElementTree.fromstring( self.successResultOf(content(response))) submitButton = responseDom.findall(".//*[@type='submit']") self.assertEqual(len(submitButton), 1) self.assertEqual(submitButton[0].attrib['name'], 'button')
def test_noSessionPOST(self): # type: () -> None """ An unauthenticated, CSRF-protected form will return a 403 Forbidden status code. """ mem = MemorySessionStore() to = TestObject(mem) stub = StubTreq(to.router.resource()) response = self.successResultOf( stub.post('https://localhost/handle', data=dict(name='hello', value='1234'))) self.assertEqual(to.calls, []) self.assertEqual(response.code, 403) self.assertIn(b'CSRF', self.successResultOf(content(response)))
def test_renderingWithNoSessionYet(self): # type: () -> None """ When a route is rendered with no session, it sets a cookie to establish a new session. """ mem = MemorySessionStore() stub = StubTreq(TestObject(mem).router.resource()) response = self.successResultOf(stub.get("https://localhost/render")) self.assertEqual(response.code, 200) setCookie = response.cookies()["Klein-Secure-Session"] expected = 'value="{}"'.format(setCookie) actual = self.successResultOf(content(response)) if not isinstance(expected, bytes): # type: ignore[unreachable] actual = actual.decode("utf-8") self.assertIn(expected, actual)
def simpleSessionRouter(): # type: () -> Tuple[sessions, errors, str, str, StubTreq] """ Construct a simple router. """ sessions = [] exceptions = [] mss = MemorySessionStore.fromAuthorizers([memoryAuthorizer]) router = Klein() token = "X-Test-Session-Token" cookie = "X-Test-Session-Cookie" sproc = SessionProcurer( mss, secureTokenHeader=b"X-Test-Session-Token", secureCookie=b"X-Test-Session-Cookie", ) @router.route("/") @inlineCallbacks def route(request): # type: (IRequest) -> Deferred try: sessions.append((yield sproc.procureSession(request))) except NoSuchSession as nss: exceptions.append(nss) returnValue(b"ok") requirer = Requirer() @requirer.prerequisite([ISession]) def procure(request): # type: (IRequest) -> Deferred return sproc.procureSession(request) @requirer.require(router.route("/test"), simple=Authorization(ISimpleTest)) def testRoute(simple): # type: (SimpleTest) -> str return "ok: " + str(simple.doTest() + 4) @requirer.require(router.route("/denied"), nope=Authorization(IDenyMe)) def testDenied(nope): # type: (IDenyMe) -> str return "bad" treq = StubTreq(router.resource()) return sessions, exceptions, token, cookie, treq
def test_cookiesTurnedOff(self) -> None: """ If cookies can't be set, then C{procureSession} raises L{NoSuchSession}. """ mss = MemorySessionStore() router = Klein() @router.route("/") @inlineCallbacks def route(request: IRequest) -> Deferred: sproc = SessionProcurer(mss, setCookieOnGET=False) with self.assertRaises(NoSuchSession): yield sproc.procureSession(request) returnValue(b"no session") treq = StubTreq(router.resource()) result = self.successResultOf(treq.get("http://unittest.example.com/")) self.assertEqual(self.successResultOf(result.content()), b"no session")
def test_procuredTooLate(self) -> None: """ If you start writing stuff to the response before procuring the session, when cookies need to be set, you will get a comprehensible error. """ mss = MemorySessionStore() router = Klein() @router.route("/") @inlineCallbacks def route(request: IRequest) -> Deferred: sproc = SessionProcurer(mss) request.write(b"oops...") with self.assertRaises(TooLateForCookies): yield sproc.procureSession(request) request.write(b"bye") request.finish() treq = StubTreq(router.resource()) result = self.successResultOf(treq.get("http://unittest.example.com/")) self.assertEqual(self.successResultOf(result.content()), b"oops...bye")
from twisted.web.template import slot, tags from klein import Field, Form, Klein, Plating, Requirer, SessionProcurer from klein.interfaces import ISession from klein.storage.memory import MemorySessionStore app = Klein() sessions = MemorySessionStore() requirer = Requirer() @requirer.prerequisite([ISession]) def procurer(request): return SessionProcurer(sessions).procureSession(request) style = Plating(tags=tags.html(tags.head(tags.title("yay")), tags.body(tags.div(slot(Plating.CONTENT))))) @requirer.require( style.routed( app.route("/", methods=["POST"]), tags.h1("u did it: ", slot("an-form-arg")), ), foo=Field.number(minimum=3, maximum=10), bar=Field.text(), ) def postHandler(foo, bar):