Repository: pypi/warehouse

Issue:
Uploading doesn't add a journal entry
Currently the upload API is not actually adding a journal entry, this needs to happen.



Discussion:

Resolving patch:
diff --git a/warehouse/legacy/api/pypi.py b/warehouse/legacy/api/pypi.py
--- a/warehouse/legacy/api/pypi.py
+++ b/warehouse/legacy/api/pypi.py
@@ -15,6 +15,7 @@
 import os.path
 import re
 import tempfile
+import zipfile
 
 import packaging.specifiers
 import packaging.version
@@ -35,7 +36,9 @@
 from warehouse.packaging.interfaces import IFileStorage
 from warehouse.packaging.models import (
     Project, Release, Dependency, DependencyKind, Role, File, Filename,
+    JournalEntry,
 )
+from warehouse.sessions import uses_session
 from warehouse.utils.http import require_POST
 
 
@@ -415,13 +418,85 @@ def full_validate(self):
                 )
 
 
-# TODO: Uncomment the below code once the upload view is safe to be used on
-#       warehouse.python.org. For now, we'll disable it so people can't use
-#       Warehouse to upload and get broken or not properly validated data.
-# @view_config(
-#     route_name="legacy.api.pypi.file_upload",
-#     decorator=[require_POST, csrf_exempt, uses_session],
-# )
+_safe_zipnames = re.compile(r"(purelib|platlib|headers|scripts|data).+", re.I)
+
+
+def _is_valid_dist_file(filename, filetype):
+    """
+    Perform some basic checks to see whether the indicated file could be
+    a valid distribution file.
+    """
+
+    if filename.endswith(".exe"):
+        # The only valid filetype for a .exe file is "bdist_wininst".
+        if filetype != "bdist_wininst":
+            return False
+
+        # Ensure that the .exe is a valid zip file, and that all of the files
+        # contained within it have safe filenames.
+        try:
+            with zipfile.ZipFile(filename, "r") as zfp:
+                # We need the no branch below to work around a bug in
+                # coverage.py where it's detecting a missed branch where there
+                # isn't one.
+                for zipname in zfp.namelist():  # pragma: no branch
+                    if not _safe_zipnames.match(zipname):
+                        return False
+        except zipfile.BadZipFile:
+            return False
+    elif filename.endswith(".msi"):
+        # The only valid filetype for a .msi is "bdist_msi"
+        if filetype != "bdist_msi":
+            return False
+
+        # Check the first 8 bytes of the MSI file. This was taken from the
+        # legacy implementation of PyPI which itself took it from the
+        # implementation of `file` I believe.
+        with open(filename, "rb") as fp:
+            if fp.read(8) != b"\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1":
+                return False
+    elif filename.endswith(".zip") or filename.endswith(".egg"):
+        # Ensure that the .zip/.egg is a valid zip file, and that it has a
+        # PKG-INFO file.
+        try:
+            with zipfile.ZipFile(filename, "r") as zfp:
+                for zipname in zfp.namelist():
+                    parts = os.path.split(zipname)
+                    if len(parts) == 2 and parts[1] == "PKG-INFO":
+                        # We need the no branch below to work around a bug in
+                        # coverage.py where it's detecting a missed branch
+                        # where there isn't one.
+                        break  # pragma: no branch
+                else:
+                    return False
+        except zipfile.BadZipFile:
+            return False
+    elif filename.endswith(".whl"):
+        # Ensure that the .whl is a valid zip file, and that it has a WHEEL
+        # file.
+        try:
+            with zipfile.ZipFile(filename, "r") as zfp:
+                for zipname in zfp.namelist():
+                    parts = os.path.split(zipname)
+                    if len(parts) == 2 and parts[1] == "WHEEL":
+                        # We need the no branch below to work around a bug in
+                        # coverage.py where it's detecting a missed branch
+                        # where there isn't one.
+                        break  # pragma: no branch
+                else:
+                    return False
+        except zipfile.BadZipFile:
+            return False
+
+    # If we haven't yet decided it's not valid, then we'll assume it is and
+    # allow it.
+    return True
+
+
+@view_config(
+    route_name="legacy.api.pypi.file_upload",
+    decorator=[require_POST, csrf_exempt, uses_session],
+)
 def file_upload(request):
     # Before we do anything, if there isn't an authenticated user with this
     # request, then we'll go ahead and bomb out.
