Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • geant-swd/sage-validation
1 result
Show changes
Commits on Source (27)
Showing
with 263 additions and 38 deletions
# Changelog # Changelog
## [0.6] - 2025-03-14
-Added SOCIAL_AUTH_REDIRECT_IS_HTTPS
## [0.5] - 2025-03-14
- Improved CSV eport functionality
- Added Google authentication support
- Set up PstgreSQL as the default DB
- Imporved file export name
- Added unittests
## [0.4] - 2025-02-13 ## [0.4] - 2025-02-13
- Added CSV Export and Modify Functionality - Added CSV Export and Modify Functionality
......
Django==5.0.11 Django>=5.0,<5.1
django-rest-framework djangorestframework==3.15.2
ruff ruff
mypy mypy
tox tox
...@@ -11,3 +11,5 @@ pytest-django ...@@ -11,3 +11,5 @@ pytest-django
pytest-mock pytest-mock
faker faker
coverage coverage
social-auth-app-django==5.4.3
psycopg[binary,pool]
\ No newline at end of file
"""Admin configuration for the UserActivityLog model."""
from django.contrib import admin
from sage_validation.accounts.models import UserActivityLog
@admin.register(UserActivityLog)
class UserActivityLogAdmin(admin.ModelAdmin):
"""Admin configuration for the UserActivityLog model."""
list_display = ("user", "action", "name", "input_file_hash", "output_file_hash", "timestamp")
search_fields = ("user__username", "name", "action")
list_filter = ("action", "timestamp")
"""Django app configuration for the accounts' app."""
from django.apps import AppConfig
class AccountsConfig(AppConfig):
"""App configuration for the accounts' app."""
default_auto_field = "django.db.models.BigAutoField"
name = "sage_validation.accounts"
# Generated by Django 5.0.11 on 2025-03-03 09:09
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="UserActivityLog",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"action",
models.CharField(
choices=[("upload", "Upload"), ("download", "Download")],
max_length=10,
),
),
("name", models.CharField(max_length=255)),
(
"input_file_hash",
models.CharField(blank=True, max_length=64, null=True),
),
(
"output_file_hash",
models.CharField(blank=True, max_length=64, null=True),
),
("timestamp", models.DateTimeField(auto_now_add=True)),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="activity_logs",
to=settings.AUTH_USER_MODEL,
),
),
],
),
]
"""Models for the accounts app."""
import hashlib
from typing import ClassVar
from django.contrib.auth.models import User
from django.db import models
class UserActivityLog(models.Model):
"""Model to log user activities."""
ACTION_CHOICES: ClassVar[list[tuple[str, str]]] = [
("upload", "Upload"),
("download", "Download"),
]
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="activity_logs")
action = models.CharField(max_length=10, choices=ACTION_CHOICES)
name = models.CharField(max_length=255)
input_file_hash = models.CharField(max_length=64, blank=True, null=True)
output_file_hash = models.CharField(max_length=64, blank=True, null=True)
timestamp = models.DateTimeField(auto_now_add=True)
def __str__(self) -> str:
"""Return a string representation of the UserActivityLog object."""
return f"{self.user.username} - {self.action} - {self.name} ({self.timestamp})"
@staticmethod
def generate_file_hash(file_obj: object) -> str:
"""Generate SHA-256 hash for a file-like object."""
hasher = hashlib.sha256()
if hasattr(file_obj, "chunks"):
for chunk in file_obj.chunks():
hasher.update(chunk)
elif isinstance(file_obj, str):
hasher.update(file_obj.encode("utf-8"))
elif isinstance(file_obj, list):
hasher.update(str(file_obj).encode("utf-8"))
else:
err = "generate_file_hash() expected a file, string, or list."
raise TypeError(err)
return hasher.hexdigest()
"""URL configuration for the accounts' app."""
from django.contrib.auth.views import LogoutView
from django.urls import path
urlpatterns = [
path("logout/", LogoutView.as_view(), name="logout"),
]
"""Views for accounts app."""
"""Forms for the file_validator app.""" """Forms for the file_validator app."""
import csv import csv
import re
from collections.abc import Sequence from collections.abc import Sequence
from typing import ClassVar from typing import ClassVar
...@@ -169,13 +170,18 @@ class CSVUploadForm(forms.Form): ...@@ -169,13 +170,18 @@ class CSVUploadForm(forms.Form):
continue continue
pl_account_name = account_code_map.get(account_code) pl_account_name = account_code_map.get(account_code)
if pl_account_name is None: if pl_account_name is None:
errors.append(f"Row {index}: 'AccountNumber' {account_code} does not exist in PL Account Codes.") errors.append(f"Row {index}: 'AccountNumber' {account_code} does not exist in PL Account Codes.")
elif pl_account_name not in nominal: else:
errors.append( # Remove 'Soldo' and any hyphens from the PL account name. This is for credit card accounts.
f"Row {index}: 'AccountNumber' must match '{pl_account_name}' in " revised_pl_account_name = re.sub(
f"'NominalAnalysisNominalAnalysisNarrative/1', but found '{nominal}'." r"\bSoldo\b|\s*-\s*", "", pl_account_name, flags=re.IGNORECASE).strip()
) if revised_pl_account_name not in nominal:
errors.append(
f"Row {index}: 'AccountNumber' must match '{revised_pl_account_name}' in "
f"'NominalAnalysisNominalAnalysisNarrative/1', but found '{nominal}'."
)
return errors return errors
...@@ -241,7 +247,7 @@ class CSVUploadForm(forms.Form): ...@@ -241,7 +247,7 @@ class CSVUploadForm(forms.Form):
errors.append( errors.append(
f"Row {index}: The combination of '{cc_field}' ({cc}), " f"Row {index}: The combination of '{cc_field}' ({cc}), "
f"'{dep_field}' ({dep}), and '{nominal_account_field}' " f"'{dep_field}' ({dep}), and '{nominal_account_field}' "
f"({nominal_account_name}) does not exist in MEO valid Sage accounts." f"({nc}) does not exist in MEO valid Sage accounts."
) )
return errors return errors
...@@ -38,6 +38,7 @@ class MeoNominal(models.Model): ...@@ -38,6 +38,7 @@ class MeoNominal(models.Model):
class MeoValidSageAccounts(models.Model): class MeoValidSageAccounts(models.Model):
"""View for MEO valid Sage accounts.""" """View for MEO valid Sage accounts."""
id = models.UUIDField(db_column="ID", primary_key=True)
account_name = models.CharField(db_column="AccountName", max_length=60) account_name = models.CharField(db_column="AccountName", max_length=60)
account_number = models.CharField(db_column="AccountNumber", max_length=8, blank=True, null=True) account_number = models.CharField(db_column="AccountNumber", max_length=8, blank=True, null=True)
account_cost_centre = models.CharField(db_column="AccountCostCentre", max_length=3, blank=True, null=True) account_cost_centre = models.CharField(db_column="AccountCostCentre", max_length=3, blank=True, null=True)
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
{% block title %}File Upload{% endblock %} {% block title %}File Upload{% endblock %}
{% block content %} {% block content %}
<div class="bg-white p-10 rounded-lg shadow-lg w-11/12 md:w-1/2 lg:w-1/3 mx-auto"> <div class="bg-white p-10 rounded-lg shadow-lg w-11/12 md:w-3/4 lg:w-3/4 mx-auto">
<h2 class="text-2xl font-bold mb-6 text-gray-800 text-center">Upload CSV File</h2> <h2 class="text-2xl font-bold mb-6 text-gray-800 text-center">Upload CSV File</h2>
<form id="uploadForm" enctype="multipart/form-data" class="space-y-6"> <form id="uploadForm" enctype="multipart/form-data" class="space-y-6">
...@@ -77,19 +77,32 @@ ...@@ -77,19 +77,32 @@
successSection.classList.remove('hidden'); successSection.classList.remove('hidden');
downloadLink.href = result.download_url; downloadLink.href = result.download_url;
downloadSection.classList.remove('hidden'); downloadSection.classList.remove('hidden');
} else if (response.status === 400 && result.status === 'error') { } else {
for (const [field, messages] of Object.entries(result.errors)) { errorList.innerHTML = '';
messages.forEach(message => {
const li = document.createElement('li'); if (response.status === 403) {
li.textContent = `${field}: ${message}`; const li = document.createElement('li');
errorList.appendChild(li); li.textContent = 'You are not authorized to perform this action.';
}); errorList.appendChild(li);
} else if (response.status === 400 && result.status === 'error') {
for (const [field, messages] of Object.entries(result.errors)) {
messages.forEach(message => {
const li = document.createElement('li');
li.textContent = `${field}: ${message}`;
errorList.appendChild(li);
});
}
} else {
const li = document.createElement('li');
li.textContent = 'An unexpected error occurred. Please try again.';
errorList.appendChild(li);
} }
errorSection.classList.remove('hidden'); errorSection.classList.remove('hidden');
} }
} catch (error) { } catch (error) {
const li = document.createElement('li'); const li = document.createElement('li');
li.textContent = 'An unexpected error occurred. Please try again.'; li.textContent = 'Failed to connect to the server. Please check your internet connection.';
errorList.appendChild(li); errorList.appendChild(li);
errorSection.classList.remove('hidden'); errorSection.classList.remove('hidden');
} }
......
"""All the tests for the file_validator app."""
"""Views for the file_validator app.""" """Views for the file_validator app."""
import csv import csv
import io import io
from typing import ClassVar
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.shortcuts import render from django.shortcuts import render
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils import timezone from django.utils import timezone
from rest_framework import status from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from sage_validation.accounts.models import UserActivityLog
from sage_validation.file_validator.forms import CSVUploadForm from sage_validation.file_validator.forms import CSVUploadForm
from sage_validation.file_validator.models import MeoCostCentres, XxData from sage_validation.file_validator.models import MeoCostCentres, XxData
...@@ -28,6 +31,8 @@ def upload_page_view(request: HttpRequest) -> HttpResponse: ...@@ -28,6 +31,8 @@ def upload_page_view(request: HttpRequest) -> HttpResponse:
class CSVUploadAPIView(APIView): class CSVUploadAPIView(APIView):
"""API view for uploading a CSV file.""" """API view for uploading a CSV file."""
permission_classes: ClassVar[list] = [IsAuthenticated]
def post(self, request: Request) -> Response: def post(self, request: Request) -> Response:
"""Handle CSV upload and validation.""" """Handle CSV upload and validation."""
form = CSVUploadForm(data=request.data, files=request.FILES) form = CSVUploadForm(data=request.data, files=request.FILES)
...@@ -48,6 +53,7 @@ class CSVUploadAPIView(APIView): ...@@ -48,6 +53,7 @@ class CSVUploadAPIView(APIView):
updated_data = self.update_fields(csv_data) updated_data = self.update_fields(csv_data)
request.session["validated_csv"] = updated_data request.session["validated_csv"] = updated_data
request.session["input_file_hash"] = UserActivityLog.generate_file_hash(csv_file)
request.session.modified = True request.session.modified = True
return Response({ return Response({
...@@ -98,16 +104,25 @@ class CSVExportAPIView(APIView): ...@@ -98,16 +104,25 @@ class CSVExportAPIView(APIView):
def get(self, request: Request) -> Response: def get(self, request: Request) -> Response:
"""Return processed CSV as a downloadable response.""" """Return processed CSV as a downloadable response."""
csv_data: list[dict[str, str]] = request.session.get("validated_csv", []) csv_data: list[dict[str, str]] = request.session.get("validated_csv", [])
input_file_hash: str = request.session.get("input_file_hash", "")
if not csv_data: if not csv_data:
return Response({"status": "error", "message": "No data available for export."}, return Response({"status": "error", "message": "No data available for export."},
status=status.HTTP_400_BAD_REQUEST) status=status.HTTP_400_BAD_REQUEST)
file_name = f"Validated_{csv_data[0].get('TransactionReference', 'file')}.csv"
response = HttpResponse(content_type="text/csv") response = HttpResponse(content_type="text/csv")
response["Content-Disposition"] = 'attachment; filename="updated_file.csv"' response["Content-Disposition"] = f"attachment; filename={file_name}"
writer = csv.DictWriter(response, fieldnames=csv_data[0].keys()) writer = csv.DictWriter(response, fieldnames=csv_data[0].keys())
writer.writeheader() writer.writeheader()
writer.writerows(csv_data) writer.writerows(csv_data)
# Log the user activity
UserActivityLog.objects.create(
user=request.user,
action="download",
name=file_name,
input_file_hash=input_file_hash,
output_file_hash=UserActivityLog.generate_file_hash(csv_data),
timestamp=timezone.now()
)
return response return response
...@@ -9,6 +9,7 @@ For the full list of settings and their values, see ...@@ -9,6 +9,7 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.1/ref/settings/ https://docs.djangoproject.com/en/5.1/ref/settings/
""" """
import os import os
from datetime import timedelta
from pathlib import Path from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
...@@ -27,6 +28,9 @@ ALLOWED_HOSTS: list[str] = os.getenv("ALLOWED_HOSTS", "").split(",") ...@@ -27,6 +28,9 @@ ALLOWED_HOSTS: list[str] = os.getenv("ALLOWED_HOSTS", "").split(",")
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
"rest_framework",
"rest_framework.authtoken",
"social_django",
"django.contrib.admin", "django.contrib.admin",
"django.contrib.auth", "django.contrib.auth",
"django.contrib.contenttypes", "django.contrib.contenttypes",
...@@ -36,6 +40,7 @@ INSTALLED_APPS = [ ...@@ -36,6 +40,7 @@ INSTALLED_APPS = [
] ]
LOCAL_APPS = [ LOCAL_APPS = [
"sage_validation.file_validator", "sage_validation.file_validator",
"sage_validation.accounts",
] ]
THIRD_PARTY_APPS: list[str] = [] THIRD_PARTY_APPS: list[str] = []
...@@ -48,6 +53,7 @@ MIDDLEWARE = [ ...@@ -48,6 +53,7 @@ MIDDLEWARE = [
"django.middleware.csrf.CsrfViewMiddleware", "django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware", "django.contrib.messages.middleware.MessageMiddleware",
"social_django.middleware.SocialAuthExceptionMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware",
] ]
...@@ -64,6 +70,8 @@ TEMPLATES = [ ...@@ -64,6 +70,8 @@ TEMPLATES = [
"django.template.context_processors.request", "django.template.context_processors.request",
"django.contrib.auth.context_processors.auth", "django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages", "django.contrib.messages.context_processors.messages",
"social_django.context_processors.backends",
"social_django.context_processors.login_redirect",
], ],
}, },
}, },
...@@ -76,20 +84,24 @@ WSGI_APPLICATION = "sage_validation.wsgi.application" ...@@ -76,20 +84,24 @@ WSGI_APPLICATION = "sage_validation.wsgi.application"
DATABASES = { DATABASES = {
"default": { "default": {
"ENGINE": "django.db.backends.sqlite3", "ENGINE": "django.db.backends.postgresql",
"NAME": BASE_DIR / "db.sqlite3", "NAME": os.getenv("POSTGRES_DB_NAME", "sage_validation_db"),
"USER": os.getenv("POSTGRES_DB_USER", "sage_user"),
"PASSWORD": os.getenv("POSTGRES_DB_PASSWORD", "sage_password"),
"HOST": os.getenv("POSTGRES_DB_HOST", "localhost"),
"PORT": os.getenv("POSTGRES_DB_PORT", "5432"),
}, },
"meo": { "meo": {
'ENGINE': 'mssql', "ENGINE": "mssql",
'NAME': os.getenv("MSSQL_DB_NAME", ""), "NAME": os.getenv("MSSQL_DB_NAME", ""),
'USER': os.getenv("MSSQL_DB_USER", ""), "USER": os.getenv("MSSQL_DB_USER", ""),
'PASSWORD': os.getenv("MSSQL_DB_PASSWORD", ""), "PASSWORD": os.getenv("MSSQL_DB_PASSWORD", ""),
'HOST': os.getenv("MSSQL_DB_HOST", "localhost"), "HOST": os.getenv("MSSQL_DB_HOST", "localhost"),
'PORT': os.getenv("MSSQL_DB_PORT", ""), "PORT": os.getenv("MSSQL_DB_PORT", ""),
'OPTIONS': { "OPTIONS": {
'driver': 'ODBC Driver 18 for SQL Server', "driver": "ODBC Driver 18 for SQL Server",
'extra_params': 'TrustServerCertificate=Yes;' "extra_params": "TrustServerCertificate=Yes;"
}, },
}, },
...@@ -143,6 +155,20 @@ STATICFILES_DIRS = [ ...@@ -143,6 +155,20 @@ STATICFILES_DIRS = [
] ]
MEDIA_ROOT = os.getenv("MEDIA_ROOT", BASE_DIR / "media") MEDIA_ROOT = os.getenv("MEDIA_ROOT", BASE_DIR / "media")
MEDIA_URL = '/media/' MEDIA_URL = "/media/"
CSRF_TRUSTED_ORIGINS = os.getenv("CSRF_TRUSTED_ORIGINS", "").split(",") CSRF_TRUSTED_ORIGINS = os.getenv("CSRF_TRUSTED_ORIGINS", "").split(",")
AUTHENTICATION_BACKENDS = (
"social_core.backends.google.GoogleOAuth2",
"django.contrib.auth.backends.ModelBackend",
)
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = os.getenv("SOCIAL_AUTH_GOOGLE_OAUTH2_KEY", "")
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = os.getenv("SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET", "")
SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = ["email", "profile"]
SOCIAL_AUTH_GOOGLE_OAUTH2_REDIRECT_URI = os.getenv("SOCIAL_AUTH_GOOGLE_OAUTH2_REDIRECT_URI", "")
SOCIAL_AUTH_REDIRECT_IS_HTTPS = os.getenv('SOCIAL_AUTH_FORCE_HTTPS', 'true').lower() == 'true'
SOCIAL_AUTH_JSONFIELD_ENABLED = True
SOCIAL_AUTH_URL_NAMESPACE = "social"
LOGIN_REDIRECT_URL = "/"
LOGOUT_REDIRECT_URL = "/"
\ No newline at end of file
...@@ -18,7 +18,18 @@ ...@@ -18,7 +18,18 @@
<a href="{% url "index" %}" class="text-white text-2xl font-bold tracking-wide">Sage Validation</a> <!-- Made title clickable --> <a href="{% url "index" %}" class="text-white text-2xl font-bold tracking-wide">Sage Validation</a> <!-- Made title clickable -->
</div> </div>
<div class="flex items-center"> <!-- Login link --> <div class="flex items-center"> <!-- Login link -->
<a href="/login" class="text-white hover:text-gray-300">Login</a> {% if user.is_authenticated %}
<p class="text-white">Welcome, {{ user.username }}!</p>
<form action="{% url 'logout' %}" method="post" class="inline-block ml-4">
{% csrf_token %}
<button type="submit"
class="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 focus:outline-none focus:ring focus:ring-red-400">
Logout
</button>
</form>
{% else %}
<a href="{% url "social:begin" "google-oauth2" %}" class="text-white hover:text-gray-300">Login</a>
{% endif %}
<button id="mobile-menu-button" class="md:hidden text-white focus:outline-none ml-4"> <button id="mobile-menu-button" class="md:hidden text-white focus:outline-none ml-4">
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-7 6h7"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-7 6h7"></path>
......
...@@ -9,5 +9,6 @@ urlpatterns = [ ...@@ -9,5 +9,6 @@ urlpatterns = [
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
path("file-validator/", include("sage_validation.file_validator.urls")), path("file-validator/", include("sage_validation.file_validator.urls")),
path("", index_view, name="index"), path("", index_view, name="index"),
path("", include("social_django.urls", namespace="social")),
path("accounts/", include("sage_validation.accounts.urls")),
] ]
...@@ -3,12 +3,15 @@ from setuptools import find_packages, setup ...@@ -3,12 +3,15 @@ from setuptools import find_packages, setup
setup( setup(
name="sage-validation", name="sage-validation",
version="0.5", version="0.7",
packages=find_packages(), packages=find_packages(),
include_package_data=True, include_package_data=True,
install_requires=[ install_requires=[
"Django==5.0.11", "Django>=5.0,<5.1",
"mssql-django==1.5", "mssql-django==1.5",
"djangorestframework==3.15.2",
"social-auth-app-django==5.4.3",
"psycopg[binary,pool]",
], ],
extras_require={ extras_require={
"prod": [ "prod": [
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pytest import pytest
from django.contrib.auth.models import User
from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.uploadedfile import SimpleUploadedFile
from faker import Faker from faker import Faker
from rest_framework.test import APIClient from rest_framework.test import APIClient
...@@ -35,6 +36,7 @@ def sample_input_file() -> SimpleUploadedFile: ...@@ -35,6 +36,7 @@ def sample_input_file() -> SimpleUploadedFile:
"TaxValue", "TaxValue",
"SYSTraderGenerationReasonType", "SYSTraderGenerationReasonType",
"GoodsValueInBaseCurrency", "GoodsValueInBaseCurrency",
"TransactionReference",
# NominalAnalysis repeating columns (Example: /1 for first occurrence) # NominalAnalysis repeating columns (Example: /1 for first occurrence)
"NominalAnalysisTransactionValue/1", "NominalAnalysisTransactionValue/1",
...@@ -76,6 +78,7 @@ def sample_input_file() -> SimpleUploadedFile: ...@@ -76,6 +78,7 @@ def sample_input_file() -> SimpleUploadedFile:
"10", # TaxValue "10", # TaxValue
"1000", # SYSTraderGenerationReasonType "1000", # SYSTraderGenerationReasonType
"1200", # GoodsValueInBaseCurrency "1200", # GoodsValueInBaseCurrency
"BK123", # TransactionReference(Batch Number)
# NominalAnalysis repeating values (Example: /1) # NominalAnalysis repeating values (Example: /1)
"500.75", # NominalAnalysisTransactionValue/1 "500.75", # NominalAnalysisTransactionValue/1
...@@ -144,4 +147,8 @@ def mock_meo_database(mocker: MagicMock)-> None: ...@@ -144,4 +147,8 @@ def mock_meo_database(mocker: MagicMock)-> None:
@pytest.fixture @pytest.fixture
def api_client() -> APIClient: def api_client() -> APIClient:
"""Fixture to return Django API test client.""" """Fixture to return Django API test client."""
return APIClient() fake = Faker()
user = User.objects.create_user(username=fake.user_name(), password=fake.password())
client = APIClient()
client.force_authenticate(user=user)
return client