Skip to content

Commit

Permalink
chore(backend): increase coverage (#9039)
Browse files Browse the repository at this point in the history
* move version tests

* factor out read_license_file

* add testing for license file

* ignore special case when we create the schema

* extent no found api tests

* extend info view tests

* try fixing test?

* fix?

* test user create api

* measure impact of removing bom import

* remove dead code

* Revert "measure impact of removing bom import"

This reverts commit bb31db0.

* remove dead code

* remove plugin tags that were made for CUI

* add testing for filters

* add test for config delete

* add more api tests

* adjust tests

* fix test

* use superuser

* adapt error code

* Add test for #9077

* add mixin_available mixin

* make check_reload more observable

* test check_reload too

* test clean_barcode

* reset after testing

* extend datamatrix testing

* debug print

* fix assertation
  • Loading branch information
matmair authored Feb 17, 2025
1 parent ed4240a commit 4a9138c
Show file tree
Hide file tree
Showing 14 changed files with 284 additions and 239 deletions.
88 changes: 43 additions & 45 deletions src/backend/InvenTree/InvenTree/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,60 +35,58 @@
logger = structlog.get_logger('inventree')


class LicenseViewSerializer(serializers.Serializer):
"""Serializer for license information."""
def read_license_file(path: Path) -> list:
"""Extract license information from the provided file.
backend = serializers.CharField(help_text='Backend licenses texts', read_only=True)
frontend = serializers.CharField(
help_text='Frontend licenses texts', read_only=True
)
Arguments:
path: Path to the license file
Returns: A list of items containing the license information
"""
# Check if the file exists
if not path.exists():
logger.error("License file not found at '%s'", path)
return []

class LicenseView(APIView):
"""Simple JSON endpoint for InvenTree license information."""
try:
data = json.loads(path.read_text())
except Exception as e:
logger.exception("Failed to parse license file '%s': %s", path, e)
return []

permission_classes = [permissions.IsAuthenticated]
output = []
names = set()

def read_license_file(self, path: Path) -> list:
"""Extract license information from the provided file.
# Ensure we do not have any duplicate 'name' values in the list
for entry in data:
name = None
for key in entry:
if key.lower() == 'name':
name = entry[key]
break

Arguments:
path: Path to the license file
if name is None or name in names:
continue

Returns: A list of items containing the license information
"""
# Check if the file exists
if not path.exists():
logger.error("License file not found at '%s'", path)
return []
names.add(name)
output.append({key.lower(): value for key, value in entry.items()})

return output

try:
data = json.loads(path.read_text())
except json.JSONDecodeError as e:
logger.exception("Failed to parse license file '%s': %s", path, e)
return []
except Exception as e:
logger.exception("Exception while reading license file '%s': %s", path, e)
return []

output = []
names = set()
class LicenseViewSerializer(serializers.Serializer):
"""Serializer for license information."""

# Ensure we do not have any duplicate 'name' values in the list
for entry in data:
name = None
for key in entry:
if key.lower() == 'name':
name = entry[key]
break
backend = serializers.CharField(help_text='Backend licenses texts', read_only=True)
frontend = serializers.CharField(
help_text='Frontend licenses texts', read_only=True
)

if name is None or name in names:
continue

names.add(name)
output.append({key.lower(): value for key, value in entry.items()})
class LicenseView(APIView):
"""Simple JSON endpoint for InvenTree license information."""

return output
permission_classes = [permissions.IsAuthenticated]

@extend_schema(responses={200: OpenApiResponse(response=LicenseViewSerializer)})
def get(self, request, *args, **kwargs):
Expand All @@ -98,8 +96,8 @@ def get(self, request, *args, **kwargs):
'web/static/web/.vite/dependencies.json'
)
return JsonResponse({
'backend': self.read_license_file(backend),
'frontend': self.read_license_file(frontend),
'backend': read_license_file(backend),
'frontend': read_license_file(frontend),
})


Expand Down Expand Up @@ -595,7 +593,7 @@ def get_model_type(self):
if model is None:
raise ValidationError(
f"MetadataView called without '{self.MODEL_REF}' parameter"
)
) # pragma: no cover

return model

Expand All @@ -611,5 +609,5 @@ def get_serializer(self, *args, **kwargs):
"""Return MetadataSerializer instance."""
# Detect if we are currently generating the OpenAPI schema
if 'spectacular' in sys.argv:
return MetadataSerializer(Part, *args, **kwargs)
return MetadataSerializer(Part, *args, **kwargs) # pragma: no cover
return MetadataSerializer(self.get_model_type(), *args, **kwargs)
31 changes: 2 additions & 29 deletions src/backend/InvenTree/InvenTree/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,14 @@
import os.path
import re
from decimal import Decimal, InvalidOperation
from pathlib import Path
from typing import Optional, TypeVar, Union
from typing import Optional, TypeVar
from wsgiref.util import FileWrapper
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError

from django.conf import settings
from django.contrib.staticfiles.storage import StaticFilesStorage
from django.core.exceptions import FieldError, ValidationError
from django.core.files.storage import Storage, default_storage
from django.core.files.storage import default_storage
from django.http import StreamingHttpResponse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
Expand Down Expand Up @@ -235,22 +234,6 @@ def str2bool(text, test=True):
return str(text).lower() in ['0', 'n', 'no', 'none', 'f', 'false', 'off']


def str2int(text, default=None):
"""Convert a string to int if possible.
Args:
text: Int like string
default: Return value if str is no int like
Returns:
Converted int value
"""
try:
return int(text)
except Exception:
return default


def is_bool(text):
"""Determine if a string value 'looks' like a boolean."""
return str2bool(text, True) or str2bool(text, False)
Expand Down Expand Up @@ -902,16 +885,6 @@ def hash_barcode(barcode_data: str) -> str:
return str(barcode_hash.hexdigest())


