diff --git a/tests/unit/admin/views/test_classifiers.py b/tests/unit/admin/views/test_classifiers.py index c705251ddc01..b3be0dad91ed 100644 --- a/tests/unit/admin/views/test_classifiers.py +++ b/tests/unit/admin/views/test_classifiers.py @@ -12,6 +12,7 @@ import pretend import pytest +import sqlalchemy from warehouse.admin.views import classifiers as views from warehouse.classifiers.models import Classifier @@ -80,6 +81,44 @@ def test_add_parent_classifier(self, db_request): assert new.l4 == 0 assert new.l5 == 0 + @pytest.mark.parametrize( + "parent_classifier, parent_levels, expected_levels", + [ + ("private", (2, 0, 0, 0), (2, None, 0, 0)), + ("private", (2, 3, 0, 0), (2, 3, None, 0)), + ("private", (2, 3, 4, 0), (2, 3, 4, None)), + ("Private", (2, 0, 0, 0), (2, None, 0, 0)), + ("Private", (2, 3, 0, 0), (2, 3, None, 0)), + ("Private", (2, 3, 4, 0), (2, 3, 4, None)), + ("PrIvAtE", (2, 0, 0, 0), (2, None, 0, 0)), + ("PrIvAtE", (2, 3, 0, 0), (2, 3, None, 0)), + ("PrIvAtE", (2, 3, 4, 0), (2, 3, 4, None)), + ], + ) + def test_add_private_child_classifier( + self, db_request, parent_classifier, parent_levels, expected_levels + ): + l2, l3, l4, l5 = parent_levels + parent = ClassifierFactory( + l2=l2, l3=l3, l4=l4, l5=l5, classifier=parent_classifier + ) + + db_request.params = {"parent_id": parent.id, "child": "Foobar"} + db_request.session.flash = pretend.call_recorder(lambda *a, **kw: None) + db_request.route_path = lambda *a: "/the/path" + + with pytest.raises(sqlalchemy.exc.IntegrityError): + views.AddClassifier(db_request).add_child_classifier() + + @pytest.mark.parametrize("parent_classifier", ["private", "Private", "PrIvAtE"]) + def test_add_private_parent_classifier(self, db_request, parent_classifier): + db_request.params = {"parent": f"{parent_classifier} :: Do Not Upload"} + db_request.session.flash = pretend.call_recorder(lambda *a, **kw: None) + db_request.route_path = lambda *a: "/the/path" + + with pytest.raises(sqlalchemy.exc.IntegrityError): + views.AddClassifier(db_request).add_parent_classifier() + class TestDeprecateClassifier: def test_deprecate_classifier(self, db_request): diff --git a/warehouse/classifiers/models.py b/warehouse/classifiers/models.py index 97615931f2cc..f4eecefce20c 100644 --- a/warehouse/classifiers/models.py +++ b/warehouse/classifiers/models.py @@ -10,7 +10,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from sqlalchemy import Boolean, Column, Integer, Text, sql +from sqlalchemy import Boolean, CheckConstraint, Column, Integer, Text, sql from warehouse import db from warehouse.utils.attrs import make_repr @@ -19,6 +19,10 @@ class Classifier(db.ModelBase): __tablename__ = "trove_classifiers" + __tableargs__ = CheckConstraint( + "classifier not ilike 'private ::%'", + name="ck_disallow_private_top_level_classifier", + ) __repr__ = make_repr("classifier") diff --git a/warehouse/migrations/versions/c4a1ee483bb3_do_not_allow_private_trove_classifiers.py b/warehouse/migrations/versions/c4a1ee483bb3_do_not_allow_private_trove_classifiers.py new file mode 100644 index 000000000000..e69a3ced0109 --- /dev/null +++ b/warehouse/migrations/versions/c4a1ee483bb3_do_not_allow_private_trove_classifiers.py @@ -0,0 +1,35 @@ +# 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. +""" +Do not allow Private trove classifiers + +Revision ID: c4a1ee483bb3 +Revises: 3db69c05dd11 +Create Date: 2019-02-17 20:01:54.314170 +""" + +from alembic import op + +revision = "c4a1ee483bb3" +down_revision = "3db69c05dd11" + + +def upgrade(): + op.create_check_constraint( + "ck_disallow_private_top_level_classifier", + "trove_classifiers", + "classifier not ilike 'private ::%'", + ) + + +def downgrade(): + op.drop_constraint("ck_disallow_private_top_level_classifier")