Files
2023-10-09 17:21:03 +02:00

315 lines
12 KiB
Python

from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase, Client
from django.urls import reverse
from winget.models import Package, Version, Installer
from winget.tests.util import create_tenant
class APITest(TestCase):
def setUp(self):
super().setUp()
self.client = Client()
self.tenant = create_tenant()
def test_information_required_parts(self):
data = self._get('information')['Data']
self.assertEqual('api.winget.pro', data['SourceIdentifier'])
self.assertEqual(['1.4.0', '1.5.0'], data['ServerSupportedVersions'])
def test_search_all_empty(self):
# Simulate `winget search` without any extra parameters.
data = self._post('manifestSearch', {'FetchAllManifests': True})['Data']
self.assertEqual([], data)
def test_search(self, request=None):
if request is None:
# Simulate `winget search 'visual studio code'`.
request = {
'Query': {
'KeyWord': 'visual studio code',
'MatchType': 'Substring'
}
}
data = self._post('manifestSearch', request)['Data']
self.assertEqual([], data)
package, version = self._create_vscode()[:2]
# Create another package that should not be returned:
self._create_powertoys()
data = self._post('manifestSearch', request)['Data']
self.assertEqual(1, len(data))
result, = data
self.assertEqual(package.identifier, result['PackageIdentifier'])
self.assertEqual(package.name, result['PackageName'])
self.assertEqual(package.publisher, result['Publisher'])
self.assertEqual(
[{'PackageVersion': version.version}], result['Versions']
)
# Test that the package isn't returned when it has no version. The
# motivation for this is that `winget search` gives an error when the
# REST source returns a package with 'Versions' empty or nonexistent.
version.delete()
data = self._post('manifestSearch', request)['Data']
self.assertEqual([], data)
def test_search_by_id(self):
# Simulate `winget search --id XP9KHM4BK9FZ7Q`.
request = {
'Filters': [{
'PackageMatchField': 'PackageIdentifier',
'RequestMatch': {
'KeyWord': 'XP9KHM4BK9FZ7Q',
'MatchType': 'Substring'
}
}]
}
self.test_search(request)
def test_list_source(self):
# Simulate `winget list` without any extra parameters.
# Create some apps to test that we do not return superfluous results:
self._create_vscode()
self._create_powertoys()
data = self._post('manifestSearch', {
'Inclusions': [
{
'PackageMatchField': 'ProductCode',
'RequestMatch': {
'KeyWord': 'microsoft edge',
'MatchType': 'Exact'
}
},
{
'PackageMatchField': 'NormalizedPackageNameAndPublisher',
'RequestMatch': {
'KeyWord': 'microsoftedge',
'MatchType': 'Exact'
}
}
]
})['Data']
self.assertEqual([], data)
data = self._post('manifestSearch', {
'Inclusions': [
{
'PackageMatchField': 'PackageFamilyName',
'RequestMatch': {
'KeyWord':
'microsoft.microsoftedge.stable_8wekyb3d8bbwe',
'MatchType': 'Exact'
}
}
]
})['Data']
self.assertEqual([], data)
def _create_vscode(self, scope='machine', is_nested=False, **kwargs):
package = Package.objects.create(
tenant=self.tenant, identifier='XP9KHM4BK9FZ7Q',
name='Visual Studio Code',
description='Free, lightweight, extensible code editor.',
publisher='Microsoft Corporation'
)
version = Version.objects.create(version='Unknown', package=package)
installer = Installer.objects.create(
version=version, architecture='x64', scope=scope,
type='zip' if is_nested else 'exe',
file=SimpleUploadedFile(
'vscode-winsta11er-x64.' + 'zip' if is_nested else 'exe', b'1'
),
sha256='6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb78'
'75b4b',
nested_installer='nested.exe' if is_nested else '',
nested_installer_type='exe' if is_nested else None,
**kwargs
)
return package, version, installer
def _create_powertoys(self):
package = Package.objects.create(
tenant=self.tenant, identifier='XP89DCGQ3K6VLD',
name='Microsoft PowerToys',
description='A set of utilities for power users.',
publisher='Microsoft Corporation'
)
version = Version.objects.create(version='Unknown', package=package)
installer = Installer.objects.create(
version=version, architecture='x64', type='exe',
file=SimpleUploadedFile('PowerToysSetup-0.51.1-x64.exe', b'2'),
sha256='d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec1'
'3ab35'
)
return package, version, installer
def test_search_during_install(self):
# Simulate the search part of `winget install "visual studio code"`.
request = {
'Inclusions': [
{
'PackageMatchField': 'PackageFamilyName',
'RequestMatch': {
'KeyWord': 'visual studio code',
'MatchType': 'Exact'
}
},
{
'PackageMatchField': 'ProductCode',
'RequestMatch': {
'KeyWord': 'visual studio code',
'MatchType': 'Exact'
}
},
{
'PackageMatchField': 'PackageIdentifier',
'RequestMatch': {
'KeyWord': 'visual studio code',
'MatchType': 'CaseInsensitive'
}
},
{
'PackageMatchField': 'PackageName',
'RequestMatch': {
'KeyWord': 'visual studio code',
'MatchType': 'CaseInsensitive'
}
},
{
'PackageMatchField': 'Moniker',
'RequestMatch': {
'KeyWord': 'visual studio code',
'MatchType': 'CaseInsensitive'
}
}
]
}
self.test_search(request)
def test_manifests_during_install(self):
# Simulate /packageManifests/<Identifier> during `winget install ...`.
package, version, installer = self._create_vscode()
resp = self._get('packageManifests', identifier=package.identifier)
result = resp['Data']
self.assertEqual(package.identifier, result['PackageIdentifier'])
self.assertEqual(1, len(result['Versions']))
version_json, = result['Versions']
self.assertEqual(version.version, version_json['PackageVersion'])
locale = version_json['DefaultLocale']
self.assertIn('PackageLocale', locale)
self.assertEqual(package.publisher, locale['Publisher'])
self.assertEqual(package.name, locale['PackageName'])
self.assertEqual(package.description, locale['ShortDescription'])
installer_json, = version_json['Installers']
self._check_vscode_installer_json(installer, installer_json)
def test_nonexistent_manifest_returns_204_not_404(self):
# This is a peculiarity / inconsistency of the winget client. The API
# design docs say that packageManifests should return 404 when a package
# does not exist. But winget doesn't gracefully handle this case.
# Instead, it expects HTTP 204.
# See: https://github.com/microsoft/winget-cli-restsource/issues/170
url = self._reverse('packageManifests', {'identifier': 'nonexistent'})
response = self.client.get(url)
self.assertEqual(204, response.status_code)
def test_installer_switch_silent(self):
self._test_installer_switch('silent_switch', 'Silent')
def test_installer_switch_silent_progress(self):
self._test_installer_switch(
'silent_progress_switch', 'SilentWithProgress'
)
def test_installer_switch_interactive(self):
self._test_installer_switch('interactive_switch', 'Interactive')
def test_installer_switch_install_location(self):
self._test_installer_switch(
'install_location_switch', 'InstallLocation'
)
def test_installer_switch_log(self):
self._test_installer_switch('log_switch', 'Log')
def test_installer_switch_upgrade(self):
self._test_installer_switch('upgrade_switch', 'Upgrade')
def test_installer_switch_custom(self):
self._test_installer_switch('custom_switch', 'Custom')
def _test_installer_switch(self, model_field, protocol_field):
package, version, installer = self._create_vscode(**{model_field: '/T'})
resp = self._get('packageManifests', identifier=package.identifier)
self.assertEqual(
{protocol_field: '/T'},
resp['Data']['Versions'][0]['Installers'][0]['InstallerSwitches']
)
def _check_vscode_installer_json(
self, installer, installer_json, scope=None, is_nested=False
):
if scope is None:
scope = installer.scope
self.assertEqual(installer.architecture, installer_json['Architecture'])
self.assertEqual(installer.type, installer_json['InstallerType'])
self.assertEqual(
f'http://testserver{installer.file.url}',
installer_json['InstallerUrl']
)
self.assertEqual(installer.sha256, installer_json['InstallerSha256'])
self.assertEqual(scope, installer_json['Scope'])
expected_nested_files = []
if is_nested:
expected_nested_files.append(
{'RelativeFilePath': installer.nested_installer}
)
self.assertEqual(
# We use .get(...) because the field may be omitted.
installer_json.get('NestedInstallerFiles', []),
expected_nested_files
)
self.assertEqual(
installer.nested_installer_type,
# We use .get(...) because the field may be omitted.
installer_json.get('NestedInstallerType')
)
def test_scope_both(self):
package, version, installer = self._create_vscode(scope='both')
resp = self._get('packageManifests', identifier=package.identifier)
version_json, = resp['Data']['Versions']
installers_json = version_json['Installers']
self.assertEqual(2, len(installers_json)) # One for each scope
machine, user = sorted(installers_json, key=lambda i: i['Scope'])
self._check_vscode_installer_json(installer, machine, 'machine')
self._check_vscode_installer_json(installer, user, 'user')
def test_nested_installer(self):
package, version, installer = self._create_vscode(is_nested=True)
resp = self._get('packageManifests', identifier=package.identifier)
version_json, = resp['Data']['Versions']
installer_json, = version_json['Installers']
self._check_vscode_installer_json(
installer, installer_json, is_nested=True
)
def _get(self, url_name, expect_status=200, **kwargs):
response = self.client.get(self._reverse(url_name, kwargs))
self.assertEqual(expect_status, response.status_code)
return response.json()
def _post(self, url_name, data, expect_status=200):
path = self._reverse(url_name)
response = self.client.post(path, data, content_type='application/json')
self.assertEqual(expect_status, response.status_code)
return response.json()
def _reverse(self, url_name, kwargs=None):
new_kwargs = dict(kwargs or {})
new_kwargs['tenant_uuid'] = str(self.tenant.uuid)
return reverse(f'winget:{url_name}', kwargs=new_kwargs)