@@ -499,6 +574,25 @@ def file_upload(request):
         request.db.add(
             Role(user=request.user, project=project, role_name="Owner")
         )
+        # TODO: This should be handled by some sort of database trigger or a
+        #       SQLAlchemy hook or the like instead of doing it inline in this
+        #       view.
+        request.db.add(
+            JournalEntry(
+                name=project.name,
+                action="create",
+                submitted_by=request.user,
+                submitted_from=request.client_addr,
+            ),
+        )
+        request.db.add(
+            JournalEntry(
+                name=project.name,
+                action="add Owner {}".format(request.user.username),
+                submitted_by=request.user,
+                submitted_from=request.client_addr,
+            ),
+        )
 
     # Check that the user has permission to do things to this project, if this
     # is a new project this will act as a sanity check for the role we just
@@ -551,6 +645,18 @@ def file_upload(request):
             }
         )
         request.db.add(release)
+        # TODO: This should be handled by some sort of database trigger or a
+        #       SQLAlchemy hook or the like instead of doing it inline in this
+        #       view.
+        request.db.add(
+            JournalEntry(
+                name=release.project.name,
+                version=release.version,
+                action="new release",
+                submitted_by=request.user,
+                submitted_from=request.client_addr,
+            ),
+        )
 
     # TODO: We need a better solution to this than to just do it inline inside
     #       this method. Ideally the version field would just be sortable, but
@@ -590,6 +696,11 @@ def file_upload(request):
             )
         )
 
+    # Check the content type of what is being uploaded
+    if (not request.POST["content"].type
+            or request.POST["content"].type.startswith("image/")):
+        raise _exc_with_message(HTTPBadRequest, "Invalid distribution file.")
+
     # Check to see if the file that was uploaded exists already or not.
     if request.db.query(
             request.db.query(File)
@@ -614,9 +725,11 @@ def file_upload(request):
     file_size_limit = max(filter(None, [MAX_FILESIZE, project.upload_limit]))
 
     with tempfile.TemporaryDirectory() as tmpdir:
+        temporary_filename = os.path.join(tmpdir, filename)
+
         # Buffer the entire file onto disk, checking the hash of the file as we
         # go along.
-        with open(os.path.join(tmpdir, filename), "wb") as fp:
+        with open(temporary_filename, "wb") as fp:
             file_size = 0
             file_hash = hashlib.md5()
             for chunk in iter(
@@ -639,7 +752,12 @@ def file_upload(request):
                 "from the uploaded file."
             )
 
-        # TODO: Check the file to make sure it is a valid distribution file.
+        # Check the file to make sure it is a valid distribution file.
+        if not _is_valid_dist_file(temporary_filename, form.filetype.data):
+            raise _exc_with_message(
+                HTTPBadRequest,
+                "Invalid distribution file.",
+            )
 
         # Check that if it's a binary wheel, it's on a supported platform
         if filename.endswith(".whl"):
@@ -677,9 +795,9 @@ def file_upload(request):
         else:
             has_signature = False
 
-        # TODO: We need some sort of trigger that will automatically add
-        #       filenames to Filename instead of relying on this code running
-        #       inside of our upload API.
+        # TODO: This should be handled by some sort of database trigger or a
+        #       SQLAlchemy hook or the like instead of doing it inline in this
+        #       view.
         request.db.add(Filename(filename=filename))
 
         # Store the information about the file in the database.
@@ -695,6 +813,22 @@ def file_upload(request):
         )
         request.db.add(file_)
 
+        # TODO: This should be handled by some sort of database trigger or a
+        #       SQLAlchemy hook or the like instead of doing it inline in this
+        #       view.
+        request.db.add(
+            JournalEntry(
+                name=release.project.name,
+                version=release.version,
+                action="add {python_version} file {filename}".format(
+                    python_version=file_.python_version,
+                    filename=file_.filename,
+                ),
+                submitted_by=request.user,
+                submitted_from=request.client_addr,
+            ),
+        )
+
         # TODO: We need a better answer about how to make this transactional so
         #       this won't take affect until after a commit has happened, for
         #       now we'll just ignore it and save it before the transaction is