def hash_file(filename: Union[str, Path], storage: Union[Storage, None] = None):
"""Return the MD5 hash of a file."""
content = (
open(filename, 'rb').read() # noqa: SIM115
if storage is None
else storage.open(str(filename), 'rb').read()
)
return hashlib.md5(content).hexdigest()


def current_time(local=True):
"""Return the current date and time as a datetime object.
Expand Down
1 change: 1 addition & 0 deletions src/backend/InvenTree/InvenTree/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@

PLUGIN_TESTING_EVENTS = False # Flag if events are tested right now
PLUGIN_TESTING_EVENTS_ASYNC = False # Flag if events are tested asynchronously
PLUGIN_TESTING_RELOAD = False # Flag if plugin reloading is in testing (check_reload)

PLUGIN_RETRY = get_setting(
'INVENTREE_PLUGIN_RETRY', 'PLUGIN_RETRY', 3, typecast=int
Expand Down
118 changes: 100 additions & 18 deletions src/backend/InvenTree/InvenTree/test_api.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
"""Low level tests for the InvenTree API."""

from base64 import b64encode
from pathlib import Path
from tempfile import TemporaryDirectory

from django.urls import reverse

from rest_framework import status

from InvenTree.api import read_license_file
from InvenTree.api_version import INVENTREE_API_VERSION
from InvenTree.unit_test import InvenTreeAPITestCase, InvenTreeTestCase
from InvenTree.version import inventreeApiText, parse_version_text
from users.models import RuleSet, update_group_roles


Expand Down Expand Up @@ -53,13 +58,15 @@ def test_company_list(self):
self.assertEqual(response.status_code, 200)

def test_not_found(self):
"""Test that the NotFoundView is working."""
response = self.client.get('/api/anc')
self.assertEqual(response.status_code, 404)
"""Test that the NotFoundView is working with all available methods."""
methods = ['options', 'get', 'post', 'patch', 'put', 'delete']
for method in methods:
response = getattr(self.client, method)('/api/anc')
self.assertEqual(response.status_code, 404)


class APITests(InvenTreeAPITestCase):
"""Tests for the InvenTree API."""
class ApiAccessTests(InvenTreeAPITestCase):
"""Tests for various access scenarios with the InvenTree API."""

fixtures = ['location', 'category', 'part', 'stock']
roles = ['part.view']
Expand Down Expand Up @@ -100,19 +107,6 @@ def test_token_success(self):
self.tokenAuth()
self.assertIsNotNone(self.token)

def test_info_view(self):
"""Test that we can read the 'info-view' endpoint."""
url = reverse('api-inventree-info')

response = self.get(url)

data = response.json()
self.assertIn('server', data)
self.assertIn('version', data)
self.assertIn('instance', data)

self.assertEqual('InvenTree', data['server'])

def test_role_view(self):
"""Test that we can access the 'roles' view for the logged in user.
Expand Down Expand Up @@ -421,3 +415,91 @@ def test_permissions(self):
self.assertEqual(
result['error'], 'User does not have permission to view this model'
)


class GeneralApiTests(InvenTreeAPITestCase):
"""Tests for various api endpoints."""

def test_api_version(self):
"""Test that the API text is correct."""
url = reverse('api-version-text')
response = self.get(url, format='json')
data = response.json()

self.assertEqual(len(data), 10)

response = self.get(reverse('api-version')).json()
self.assertIn('version', response)
self.assertIn('dev', response)
self.assertIn('up_to_date', response)

def test_inventree_api_text_fnc(self):
"""Test that the inventreeApiText function works expected."""
# Normal run
resp = inventreeApiText()
self.assertEqual(len(resp), 10)

# More responses
resp = inventreeApiText(20)
self.assertEqual(len(resp), 20)

# Specific version
resp = inventreeApiText(start_version=5)
self.assertEqual(list(resp)[0], 'v5')

def test_parse_version_text_fnc(self):
"""Test that api version text is correctly parsed."""
resp = parse_version_text()

# Check that all texts are parsed
self.assertEqual(len(resp), INVENTREE_API_VERSION - 1)

def test_api_license(self):
"""Test that the license endpoint is working."""
response = self.get(reverse('api-license')).json()
self.assertIn('backend', response)
self.assertIn('frontend', response)

# Various problem cases
# File does not exist
with self.assertLogs(logger='inventree', level='ERROR') as log:
respo = read_license_file(Path('does not exsist'))
self.assertEqual(respo, [])

self.assertIn('License file not found at', str(log.output))

with TemporaryDirectory() as tmp:
sample_file = Path(tmp, 'temp.txt')
sample_file.write_text('abc')

# File is not a json
with self.assertLogs(logger='inventree', level='ERROR') as log:
respo = read_license_file(sample_file)
self.assertEqual(respo, [])

self.assertIn('Failed to parse license file', str(log.output))

def test_info_view(self):
"""Test that we can read the 'info-view' endpoint."""
url = reverse('api-inventree-info')

response = self.get(url)

data = response.json()
self.assertIn('server', data)
self.assertIn('version', data)
self.assertIn('instance', data)

self.assertEqual('InvenTree', data['server'])

# Test with token
token = self.get(url=reverse('api-token')).data['token']
self.client.logout()

# Anon
response = self.get(url)
self.assertEqual(response.json()['database'], None)

# Staff
response = self.get(url, headers={'Authorization': f'Token {token}'})
self.assertGreater(len(response.json()['database']), 4)
45 changes: 0 additions & 45 deletions src/backend/InvenTree/InvenTree/test_api_version.py

This file was deleted.

Loading

0 comments on commit 4a9138c

Please sign in to comment.