merge 3.19.5 into 3.20 branch
authorJulien Cristau <julien.cristau@logilab.fr>
Fri, 17 Oct 2014 18:16:58 +0200
changeset 10000 4352b7ccde04
parent 9990 c84ad981fc4a (current diff)
parent 9999 b77419a02e17 (diff)
child 10001 1245357b3b3e
merge 3.19.5 into 3.20 branch
__pkginfo__.py
cubicweb.spec
server/sources/native.py
server/test/unittest_undo.py
web/http_headers.py
wsgi/request.py
--- 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(