diff --git a/warehouse/migrations/versions/477bc785c999_add_a_server_default_for_submitted_date.py b/warehouse/migrations/versions/477bc785c999_add_a_server_default_for_submitted_date.py
new file mode 100644
--- /dev/null
+++ b/warehouse/migrations/versions/477bc785c999_add_a_server_default_for_submitted_date.py
@@ -0,0 +1,44 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+Add a server default for submitted_date
+
+Revision ID: 477bc785c999
+Revises: 6a03266b2d
+Create Date: 2015-12-16 16:19:59.419186
+"""
+
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects import postgresql
+
+revision = "477bc785c999"
+down_revision = "6a03266b2d"
+
+
+def upgrade():
+    op.alter_column(
+        "journals",
+        "submitted_date",
+        server_default=sa.func.now(),
+        nullable=False,
+    )
+
+
+def downgrade():
+    op.alter_column(
+        "journals",
+        "submitted_date",
+        existing_type=postgresql.TIMESTAMP(),
+        server_default=None,
+        nullable=True,
+    )
diff --git a/warehouse/packaging/models.py b/warehouse/packaging/models.py
--- a/warehouse/packaging/models.py
+++ b/warehouse/packaging/models.py
@@ -438,7 +438,11 @@ def __table_args__(cls):  # noqa
     name = Column(Text)
     version = Column(Text)
     action = Column(Text)
-    submitted_date = Column(DateTime(timezone=False))
+    submitted_date = Column(
+        DateTime(timezone=False),
+        nullable=False,
+        server_default=sql.func.now(),
+    )
     _submitted_by = Column(
         "submitted_by",
         CIText,


Test patch:
diff --git a/tests/unit/legacy/api/test_pypi.py b/tests/unit/legacy/api/test_pypi.py
--- a/tests/unit/legacy/api/test_pypi.py
+++ b/tests/unit/legacy/api/test_pypi.py
@@ -13,6 +13,7 @@
 import io
 import os.path
 import tempfile
+import zipfile
 
 from unittest import mock
 
@@ -29,6 +30,7 @@
 from warehouse.packaging.interfaces import IFileStorage
 from warehouse.packaging.models import (
     File, Filename, Dependency, DependencyKind, Release, Project, Role,
+    JournalEntry,
 )
 
 from ....common.db.accounts import UserFactory
@@ -315,6 +317,122 @@ def test_full_validate_invalid(self, data):
             form.full_validate()
 
 
+class TestFileValidation:
+
+    def test_defaults_to_true(self):
+        assert pypi._is_valid_dist_file("", "")
+
+    @pytest.mark.parametrize(
+        ("filename", "filetype"),
+        [
+            ("test.exe", "bdist_msi"),
+            ("test.msi", "bdist_wininst"),
+        ],
+    )
+    def test_bails_with_invalid_package_type(self, filename, filetype):
+        assert not pypi._is_valid_dist_file(filename, filetype)
+
+    @pytest.mark.parametrize(
+        ("filename", "filetype"),
+        [
+            ("test.exe", "bdist_wininst"),
+            ("test.zip", "sdist"),
+            ("test.egg", "bdist_egg"),
+            ("test.whl", "bdist_wheel"),
+        ],
+    )
+    def test_bails_with_invalid_zipfile(self, tmpdir, filename, filetype):
+        f = str(tmpdir.join(filename))
+
+        with open(f, "wb") as fp:
+            fp.write(b"this is not a valid zip file")
+
+        assert not pypi._is_valid_dist_file(f, filetype)
+
+    def test_wininst_unsafe_filename(self, tmpdir):
+        f = str(tmpdir.join("test.exe"))
+
+        with zipfile.ZipFile(f, "w") as zfp:
+            zfp.writestr("something/bar.py", b"the test file")
+
+        assert not pypi._is_valid_dist_file(f, "bdist_wininst")
+
+    def test_wininst_safe_filename(self, tmpdir):
+        f = str(tmpdir.join("test.exe"))
+
+        with zipfile.ZipFile(f, "w") as zfp:
+            zfp.writestr("purelib/bar.py", b"the test file")
+
+        assert pypi._is_valid_dist_file(f, "bdist_wininst")
+
+    def test_msi_invalid_header(self, tmpdir):
+        f = str(tmpdir.join("test.msi"))
+
+        with open(f, "wb") as fp:
+            fp.write(b"this is not the correct header for an msi")
+
+        assert not pypi._is_valid_dist_file(f, "bdist_msi")
+
+    def test_msi_valid_header(self, tmpdir):
+        f = str(tmpdir.join("test.msi"))
+
+        with open(f, "wb") as fp:
+            fp.write(b"\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1")
+
+        assert pypi._is_valid_dist_file(f, "bdist_msi")
+
+    def test_zip_no_pkg_info(self, tmpdir):
+        f = str(tmpdir.join("test.zip"))
+
+        with zipfile.ZipFile(f, "w") as zfp:
+            zfp.writestr("something.txt", b"Just a placeholder file")
+
+        assert not pypi._is_valid_dist_file(f, "sdist")
+
+    def test_zip_has_pkg_info(self, tmpdir):
+        f = str(tmpdir.join("test.zip"))
+
+        with zipfile.ZipFile(f, "w") as zfp:
+            zfp.writestr("something.txt", b"Just a placeholder file")
+            zfp.writestr("PKG-INFO", b"this is the package info")
+
+        assert pypi._is_valid_dist_file(f, "sdist")
+
+    def test_egg_no_pkg_info(self, tmpdir):
+        f = str(tmpdir.join("test.egg"))
+
+        with zipfile.ZipFile(f, "w") as zfp:
+            zfp.writestr("something.txt", b"Just a placeholder file")
+
+        assert not pypi._is_valid_dist_file(f, "bdist_egg")
+
+    def test_egg_has_pkg_info(self, tmpdir):
+        f = str(tmpdir.join("test.egg"))
+
+        with zipfile.ZipFile(f, "w") as zfp:
+            zfp.writestr("something.txt", b"Just a placeholder file")
+            zfp.writestr("PKG-INFO", b"this is the package info")
+
+        assert pypi._is_valid_dist_file(f, "bdist_egg")
+
+    def test_wheel_no_wheel_file(self, tmpdir):
+        f = str(tmpdir.join("test.whl"))
+
+        with zipfile.ZipFile(f, "w") as zfp:
+            zfp.writestr("something.txt", b"Just a placeholder file")
+
+        assert not pypi._is_valid_dist_file(f, "bdist_wheel")
+
+    def test_wheel_has_wheel_file(self, tmpdir):
+        f = str(tmpdir.join("test.whl"))
+
+        with zipfile.ZipFile(f, "w") as zfp:
+            zfp.writestr("something.txt", b"Just a placeholder file")
+            zfp.writestr("WHEEL", b"this is the package info")
+
+        assert pypi._is_valid_dist_file(f, "bdist_wheel")
+
+
 class TestFileUpload:
 
     @pytest.mark.parametrize("version", ["2", "3", "-1", "0", "dog", "cat"])
@@ -523,6 +641,8 @@ def test_successful_upload(self, tmpdir, monkeypatch, pyramid_config,
 
         filename = "{}-{}.tar.gz".format(project.name, release.version)
 
+        db_request.user = user
+        db_request.client_addr = "10.10.10.40"
         db_request.POST = MultiDict({
             "metadata_version": "1.2",
             "name": project.name,
@@ -533,6 +653,7 @@ def test_successful_upload(self, tmpdir, monkeypatch, pyramid_config,
             "content": pretend.stub(
                 filename=filename,
                 file=io.BytesIO(b"A fake file."),
+                type="application/tar",
             ),
         })
         db_request.POST.extend([
@@ -602,6 +723,68 @@ def storage_service_store(path, file_path):
         db_request.db.query(Filename) \
                      .filter(Filename.filename == filename).one()
 
+        # Ensure that all of our journal entries have been created
+        journals = (
+            db_request.db.query(JournalEntry)
+                         .order_by("submitted_date")
+                         .all()
+        )
+        assert [
+            (j.name, j.version, j.action, j.submitted_by, j.submitted_from)
+            for j in journals
+        ] == [
+            (
+                release.project.name,
+                release.version,
+                "add source file {}".format(filename),
+                user,
+                "10.10.10.40",
+            ),
+        ]
+
+    @pytest.mark.parametrize("content_type", [None, "image/foobar"])
+    def test_upload_fails_invlaid_content_type(self, tmpdir, monkeypatch,
+                                               pyramid_config, db_request,
+                                               content_type):
+        monkeypatch.setattr(tempfile, "tempdir", str(tmpdir))
+
+        pyramid_config.testing_securitypolicy(userid=1)
+        user = UserFactory.create()
+        project = ProjectFactory.create()
+        release = ReleaseFactory.create(project=project, version="1.0")
+        RoleFactory.create(user=user, project=project)
+
+        db_request.db.add(
+            Classifier(classifier="Environment :: Other Environment"),
+        )
+
+        filename = "{}-{}.tar.gz".format(project.name, release.version)
+
+        db_request.POST = MultiDict({
+            "metadata_version": "1.2",
+            "name": project.name,
+            "version": release.version,
+            "filetype": "sdist",
+            "pyversion": "source",
+            "md5_digest": "335c476dc930b959dda9ec82bd65ef19",
+            "content": pretend.stub(
+                filename=filename,
+                file=io.BytesIO(b"A fake file."),
+                type=content_type,
+            ),
+        })
+        db_request.POST.extend([
+            ("classifiers", "Environment :: Other Environment"),
+        ])
+
+        with pytest.raises(HTTPBadRequest) as excinfo:
+            pypi.file_upload(db_request)
+
+        resp = excinfo.value
+
+        assert resp.status_code == 400
+        assert resp.status == "400 Invalid distribution file."
+
     @pytest.mark.parametrize("sig", [b"lol nope"])
     def test_upload_fails_with_invalid_signature(self, pyramid_config,
                                                  db_request, sig):
@@ -623,6 +806,7 @@ def test_upload_fails_with_invalid_signature(self, pyramid_config,
             "content": pretend.stub(
                 filename=filename,
                 file=io.BytesIO(b"A fake file."),
+                type="application/tar",
             ),
             "gpg_signature": pretend.stub(
                 filename=filename + ".asc",
@@ -658,6 +842,7 @@ def test_upload_fails_with_invalid_classifier(self, pyramid_config,
             "content": pretend.stub(
                 filename=filename,
                 file=io.BytesIO(b"A fake file."),
+                type="application/tar",
             ),
         })
         db_request.POST.extend([
@@ -694,6 +879,7 @@ def test_upload_fails_with_invalid_hash(self, pyramid_config, db_request):
             "content": pretend.stub(
                 filename=filename,
                 file=io.BytesIO(b"A fake file."),
+                type="application/tar",
             ),
         })
 
@@ -708,6 +894,37 @@ def test_upload_fails_with_invalid_hash(self, pyramid_config, db_request):
             "from the uploaded file."
         )
 
+    def test_upload_fails_with_invalid_file(self, pyramid_config, db_request):
+        pyramid_config.testing_securitypolicy(userid=1)
+
+        user = UserFactory.create()
+        project = ProjectFactory.create()
+        release = ReleaseFactory.create(project=project, version="1.0")
+        RoleFactory.create(user=user, project=project)
+
+        filename = "{}-{}.zip".format(project.name, release.version)
+
+        db_request.POST = MultiDict({
+            "metadata_version": "1.2",
+            "name": project.name,
+            "version": release.version,
+            "filetype": "sdist",
+            "md5_digest": "0cc175b9c0f1b6a831c399e269772661",
+            "content": pretend.stub(
+                filename=filename,
+                file=io.BytesIO(b"a"),
+                type="application/zip",
+            ),
+        })
+
+        with pytest.raises(HTTPBadRequest) as excinfo:
+            pypi.file_upload(db_request)
+
+        resp = excinfo.value
+
+        assert resp.status_code == 400
+        assert resp.status == "400 Invalid distribution file."
+
     def test_upload_fails_with_too_large_file(self, pyramid_config,
                                               db_request):
         pyramid_config.testing_securitypolicy(userid=1)
@@ -728,6 +945,7 @@ def test_upload_fails_with_too_large_file(self, pyramid_config,
             "content": pretend.stub(
                 filename=filename,
                 file=io.BytesIO(b"a" * (pypi.MAX_FILESIZE + 1)),
+                type="application/tar",
             ),
         })
 
@@ -759,6 +977,7 @@ def test_upload_fails_with_too_large_signature(self, pyramid_config,
             "content": pretend.stub(
                 filename=filename,
                 file=io.BytesIO(b"a"),
+                type="application/tar",
             ),
             "gpg_signature": pretend.stub(
                 filename=filename + ".asc",
@@ -794,6 +1013,7 @@ def test_upload_fails_with_previously_used_filename(self, pyramid_config,
             "content": pretend.stub(
                 filename=filename,
                 file=io.BytesIO(b"a" * (pypi.MAX_FILESIZE + 1)),
+                type="application/tar",
             ),
         })
 
@@ -829,6 +1049,7 @@ def test_upload_fails_with_existing_file(self, pyramid_config, db_request):
             "content": pretend.stub(
                 filename=filename,
                 file=io.BytesIO(b"a" * (pypi.MAX_FILESIZE + 1)),
+                type="application/tar",
             ),
         })
 
@@ -862,6 +1083,7 @@ def test_upload_fails_with_wrong_filename(self, pyramid_config,
             "content": pretend.stub(
                 filename=filename,
                 file=io.BytesIO(b"a" * (pypi.MAX_FILESIZE + 1)),
+                type="application/tar",
             ),
         })
 
@@ -898,6 +1120,7 @@ def test_upload_fails_with_invalid_extension(self, pyramid_config,
             "content": pretend.stub(
                 filename=filename,
                 file=io.BytesIO(b"a" * (pypi.MAX_FILESIZE + 1)),
+                type="application/tar",
             ),
         })
 
@@ -933,6 +1156,7 @@ def test_upload_fails_with_unsafe_filename(self, pyramid_config,
             "content": pretend.stub(
                 filename=filename,
                 file=io.BytesIO(b"a" * (pypi.MAX_FILESIZE + 1)),
+                type="application/tar",
             ),
         })
 
@@ -962,6 +1186,7 @@ def test_upload_fails_without_permission(self, pyramid_config, db_request):
             "content": pretend.stub(
                 filename=filename,
                 file=io.BytesIO(b"a" * (pypi.MAX_FILESIZE + 1)),
+                type="application/tar",
             ),
         })
 
@@ -989,6 +1214,8 @@ def test_upload_succeeds_with_wheel(self, tmpdir, monkeypatch,
             plat,
         )
 
+        db_request.user = user
+        db_request.client_addr = "10.10.10.30"
         db_request.POST = MultiDict({
             "metadata_version": "1.2",
             "name": project.name,
@@ -999,6 +1226,7 @@ def test_upload_succeeds_with_wheel(self, tmpdir, monkeypatch,
             "content": pretend.stub(
                 filename=filename,
                 file=io.BytesIO(b"A fake file."),
+                type="application/tar",
             ),
         })
 
@@ -1012,6 +1240,8 @@ def storage_service_store(path, file_path):
             lambda svc: storage_service
         )
 
+        monkeypatch.setattr(pypi, "_is_valid_dist_file", lambda *a, **kw: True)
+
         resp = pypi.file_upload(db_request)
 
         assert resp.status_code == 200
@@ -1038,8 +1268,28 @@ def storage_service_store(path, file_path):
         db_request.db.query(Filename) \
                      .filter(Filename.filename == filename).one()
 
+        # Ensure that all of our journal entries have been created
+        journals = (
+            db_request.db.query(JournalEntry)
+                         .order_by("submitted_date")
+                         .all()
+        )
+        assert [
+            (j.name, j.version, j.action, j.submitted_by, j.submitted_from)
+            for j in journals
+        ] == [
+            (
+                release.project.name,
+                release.version,
+                "add cp34 file {}".format(filename),
+                user,
+                "10.10.10.30",
+            ),
+        ]
+
     @pytest.mark.parametrize("plat", ["linux_x86_64", "linux_x86_64.win32"])
-    def test_upload_fails_with_unsupported_wheel_plat(self, pyramid_config,
+    def test_upload_fails_with_unsupported_wheel_plat(self, monkeypatch,
+                                                      pyramid_config,
                                                       db_request, plat):
         pyramid_config.testing_securitypolicy(userid=1)
 
@@ -1064,9 +1314,12 @@ def test_upload_fails_with_unsupported_wheel_plat(self, pyramid_config,
             "content": pretend.stub(
                 filename=filename,
                 file=io.BytesIO(b"A fake file."),
+                type="application/tar",
             ),
         })
 
+        monkeypatch.setattr(pypi, "_is_valid_dist_file", lambda *a, **kw: True)
+
         with pytest.raises(HTTPBadRequest) as excinfo:
             pypi.file_upload(db_request)
 
@@ -1091,6 +1344,8 @@ def test_upload_succeeds_creates_release(self, pyramid_config, db_request):
 
         filename = "{}-{}.tar.gz".format(project.name, "1.0")
 
+        db_request.user = user
+        db_request.client_addr = "10.10.10.20"
         db_request.POST = MultiDict({
             "metadata_version": "1.2",
             "name": project.name,
@@ -1101,6 +1356,7 @@ def test_upload_succeeds_creates_release(self, pyramid_config, db_request):
             "content": pretend.stub(
                 filename=filename,
                 file=io.BytesIO(b"A fake file."),
+                type="application/tar",
             ),
         })
         db_request.POST.extend([
@@ -1147,6 +1403,32 @@ def test_upload_succeeds_creates_release(self, pyramid_config, db_request):
         db_request.db.query(Filename) \
                      .filter(Filename.filename == filename).one()
 
+        # Ensure that all of our journal entries have been created
+        journals = (
+            db_request.db.query(JournalEntry)
+                         .order_by("submitted_date")
+                         .all()
+        )
+        assert [
+            (j.name, j.version, j.action, j.submitted_by, j.submitted_from)
+            for j in journals
+        ] == [
+            (
+                release.project.name,
+                release.version,
+                "new release",
+                user,
+                "10.10.10.20",
+            ),
+            (
+                release.project.name,
+                release.version,
+                "add source file {}".format(filename),
+                user,
+                "10.10.10.20",
+            ),
+        ]
+
     def test_upload_succeeds_creates_project(self, pyramid_config, db_request):
         pyramid_config.testing_securitypolicy(userid=1)
 
@@ -1164,11 +1446,13 @@ def test_upload_succeeds_creates_project(self, pyramid_config, db_request):
             "content": pretend.stub(
                 filename=filename,
                 file=io.BytesIO(b"A fake file."),
+                type="application/tar",
             ),
         })
 
         storage_service = pretend.stub(store=lambda path, content: None)
         db_request.find_service = lambda svc: storage_service
+        db_request.client_addr = "10.10.10.10"
 
         resp = pypi.file_upload(db_request)
 
@@ -1206,6 +1490,34 @@ def test_upload_succeeds_creates_project(self, pyramid_config, db_request):
         db_request.db.query(Filename) \
                      .filter(Filename.filename == filename).one()
 
+        # Ensure that all of our journal entries have been created
+        journals = (
+            db_request.db.query(JournalEntry)
+                         .order_by("submitted_date")
+                         .all()
+        )
+        assert [
+            (j.name, j.version, j.action, j.submitted_by, j.submitted_from)
+            for j in journals
+        ] == [
+            ("example", None, "create", user, "10.10.10.10"),
+            (
+                "example",
+                None,
+                "add Owner {}".format(user.username),
+                user,
+                "10.10.10.10",
+            ),
+            ("example", "1.0", "new release", user, "10.10.10.10"),
+            (
+                "example",
+                "1.0",
+                "add source file example-1.0.tar.gz",
+                user,
+                "10.10.10.10",
+            ),
+        ]
+
     def test_fails_without_user(self, pyramid_config, pyramid_request):
         pyramid_config.testing_securitypolicy(userid=None)