Verified Commit db4c0c3f authored by Mattia Rizzolo's avatar Mattia Rizzolo
Browse files

Merge branch 'branch-coverage' of salsa.debian.org:lyknode/debexpo into live

MR: !187


Signed-off-by: Mattia Rizzolo's avatarMattia Rizzolo <mattia@debian.org>
parents 857b453f 6453d37e
Pipeline #335265 passed with stage
in 10 minutes and 57 seconds
...@@ -47,7 +47,7 @@ before_script: ...@@ -47,7 +47,7 @@ before_script:
script: script:
- python3 setup.py develop | tee /tmp/deps.txt - python3 setup.py develop | tee /tmp/deps.txt
- (! grep Downloading /tmp/deps.txt) - (! grep Downloading /tmp/deps.txt)
- python3 -m coverage run manage.py test -v 2 --exclude-tag nntp - python3 -m coverage run --branch manage.py test -v 2 --exclude-tag nntp
- python3 -m coverage report --include='debexpo*' --omit '*/nntp.py' - python3 -m coverage report --include='debexpo*' --omit '*/nntp.py'
- python3 -m coverage html --include='debexpo*' --omit '*/nntp.py' - python3 -m coverage html --include='debexpo*' --omit '*/nntp.py'
artifacts: artifacts:
......
...@@ -26,6 +26,7 @@ ...@@ -26,6 +26,7 @@
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE. # OTHER DEALINGS IN THE SOFTWARE.
from email.utils import getaddresses
from enum import Enum from enum import Enum
from django.contrib.auth.models import AbstractUser, BaseUserManager from django.contrib.auth.models import AbstractUser, BaseUserManager
...@@ -92,6 +93,28 @@ class UserManager(BaseUserManager): ...@@ -92,6 +93,28 @@ class UserManager(BaseUserManager):
return self._create_user(email, name, password, **extra_fields) return self._create_user(email, name, password, **extra_fields)
def lookup_user_from_address(self, address):
"""
Lookup a user using an arbitrary formatted email.
This method is used by Changes to lookup the user of an unsigned upload,
when allowed. The address is extracted from the .chagnes Changed-By
field or, if missing, from the Maintainer field.
The address format (Vincent Time <vtime@example.org>) is decoded using
email.utils.getaddresses.
"""
if not address:
return
decoded_address = getaddresses([address])
email = decoded_address[0][1]
try:
return self.get(email=email)
except User.DoesNotExist:
return
class User(AbstractUser): class User(AbstractUser):
"""User model.""" """User model."""
......
...@@ -278,6 +278,7 @@ class EmailChangeConfirmView(PasswordResetConfirmView): ...@@ -278,6 +278,7 @@ class EmailChangeConfirmView(PasswordResetConfirmView):
assert 'uidb64' in kwargs and 'token' in kwargs and 'email' in kwargs assert 'uidb64' in kwargs and 'token' in kwargs and 'email' in kwargs
self.validlink = False self.validlink = False
self.email = None
self.user = self.get_user(kwargs['uidb64']) self.user = self.get_user(kwargs['uidb64'])
if self.user is not None: if self.user is not None:
...@@ -307,7 +308,6 @@ class EmailChangeConfirmView(PasswordResetConfirmView): ...@@ -307,7 +308,6 @@ class EmailChangeConfirmView(PasswordResetConfirmView):
return self.render_to_response(self.get_context_data()) return self.render_to_response(self.get_context_data())
def form_valid(self, form): def form_valid(self, form):
if self.validlink:
self.user.email = self.email self.user.email = self.email
self.user.full_clean() self.user.full_clean()
self.user.save() self.user.save()
......
...@@ -79,7 +79,12 @@ def subscribe(request, name): ...@@ -79,7 +79,12 @@ def subscribe(request, name):
form = SubscriptionForm(request.POST, instance=instance) form = SubscriptionForm(request.POST, instance=instance)
package = request.POST.get('next') package = request.POST.get('next')
if form.is_valid(): # The condition here is just for show. The form is composed of two
# checkbox which are either absent for False or present with any value
# for True. It's not possible for it to be not valid.
#
# The test is kept here for future needs.
if form.is_valid(): # pragma: no branch
subscription = form.save(commit=False) subscription = form.save(commit=False)
subscription.user = request.user subscription.user = request.user
subscription.package = name subscription.package = name
...@@ -129,3 +134,9 @@ def comment(request, name): ...@@ -129,3 +134,9 @@ def comment(request, name):
comment.notify(request) comment.notify(request)
return HttpResponseRedirect(f"{redirect_url}#upload-{index}") return HttpResponseRedirect(f"{redirect_url}#upload-{index}")
return render(request, 'package.html', {
'settings': settings,
'package': package,
'comment_form': form,
})
...@@ -237,9 +237,7 @@ class Importer(): ...@@ -237,9 +237,7 @@ class Importer():
finally: finally:
changes.remove() changes.remove()
if self.repository:
self.repository.update() self.repository.update()
self.spool.cleanup() self.spool.cleanup()
return success return success
...@@ -411,7 +409,6 @@ class Importer(): ...@@ -411,7 +409,6 @@ class Importer():
git_ref = git_storage.install(source) git_ref = git_storage.install(source)
# Install to repository # Install to repository
if self.repository:
self.repository.install(changes) self.repository.install(changes)
# Create DB entries # Create DB entries
...@@ -424,8 +421,7 @@ class Importer(): ...@@ -424,8 +421,7 @@ class Importer():
# match their checksum # match their checksum
try: try:
changes.validate() changes.validate()
if not self.skip_gpg: changes.authenticate(not self.skip_gpg)
changes.authenticate()
if getattr(settings, 'CHECK_NEWER_UPLOAD', True): if getattr(settings, 'CHECK_NEWER_UPLOAD', True):
changes.assert_newer() changes.assert_newer()
changes.files.validate() changes.files.validate()
......
...@@ -59,13 +59,11 @@ def create_distributions(apps, schema_editor): ...@@ -59,13 +59,11 @@ def create_distributions(apps, schema_editor):
Project = apps.get_model('packages', 'Project') Project = apps.get_model('packages', 'Project')
for name, project in DISTRIBUTIONS: for name, project in DISTRIBUTIONS:
distribution = Distribution()
if project:
project, _ = Project.objects.get_or_create(name=project) project, _ = Project.objects.get_or_create(name=project)
distribution = Distribution()
distribution.name = name distribution.name = name
distribution.project = project distribution.project = project
distribution.full_clean() distribution.full_clean()
distribution.save() distribution.save()
......
...@@ -141,7 +141,6 @@ class Package(models.Model): ...@@ -141,7 +141,6 @@ class Package(models.Model):
return self.name return self.name
def get_description(self): def get_description(self):
if self.packageupload_set.count():
upload = self.packageupload_set.latest('uploaded') upload = self.packageupload_set.latest('uploaded')
if upload.binarypackage_set.count(): if upload.binarypackage_set.count():
...@@ -157,8 +156,6 @@ class Package(models.Model): ...@@ -157,8 +156,6 @@ class Package(models.Model):
def get_full_description(self): def get_full_description(self):
description = [] description = []
if self.packageupload_set.count():
upload = self.packageupload_set.latest('uploaded') upload = self.packageupload_set.latest('uploaded')
for binary in upload.binarypackage_set.all(): for binary in upload.binarypackage_set.all():
......
...@@ -81,7 +81,8 @@ class PackageGroups(object): ...@@ -81,7 +81,8 @@ class PackageGroups(object):
x for x in packages if x for x in packages if
x.packageupload_set.latest('uploaded').uploaded > deltamax x.packageupload_set.latest('uploaded').uploaded > deltamax
] ]
elif (deltamin is not None and deltamax is None): # Last actual possibility
elif (deltamin is not None and deltamax is None): # pragma: no branch
self.packages = [ self.packages = [
x for x in packages if x for x in packages if
x.packageupload_set.latest('uploaded').uploaded <= deltamin x.packageupload_set.latest('uploaded').uploaded <= deltamin
......
...@@ -38,20 +38,20 @@ from debexpo.tools.proc import debexpo_exec ...@@ -38,20 +38,20 @@ from debexpo.tools.proc import debexpo_exec
class PluginLintian(BasePlugin): class PluginLintian(BasePlugin):
levels = [ levels = {
{'level': 'X', 'name': 'Experimental', 'severity': PluginSeverity.info, 'X': {'name': 'Experimental', 'severity': PluginSeverity.info,
'outcome': 'Package has lintian experimental tags'}, 'outcome': 'Package has lintian experimental tags'},
{'level': 'P', 'name': 'Pedantic', 'severity': PluginSeverity.info, 'P': {'name': 'Pedantic', 'severity': PluginSeverity.info,
'outcome': 'Package has lintian pedantic tags'}, 'outcome': 'Package has lintian pedantic tags'},
{'level': 'O', 'name': 'Override', 'severity': PluginSeverity.info, 'O': {'name': 'Override', 'severity': PluginSeverity.info,
'outcome': 'Package has overridden lintian tags'}, 'outcome': 'Package has overridden lintian tags'},
{'level': 'I', 'name': 'Info', 'severity': PluginSeverity.info, 'I': {'name': 'Info', 'severity': PluginSeverity.info,
'outcome': 'Package has lintian informational tags'}, 'outcome': 'Package has lintian informational tags'},
{'level': 'W', 'name': 'Warning', 'severity': PluginSeverity.warning, 'W': {'name': 'Warning', 'severity': PluginSeverity.warning,
'outcome': 'Package has lintian warnings'}, 'outcome': 'Package has lintian warnings'},
{'level': 'E', 'name': 'Error', 'severity': PluginSeverity.error, 'E': {'name': 'Error', 'severity': PluginSeverity.error,
'outcome': 'Package has lintian errors'}, 'outcome': 'Package has lintian errors'},
] }
@property @property
def name(self): def name(self):
...@@ -102,15 +102,11 @@ class PluginLintian(BasePlugin): ...@@ -102,15 +102,11 @@ class PluginLintian(BasePlugin):
continue continue
severity, package, rest = tag.split(': ', 2) severity, package, rest = tag.split(': ', 2)
name = [level['name'] for level in self.levels
if level['level'] == severity]
lintian_severities.add(severity) lintian_severities.add(severity)
lintian_tag_data = rest.split() lintian_tag_data = rest.split()
lintian_tag = lintian_tag_data[0] lintian_tag = lintian_tag_data[0]
lintian_data = ' '.join(lintian_tag_data[1:]) lintian_data = ' '.join(lintian_tag_data[1:])
severity = f'{severity}-{self.levels[severity]["name"]}'
if name:
severity = f'{severity}-{name[0]}'
if override_comments: if override_comments:
comments = ' '.join(override_comments) comments = ' '.join(override_comments)
...@@ -124,8 +120,8 @@ class PluginLintian(BasePlugin): ...@@ -124,8 +120,8 @@ class PluginLintian(BasePlugin):
severity = PluginSeverity.info severity = PluginSeverity.info
outcome = 'Package is lintian clean' outcome = 'Package is lintian clean'
for level in self.levels: for name, level in self.levels.items():
if level['level'] in lintian_severities: if name in lintian_severities:
severity = level['severity'] severity = level['severity']
outcome = level['outcome'] outcome = level['outcome']
......
...@@ -43,10 +43,11 @@ class PluginMaintainerEmail(BasePlugin): ...@@ -43,10 +43,11 @@ class PluginMaintainerEmail(BasePlugin):
""" """
Tests whether the maintainer email is the same as the uploader email. Tests whether the maintainer email is the same as the uploader email.
""" """
if not changes.maintainer:
self.failed('No maintainer address found')
uploader_emails = [] uploader_emails = []
maintainer_emails = email.utils.getaddresses([changes.maintainer]) maintainer_emails = email.utils.getaddresses([changes.maintainer])
if maintainer_emails:
maintainer_email = maintainer_emails[0][1] maintainer_email = maintainer_emails[0][1]
if changes.dsc.uploaders: if changes.dsc.uploaders:
......
...@@ -86,9 +86,17 @@ class Changes(GPGSignedFile): ...@@ -86,9 +86,17 @@ class Changes(GPGSignedFile):
return False return False
def authenticate(self): def authenticate(self, gpg=True):
if gpg:
super().authenticate() super().authenticate()
self.uploader = User.objects.get(key=self.key) self.uploader = User.objects.get(key=self.key)
else:
user = User.objects.lookup_user_from_address(self.uploader)
if not user:
raise ExceptionChanges(f'No user found for {self.uploader}')
self.uploader = user
def _build_changes(self): def _build_changes(self):
self.dsc = None self.dsc = None
......
...@@ -78,7 +78,6 @@ class Copyright(): ...@@ -78,7 +78,6 @@ class Copyright():
return licenses return licenses
for paragraph in self.copyright.all_files_paragraphs(): for paragraph in self.copyright.all_files_paragraphs():
if paragraph.license and paragraph.license.synopsis:
if not paragraph.files == ('debian/*',): if not paragraph.files == ('debian/*',):
licenses.update([paragraph.license.synopsis]) licenses.update([paragraph.license.synopsis])
......
...@@ -60,7 +60,6 @@ class Origin(): ...@@ -60,7 +60,6 @@ class Origin():
# For the origin tarball and its signature # For the origin tarball and its signature
for origin in origin_files: for origin in origin_files:
if origin:
if isfile(join(self.dest_dir, str(origin))): if isfile(join(self.dest_dir, str(origin))):
# Origin file is present in the upload # Origin file is present in the upload
continue continue
......
...@@ -56,7 +56,9 @@ class GitStorage(): ...@@ -56,7 +56,9 @@ class GitStorage():
return ref return ref
def remove(self): def remove(self):
if isdir(self.repository): # Exclude the False branch from coverage since having the repository
# removed is unlikely to happen and would be an external action
if isdir(self.repository): # pragma: no branch
rmtree(self.repository) rmtree(self.repository)
# def diff(self, upload_from, upload_to): # def diff(self, upload_from, upload_to):
...@@ -107,7 +109,9 @@ class GitBackendDulwich(): ...@@ -107,7 +109,9 @@ class GitBackendDulwich():
files.update(set(get_unstaged_changes(self.repo.open_index(), files.update(set(get_unstaged_changes(self.repo.open_index(),
'sources'))) 'sources')))
if len(files) != 0: # Exclude the False branch from coverage. In practice, there is always
# files present in debian/ with accepted packages
if len(files) != 0: # pragma: no branch
self.repo.stage(list(files)) self.repo.stage(list(files))
def commit(self): def commit(self):
......
...@@ -100,7 +100,7 @@ class GnuPG(): ...@@ -100,7 +100,7 @@ class GnuPG():
"""Returns true if the gpg binary is not installed or not executable.""" """Returns true if the gpg binary is not installed or not executable."""
return self.gpg_path is None return self.gpg_path is None
def get_keys_data(self, fingerprints=None): def get_keys_data(self):
""" """
Returns the key object of the given GPG public key. Returns the key object of the given GPG public key.
...@@ -109,11 +109,8 @@ class GnuPG(): ...@@ -109,11 +109,8 @@ class GnuPG():
""" """
if fingerprints is None:
fingerprints = []
try: try:
(output, status) = self._run(args=['--list-keys'] + fingerprints) (output, status) = self._run(['--list-keys'])
keys = KeyData.read_from_gpg(output.splitlines()) keys = KeyData.read_from_gpg(output.splitlines())
return list(keys.values()) return list(keys.values())
...@@ -129,8 +126,8 @@ class GnuPG(): ...@@ -129,8 +126,8 @@ class GnuPG():
``signed_file`` ``signed_file``
path to signed file path to signed file
""" """
args = ('--verify', signed_file) args = ['--verify', signed_file]
(output, status) = self._run(args=args) (output, status) = self._run(args)
output = output.splitlines() output = output.splitlines()
err_sig_re = re.compile(r'\[GNUPG:\] ERRSIG (?P<long_id>\w+)' err_sig_re = re.compile(r'\[GNUPG:\] ERRSIG (?P<long_id>\w+)'
...@@ -175,7 +172,7 @@ class GnuPG(): ...@@ -175,7 +172,7 @@ class GnuPG():
""" """
args = ['--import-options', 'import-minimal', '--import'] args = ['--import-options', 'import-minimal', '--import']
(output, status) = self._run(args=args, stdin=data) (output, status) = self._run(args, stdin=data)
if status and output and len(output.splitlines()) > 0: if status and output and len(output.splitlines()) > 0:
raise ExceptionGnuPG(_('Cannot add key:' raise ExceptionGnuPG(_('Cannot add key:'
...@@ -183,7 +180,7 @@ class GnuPG(): ...@@ -183,7 +180,7 @@ class GnuPG():
return (output, status) return (output, status)
def _run(self, stdin=None, args=None): def _run(self, args, stdin=None):
""" """
Run gpg with the given stdin and arguments and return the output and Run gpg with the given stdin and arguments and return the output and
exit status. exit status.
...@@ -217,10 +214,7 @@ class GnuPG(): ...@@ -217,10 +214,7 @@ class GnuPG():
'--trust-model', 'always', '--trust-model', 'always',
'--with-colons', '--with-colons',
'--with-fingerprint' '--with-fingerprint'
] ] + args
if args is not None:
cmd.extend(args)
try: try:
output = debexpo_exec(self.gpg_path, cmd, env=env, output = debexpo_exec(self.gpg_path, cmd, env=env,
...@@ -247,8 +241,6 @@ class KeyData(): ...@@ -247,8 +241,6 @@ class KeyData():
def get_uid(self, uid): def get_uid(self, uid):
uidfpr = uid[7] uidfpr = uid[7]
res = self.uids.get(uidfpr, None)
if res is None:
self.uids[uidfpr] = res = Uid(self, uid) self.uids[uidfpr] = res = Uid(self, uid)
return res return res
...@@ -296,7 +288,6 @@ class KeyData(): ...@@ -296,7 +288,6 @@ class KeyData():
# Correlate fpr with the previous pub record, and start # Correlate fpr with the previous pub record, and start
# gathering information for a new key # gathering information for a new key
if pub: if pub:
if cur_key is None:
keys[fpr] = cur_key = cls(fpr, pub) keys[fpr] = cur_key = cls(fpr, pub)
pub_fpr = fpr pub_fpr = fpr
pub = None pub = None
......
...@@ -446,6 +446,16 @@ psCWDYcNhNTEmXgsDSqUlLrqqde/3hDynhWeAP9jk7QAnDToELBdyCe6HpGXLC3q ...@@ -446,6 +446,16 @@ psCWDYcNhNTEmXgsDSqUlLrqqde/3hDynhWeAP9jk7QAnDToELBdyCe6HpGXLC3q
# Login # Login
self.client.post(reverse('login'), self._AUTHDATA) self.client.post(reverse('login'), self._AUTHDATA)
# Test password change, failed
response = self.client.post(reverse('profile'), {
'old_password': 'not-my-password',
'new_password1': 'newpassword',
'new_password2': 'newpassword',
'commit_password': 'submit'
})
self.assertEquals(response.status_code, 200)
self.assertIn('errorlist', str(response.content))
# Test password change # Test password change
response = self.client.post(reverse('profile'), { response = self.client.post(reverse('profile'), {
'old_password': 'password', 'old_password': 'password',
......
...@@ -86,13 +86,16 @@ class TestUpdateEmail(TransactionTestController): ...@@ -86,13 +86,16 @@ class TestUpdateEmail(TransactionTestController):
return token return token
def _submit_token(self, token, email, invalid=False, error=None, def _submit_token(self, token, email, invalid=False, error=None,
redirect=None): redirect=None, submit_data=None):
if not submit_data:
user = User.objects.get(email=self._AUTHDATA['username']) user = User.objects.get(email=self._AUTHDATA['username'])
submit_data = { submit_data = {
'uidb64': urlsafe_base64_encode(force_bytes(user.pk)), 'uidb64': urlsafe_base64_encode(force_bytes(user.pk)),
'token': token, 'token': token,
'email': email, 'email': email,
} }
submit_url = reverse('email_change_confirm', kwargs=submit_data) submit_url = reverse('email_change_confirm', kwargs=submit_data)
response = self.client.get(submit_url) response = self.client.get(submit_url)
...@@ -173,6 +176,28 @@ class TestUpdateEmail(TransactionTestController): ...@@ -173,6 +176,28 @@ class TestUpdateEmail(TransactionTestController):
self._submit_token(token, 'new@example.org', self._submit_token(token, 'new@example.org',
error='address already exists') error='address already exists')
def test_update_email_invalid_token(self):
# On first call
invalid_data = {
'uidb64': 'a',
'token': 'x-x',
'email': 'a',
}
self._submit_token(None, 'new@example.org',
invalid=True, submit_data=invalid_data)
# On redirect call
user = User.objects.get(email=self._AUTHDATA['username'])
invalid_data = {
'uidb64': urlsafe_base64_encode(force_bytes(user.pk)),
'token': 'change-email',
'email': 'another@example.org',
}
self._submit_token(None, 'new@example.org',
invalid=True, submit_data=invalid_data)
def test_update_email_mismatch_email(self): def test_update_email_mismatch_email(self):
token = self._get_update_token('new@example.org') token = self._get_update_token('new@example.org')
......
...@@ -26,6 +26,8 @@ ...@@ -26,6 +26,8 @@
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE. # OTHER DEALINGS IN THE SOFTWARE.
from os import environ
from django.test import TestCase from django.test import TestCase
from django.test import Client from django.test import Client
...@@ -43,6 +45,19 @@ PAGES = ( ...@@ -43,6 +45,19 @@ PAGES = (
class TestBrowsingPage(TestCase): class TestBrowsingPage(TestCase):
def test_debexpo_no_version(self):
client = Client()
path = environ['PATH']
environ['PATH'] = ''
try:
response = client.get('/')
finally:
environ['PATH'] = path
self.assertEquals(response.status_code, 200)
self.assertNotIn('Version', str(response.content))
def test_page_content(self): def test_page_content(self):
client = Client() client = Client()
......
...@@ -155,7 +155,7 @@ class TestImporterController(TestController): ...@@ -155,7 +155,7 @@ class TestImporterController(TestController):
skip_email=False, base_dir=None, sub_dir=None): skip_email=False, base_dir=None, sub_dir=None):
source_package = TestSourcePackage(package_dir, base_dir) source_package = TestSourcePackage(package_dir, base_dir)
source_package.build() source_package.build(not skip_gpg)
self._run_importer(source_package.get_package_dir(), skip_gpg=skip_gpg, self._run_importer(source_package.get_package_dir(), skip_gpg=skip_gpg,
skip_email=skip_email, sub_dir=sub_dir) skip_email=skip_email, sub_dir=sub_dir)
......
...@@ -10,10 +10,7 @@ Maintainer: Baptiste BEAUPLAT <lyknode@cilg.org> ...@@ -10,10 +10,7 @@ Maintainer: Baptiste BEAUPLAT <lyknode@cilg.org>
Changed-By: Another Uploader <another@example.org> Changed-By: Another Uploader <another@example.org>
Description: Description:
hello - <insert up to 60 chars description> hello - <insert up to 60 chars description>
Changes: Changes: Single line changes
hello (1.0-1) unstable; urgency=medium
.
* Initial release (Closes: #nnnn) <nnnn is the bug number of your ITP>
Checksums-Sha1: Checksums-Sha1:
12b9785a45d1400abc684af9964fed348b2fcf9d 784 hello_1.0-1.dsc 12b9785a45d1400abc684af9964fed348b2fcf9d 784 hello_1.0-1.dsc
be505a70074ab85fed3f68026242651fcde85bf2 168 hello_1.0.orig.tar.xz be505a70074ab85fed3f68026242651fcde85bf2 168 hello_1.0.orig.tar.xz
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment