--- a/.hgtags Thu Sep 25 15:49:13 2014 +0200
+++ b/.hgtags Fri Oct 17 18:16:58 2014 +0200
@@ -371,3 +371,9 @@
37f7c60f89f13dfcf326a4ea0a98ca20d959f7bd cubicweb-version-3.19.3
37f7c60f89f13dfcf326a4ea0a98ca20d959f7bd cubicweb-debian-version-3.19.3-1
37f7c60f89f13dfcf326a4ea0a98ca20d959f7bd cubicweb-centos-version-3.19.3-1
+c4e740e50fc7d371d14df17d26bc42d1f8060261 cubicweb-version-3.19.4
+c4e740e50fc7d371d14df17d26bc42d1f8060261 cubicweb-debian-version-3.19.4-1
+c4e740e50fc7d371d14df17d26bc42d1f8060261 cubicweb-centos-version-3.19.4-1
+3ac86df519af2a1194cb3fc882d30d0e1bf44e3b cubicweb-version-3.19.5
+3ac86df519af2a1194cb3fc882d30d0e1bf44e3b cubicweb-debian-version-3.19.5-1
+3ac86df519af2a1194cb3fc882d30d0e1bf44e3b cubicweb-centos-version-3.19.5-1
--- a/__pkginfo__.py Thu Sep 25 15:49:13 2014 +0200
+++ b/__pkginfo__.py Fri Oct 17 18:16:58 2014 +0200
@@ -22,7 +22,7 @@
modname = distname = "cubicweb"
-numversion = (3, 19, 4)
+numversion = (3, 19, 5)
version = '.'.join(str(num) for num in numversion)
description = "a repository of entities / relations for knowledge management"
@@ -56,7 +56,7 @@
__recommends__ = {
'docutils': '>= 0.6',
'Pyro': '>= 3.9.1, < 4.0.0',
- 'PIL': '', # for captcha
+ 'Pillow': '', # for captcha
'pycrypto': '', # for crypto extensions
'fyzz': '>= 0.1.0', # for sparql
'vobject': '>= 0.6.0', # for ical view
--- a/cubicweb.spec Thu Sep 25 15:49:13 2014 +0200
+++ b/cubicweb.spec Fri Oct 17 18:16:58 2014 +0200
@@ -7,7 +7,7 @@
%endif
Name: cubicweb
-Version: 3.19.4
+Version: 3.19.5
Release: logilab.1%{?dist}
Summary: CubicWeb is a semantic web application framework
Source0: http://download.logilab.org/pub/cubicweb/cubicweb-%{version}.tar.gz
--- a/debian/changelog Thu Sep 25 15:49:13 2014 +0200
+++ b/debian/changelog Fri Oct 17 18:16:58 2014 +0200
@@ -1,3 +1,9 @@
+cubicweb (3.19.5-1) unstable; urgency=low
+
+ * new upstream release
+
+ -- Julien Cristau <julien.cristau@logilab.fr> Mon, 06 Oct 2014 17:32:28 +0200
+
cubicweb (3.19.4-1) unstable; urgency=low
* new upstream release
--- a/devtools/qunit.py Thu Sep 25 15:49:13 2014 +0200
+++ b/devtools/qunit.py Fri Oct 17 18:16:58 2014 +0200
@@ -62,34 +62,18 @@
def __init__(self, url=None):
self._process = None
- self._tmp_dir = mkdtemp(prefix='cwtest-ffxprof-')
- self._profile_data = {'uid': uuid4()}
- self._profile_name = self.profile_name_mask % self._profile_data
- stdout = TemporaryFile()
- stderr = TemporaryFile()
+ self._profile_dir = mkdtemp(prefix='cwtest-ffxprof-')
self.firefox_cmd = ['firefox', '-no-remote']
if os.name == 'posix':
self.firefox_cmd = [osp.join(osp.dirname(__file__), 'data', 'xvfb-run.sh'),
'-a', '-s', '-noreset -screen 0 640x480x8'] + self.firefox_cmd
- try:
- home = osp.expanduser('~')
- user = getlogin()
- assert os.access(home, os.W_OK), \
- 'No write access to your home directory, Firefox will crash.'\
- ' Are you sure "%s" is a valid home for user "%s"' % (home, user)
- check_call(self.firefox_cmd + ['-CreateProfile',
- '%s %s' % (self._profile_name, self._tmp_dir)],
- stdout=stdout, stderr=stderr)
- except CalledProcessError as cpe:
- stdout.seek(0)
- stderr.seek(0)
- raise VerboseCalledProcessError(cpe.returncode, cpe.cmd, stdout.read(), stderr.read())
def start(self, url):
self.stop()
- fnull = open(os.devnull, 'w')
- self._process = Popen(self.firefox_cmd + ['-P', self._profile_name, url],
- stdout=fnull, stderr=fnull)
+ cmd = self.firefox_cmd + ['-silent', '--profile', self._profile_dir,
+ '-url', url]
+ with open(os.devnull, 'w') as fnull:
+ self._process = Popen(cmd, stdout=fnull, stderr=fnull)
def stop(self):
if self._process is not None:
@@ -100,7 +84,6 @@
def __del__(self):
self.stop()
- rmtree(self._tmp_dir)
class QUnitTestCase(CubicWebServerTC):
@@ -111,6 +94,7 @@
all_js_tests = ()
def setUp(self):
+ self.config.global_set_option('access-control-allow-origin', '*')
super(QUnitTestCase, self).setUp()
self.test_queue = Queue()
class MyQUnitResultController(QUnitResultController):
@@ -155,7 +139,7 @@
# generate html test file
jquery_dir = 'file://' + self.config.locate_resource('jquery.js')[0]
- html_test_file = NamedTemporaryFile(suffix='.html')
+ html_test_file = NamedTemporaryFile(suffix='.html', delete=False)
html_test_file.write(make_qunit_html(test_file, depends,
base_url=self.config['base-url'],
web_data_path=jquery_dir))
@@ -168,6 +152,12 @@
self.test_queue.get(False)
browser = FirefoxHelper()
+ # start firefox once to let it init the profile (and run system-wide
+ # add-ons post setup, blegh), and then kill it ...
+ browser.start('about:blank')
+ import time; time.sleep(5)
+ browser.stop()
+ # ... then actually run the test file
browser.start(html_test_file.name)
test_count = 0
error = False
@@ -290,8 +280,10 @@
'web_test': cw_path('devtools', 'data'),
}
- html = ['''<html>
+ html = ['''<!DOCTYPE html>
+<html>
<head>
+ <meta http-equiv="content-type" content="application/html; charset=UTF-8"/>
<!-- JS lib used as testing framework -->
<link rel="stylesheet" type="text/css" media="all" href="%(web_test)s/qunit.css" />
<script src="%(web_data)s/jquery.js" type="text/javascript"></script>
@@ -317,7 +309,7 @@
<h1 id="qunit-header">QUnit example</h1>
<h2 id="qunit-banner"></h2>
<h2 id="qunit-userAgent"></h2>
- <ol id="qunit-tests">
+ <ol id="qunit-tests"></ol>
</body>
</html>''')
return u'\n'.join(html)
--- a/i18n/de.po Thu Sep 25 15:49:13 2014 +0200
+++ b/i18n/de.po Fri Oct 17 18:16:58 2014 +0200
@@ -222,9 +222,6 @@
msgid "Added relation : %(entity_from)s %(rtype)s %(entity_to)s"
msgstr ""
-msgid "Any"
-msgstr "irgendein"
-
msgid "Attributes permissions:"
msgstr "Rechte der Attribute"
@@ -1495,12 +1492,6 @@
msgid "components"
msgstr "Komponenten"
-msgid "components_etypenavigation"
-msgstr "nach Typ filtern"
-
-msgid "components_etypenavigation_description"
-msgstr "Erlaubt die Sortierung von Suchergebnissen nach Entitätstyp"
-
msgid "components_navigation"
msgstr "Seitennavigation"
@@ -4459,9 +4450,6 @@
msgid "we are not yet ready to handle this query"
msgstr "Momentan können wir diese sparql-Anfrage noch nicht ausführen."
-msgid "web sessions without CNX"
-msgstr ""
-
msgid "wednesday"
msgstr "Mittwoch"
@@ -4553,9 +4541,18 @@
msgid "you should probably delete that property"
msgstr "Sie sollten diese Eigenschaft wahrscheinlich löschen."
+#~ msgid "Any"
+#~ msgstr "irgendein"
+
#~ msgid "can't connect to source %s, some data may be missing"
#~ msgstr "Keine Verbindung zu der Quelle %s, einige Daten könnten fehlen"
+#~ msgid "components_etypenavigation"
+#~ msgstr "nach Typ filtern"
+
+#~ msgid "components_etypenavigation_description"
+#~ msgstr "Erlaubt die Sortierung von Suchergebnissen nach Entitätstyp"
+
#~ msgid "error while querying source %s, some data may be missing"
#~ msgstr ""
#~ "Fehler beim Zugriff auf Quelle %s, möglicherweise sind die Daten "
--- a/i18n/en.po Thu Sep 25 15:49:13 2014 +0200
+++ b/i18n/en.po Fri Oct 17 18:16:58 2014 +0200
@@ -211,9 +211,6 @@
msgid "Added relation : %(entity_from)s %(rtype)s %(entity_to)s"
msgstr ""
-msgid "Any"
-msgstr ""
-
msgid "Attributes permissions:"
msgstr ""
@@ -1451,12 +1448,6 @@
msgid "components"
msgstr ""
-msgid "components_etypenavigation"
-msgstr "filtering by type"
-
-msgid "components_etypenavigation_description"
-msgstr "permit to filter search results by entity type"
-
msgid "components_navigation"
msgstr "page navigation"
@@ -4350,9 +4341,6 @@
msgid "we are not yet ready to handle this query"
msgstr ""
-msgid "web sessions without CNX"
-msgstr ""
-
msgid "wednesday"
msgstr ""
@@ -4441,3 +4429,9 @@
msgid "you should probably delete that property"
msgstr ""
+
+#~ msgid "components_etypenavigation"
+#~ msgstr "filtering by type"
+
+#~ msgid "components_etypenavigation_description"
+#~ msgstr "permit to filter search results by entity type"
--- a/i18n/es.po Thu Sep 25 15:49:13 2014 +0200
+++ b/i18n/es.po Fri Oct 17 18:16:58 2014 +0200
@@ -230,9 +230,6 @@
msgid "Added relation : %(entity_from)s %(rtype)s %(entity_to)s"
msgstr "Relación agregada : %(entity_from)s %(rtype)s %(entity_to)s"
-msgid "Any"
-msgstr "Cualquiera"
-
msgid "Attributes permissions:"
msgstr "Permisos de atributos:"
@@ -1521,12 +1518,6 @@
msgid "components"
msgstr "Componentes"
-msgid "components_etypenavigation"
-msgstr "Filtar por tipo"
-
-msgid "components_etypenavigation_description"
-msgstr "Permite filtrar por tipo de entidad los resultados de una búsqueda"
-
msgid "components_navigation"
msgstr "Navigación por página"
@@ -4521,9 +4512,6 @@
msgid "we are not yet ready to handle this query"
msgstr "Aún no podemos manejar este tipo de consulta Sparql"
-msgid "web sessions without CNX"
-msgstr "sesiones web sin conexión asociada"
-
msgid "wednesday"
msgstr "Miércoles"
@@ -4619,6 +4607,9 @@
#~ msgid "%s relation should not be in mapped"
#~ msgstr "la relación %s no debería estar mapeada"
+#~ msgid "Any"
+#~ msgstr "Cualquiera"
+
#~ msgid "attribute/relation can't be mapped, only entity and relation types"
#~ msgstr ""
#~ "los atributos y las relaciones no pueden ser mapeados, solamente los "
@@ -4633,6 +4624,12 @@
#~ msgid "can't mix dontcross and write options"
#~ msgstr "no puede mezclar las opciones dontcross y write"
+#~ msgid "components_etypenavigation"
+#~ msgstr "Filtar por tipo"
+
+#~ msgid "components_etypenavigation_description"
+#~ msgstr "Permite filtrar por tipo de entidad los resultados de una búsqueda"
+
#~ msgid "error while querying source %s, some data may be missing"
#~ msgstr ""
#~ "Un error ha ocurrido al interrogar %s, es posible que los \n"
@@ -4649,6 +4646,9 @@
#~ msgid "unknown option(s): %s"
#~ msgstr "opcion(es) desconocida(s): %s"
+#~ msgid "web sessions without CNX"
+#~ msgstr "sesiones web sin conexión asociada"
+
#~ msgid "you may want to specify something for %s"
#~ msgstr "usted desea quizás especificar algo para la relación %s"
--- a/i18n/fr.po Thu Sep 25 15:49:13 2014 +0200
+++ b/i18n/fr.po Fri Oct 17 18:16:58 2014 +0200
@@ -224,9 +224,6 @@
msgid "Added relation : %(entity_from)s %(rtype)s %(entity_to)s"
msgstr "Relation ajoutée : %(entity_from)s %(rtype)s %(entity_to)s"
-msgid "Any"
-msgstr "Tous"
-
msgid "Attributes permissions:"
msgstr "Permissions des attributs"
@@ -1516,12 +1513,6 @@
msgid "components"
msgstr "composants"
-msgid "components_etypenavigation"
-msgstr "filtrage par type"
-
-msgid "components_etypenavigation_description"
-msgstr "permet de filtrer par type d'entité les résultats d'une recherche"
-
msgid "components_navigation"
msgstr "navigation par page"
@@ -4523,9 +4514,6 @@
msgstr ""
"nous ne sommes pas capable de gérer ce type de requête sparql pour le moment"
-msgid "web sessions without CNX"
-msgstr "sessions web sans connexion associée"
-
msgid "wednesday"
msgstr "mercredi"
@@ -4621,6 +4609,9 @@
#~ msgid "%s relation should not be in mapped"
#~ msgstr "la relation %s ne devrait pas ếtre mappé"
+#~ msgid "Any"
+#~ msgstr "Tous"
+
#~ msgid "attribute/relation can't be mapped, only entity and relation types"
#~ msgstr ""
#~ "les attributs et relations ne peuvent être mappés, uniquement les types "
@@ -4635,6 +4626,12 @@
#~ msgid "can't mix dontcross and write options"
#~ msgstr "ne peut mélanger dontcross et write options"
+#~ msgid "components_etypenavigation"
+#~ msgstr "filtrage par type"
+
+#~ msgid "components_etypenavigation_description"
+#~ msgstr "permet de filtrer par type d'entité les résultats d'une recherche"
+
#~ msgid "error while querying source %s, some data may be missing"
#~ msgstr ""
#~ "une erreur est survenue en interrogeant %s, il est possible que les\n"
@@ -4651,6 +4648,9 @@
#~ msgid "unknown option(s): %s"
#~ msgstr "option(s) inconnue(s) : %s"
+#~ msgid "web sessions without CNX"
+#~ msgstr "sessions web sans connexion associée"
+
#~ msgid "you may want to specify something for %s"
#~ msgstr "vous désirez peut-être spécifié quelque chose pour la relation %s"
--- a/server/sources/native.py Thu Sep 25 15:49:13 2014 +0200
+++ b/server/sources/native.py Fri Oct 17 18:16:58 2014 +0200
@@ -1023,14 +1023,16 @@
sql = self.sqlgen.select('tx_entity_actions', restr,
('txa_action', 'txa_public', 'txa_order',
'etype', 'eid', 'changes'))
- cu = self.doexec(cnx, sql, restr)
- actions = [tx.EntityAction(a,p,o,et,e,c and loads(self.binary_to_str(c)))
- for a,p,o,et,e,c in cu.fetchall()]
+ with cnx.ensure_cnx_set:
+ cu = self.doexec(cnx, sql, restr)
+ actions = [tx.EntityAction(a,p,o,et,e,c and loads(self.binary_to_str(c)))
+ for a,p,o,et,e,c in cu.fetchall()]
sql = self.sqlgen.select('tx_relation_actions', restr,
('txa_action', 'txa_public', 'txa_order',
'rtype', 'eid_from', 'eid_to'))
- cu = self.doexec(cnx, sql, restr)
- actions += [tx.RelationAction(*args) for args in cu.fetchall()]
+ with cnx.ensure_cnx_set:
+ cu = self.doexec(cnx, sql, restr)
+ actions += [tx.RelationAction(*args) for args in cu.fetchall()]
return sorted(actions, key=lambda x: x.order)
def undo_transaction(self, cnx, txuuid):
--- a/server/test/unittest_undo.py Thu Sep 25 15:49:13 2014 +0200
+++ b/server/test/unittest_undo.py Fri Oct 17 18:16:58 2014 +0200
@@ -75,6 +75,8 @@
self.assertTrue(self.txuuid)
# test transaction api
with self.admin_access.client_cnx() as cnx:
+ tx_actions = cnx.transaction_actions(self.txuuid)
+ self.assertEqual(len(tx_actions), 2, tx_actions)
self.assertRaises(NoSuchTransaction,
cnx.transaction_info, 'hop')
self.assertRaises(NoSuchTransaction,
--- a/web/http_headers.py Thu Sep 25 15:49:13 2014 +0200
+++ b/web/http_headers.py Fri Oct 17 18:16:58 2014 +0200
@@ -445,6 +445,21 @@
l.append('%s=%s' % (k, v))
return ";".join(l)
+def generateTrueFalse(value):
+ """
+ Return 'true' or 'false' depending on the value.
+
+ * 'true' values are `True`, `1`, `"true"`
+ * 'false' values are `False`, `0`, `"false"`
+
+ """
+ if (value in (True, 1) or
+ isinstance(value, basestring) and value.lower() == 'true'):
+ return 'true'
+ if (value in (False, 0) or
+ isinstance(value, basestring) and value.lower() == 'false'):
+ return 'false'
+ raise ValueError("Invalid true/false header value: %s" % value)
class MimeType(object):
def fromString(klass, mimeTypeString):
@@ -1489,12 +1504,8 @@
'Accept-Charset': (tokenize, listParser(parseAcceptQvalue), dict, addDefaultCharset),
'Accept-Encoding': (tokenize, listParser(parseAcceptQvalue), dict, addDefaultEncoding),
'Accept-Language': (tokenize, listParser(parseAcceptQvalue), dict),
- 'Access-Control-Allow-Origin': (last, parseAllowOrigin,),
- 'Access-Control-Allow-Credentials': (last, parseAllowCreds,),
- 'Access-Control-Allow-Methods': (tokenize, listParser(parseHTTPMethod), list),
'Access-Control-Request-Method': (parseHTTPMethod, ),
'Access-Control-Request-Headers': (filterTokens, ),
- 'Access-Control-Expose-Headers': (filterTokens, ),
'Authorization': (last, parseAuthorization),
'Cookie': (parseCookie,),
'Expect': (tokenize, listParser(parseExpect), dict),
@@ -1521,8 +1532,6 @@
listGenerator(generateAcceptQvalue), singleHeader),
'Accept-Language': (iteritems, listGenerator(generateAcceptQvalue), singleHeader),
'Access-Control-Request-Method': (unique, str, singleHeader, ),
- 'Access-Control-Expose-Headers': (listGenerator(str), ),
- 'Access-Control-Allow-Headers': (listGenerator(str), ),
'Authorization': (generateAuthorization,), # what is "credentials"
'Cookie': (generateCookie, singleHeader),
'Expect': (iteritems, listGenerator(generateExpect), singleHeader),
@@ -1544,6 +1553,11 @@
parser_response_headers = {
'Accept-Ranges': (tokenize, filterTokens),
+ 'Access-Control-Allow-Origin': (last, parseAllowOrigin,),
+ 'Access-Control-Allow-Credentials': (last, parseAllowCreds,),
+ 'Access-Control-Allow-Methods': (tokenize, listParser(parseHTTPMethod), list),
+ 'Access-Control-Allow-Headers': (listGenerator(str), ),
+ 'Access-Control-Expose-Headers': (filterTokens, ),
'Age': (last, int),
'ETag': (tokenize, ETag.parse),
'Location': (last,), # TODO: URI object?
@@ -1559,6 +1573,11 @@
generator_response_headers = {
'Accept-Ranges': (generateList, singleHeader),
+ 'Access-Control-Allow-Origin': (unique, str, singleHeader),
+ 'Access-Control-Allow-Credentials': (generateTrueFalse, singleHeader),
+ 'Access-Control-Allow-Headers': (set, generateList, singleHeader),
+ 'Access-Control-Allow-Methods': (set, generateList, singleHeader),
+ 'Access-Control-Expose-Headers': (set, generateList, singleHeader),
'Age': (unique, str, singleHeader),
'ETag': (ETag.generate, singleHeader),
'Location': (unique, str, singleHeader),
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/unittest_http_headers.py Fri Oct 17 18:16:58 2014 +0200
@@ -0,0 +1,14 @@
+import unittest
+
+from cubicweb.web import http_headers
+
+
+class TestGenerators(unittest.TestCase):
+ def test_generate_true_false(self):
+ for v in (True, 1, 'true', 'True', 'TRUE'):
+ self.assertEqual('true', http_headers.generateTrueFalse(v))
+ for v in (False, 0, 'false', 'False', 'FALSE'):
+ self.assertEqual('false', http_headers.generateTrueFalse(v))
+
+ with self.assertRaises(ValueError):
+ http_headers.generateTrueFalse('any value')
--- a/wsgi/request.py Thu Sep 25 15:49:13 2014 +0200
+++ b/wsgi/request.py Fri Oct 17 18:16:58 2014 +0200
@@ -32,7 +32,9 @@
from urlparse import parse_qs
from warnings import warn
-from cubicweb.multipart import copy_file, parse_form_data
+from cubicweb.multipart import (
+ copy_file, parse_form_data, MultipartError, parse_options_header)
+from cubicweb.web import RequestError
from cubicweb.web.request import CubicWebRequestBase
from cubicweb.wsgi import pformat, normalize_header
@@ -81,10 +83,7 @@
self.content = environ['wsgi.input']
if files is not None:
for key, part in files.iteritems():
- name = None
- if part.filename is not None:
- name = unicode(part.filename, self.encoding)
- self.form[key] = (name, part.file)
+ self.form[key] = (part.filename, part.file)
def __repr__(self):
# Since this is called as part of error handling, we need to be very
@@ -127,9 +126,18 @@
post = parse_qs(self.environ.get('QUERY_STRING', ''))
files = None
if self.method == 'POST':
- forms, files = parse_form_data(self.environ, strict=True,
- mem_limit=self.vreg.config['max-post-length'])
- post.update(forms.dict)
+ content_type = self.environ.get('CONTENT_TYPE')
+ if not content_type:
+ raise RequestError("Missing Content-Type")
+ content_type, options = parse_options_header(content_type)
+ if content_type in (
+ 'multipart/form-data',
+ 'application/x-www-form-urlencoded',
+ 'application/x-url-encoded'):
+ forms, files = parse_form_data(
+ self.environ, strict=True,
+ mem_limit=self.vreg.config['max-post-length'])
+ post.update(forms.dict)
self.content.seek(0, 0)
return post, files
--- a/wsgi/test/unittest_wsgi.py Thu Sep 25 15:49:13 2014 +0200
+++ b/wsgi/test/unittest_wsgi.py Fri Oct 17 18:16:58 2014 +0200
@@ -6,6 +6,7 @@
from cubicweb.devtools.webtest import CubicWebTestTC
from cubicweb.wsgi.request import CubicWebWsgiRequest
+from cubicweb.multipart import MultipartError
class WSGIAppTC(CubicWebTestTC):
@@ -66,6 +67,19 @@
'/',
params={'__login': self.admlogin, '__password': self.admpassword})
+ def test_post_bad_form(self):
+ with self.assertRaises(MultipartError):
+ self.webapp.post(
+ '/',
+ params='badcontent',
+ headers={'Content-Type': 'multipart/form-data'})
+
+ def test_post_non_form(self):
+ self.webapp.post(
+ '/',
+ params='{}',
+ headers={'Content-Type': 'application/json'})
+
def test_get_multiple_variables(self):
r = webtest.app.TestRequest.blank('/?arg=1&arg=2')
req = CubicWebWsgiRequest(r.environ, self.vreg)
@@ -78,6 +92,17 @@
self.assertEqual([u'1', u'2'], req.form['arg'])
+ def test_post_files(self):
+ content_type, params = self.webapp.encode_multipart(
+ (), (('filefield', 'aname', 'acontent'),))
+ r = webtest.app.TestRequest.blank(
+ '/', POST=params, content_type=content_type)
+ req = CubicWebWsgiRequest(r.environ, self.vreg)
+ self.assertIn('filefield', req.form)
+ fieldvalue = req.form['filefield']
+ self.assertEqual(u'aname', fieldvalue[0])
+ self.assertEqual('acontent', fieldvalue[1].read())
+
def test_post_unicode_urlencoded(self):
params = 'arg=%C3%A9'
r = webtest.app.TestRequest.blank(