Skip to content
Snippets Groups Projects
Commit d6de2117 authored by geant-release-service's avatar geant-release-service
Browse files

Finished release 0.5.

parents 28d3faca 9f6f9148
No related branches found
No related tags found
No related merge requests found
"""Fixtures for the sage_validation tests."""
from unittest.mock import MagicMock
import pytest
from django.contrib.auth.models import User
from django.core.files.uploadedfile import SimpleUploadedFile
from faker import Faker
from rest_framework.test import APIClient
from sage_validation.file_validator.models import MeoCostCentres, MeoValidSuppliers, XxData
@pytest.fixture
def sample_input_file() -> SimpleUploadedFile:
"""Create a sample valid CSV file for testing."""
csv_headers_list = [
"AccountNumber",
"CBAccountNumber",
"DaysDiscountValid",
"DiscountValue",
"DiscountPercentage",
"DueDate",
"GoodsValueInAccountCurrency",
"PurControlValueInBaseCurrency",
"DocumentToBaseCurrencyRate",
"DocumentToAccountCurrencyRate",
"PostedDate",
"QueryCode",
"TransactionReference",
"SecondReference",
"Source",
"SYSTraderTranType",
"TransactionDate",
"UniqueReferenceNumber",
"UserNumber",
"TaxValue",
"SYSTraderGenerationReasonType",
"GoodsValueInBaseCurrency",
"TransactionReference",
# NominalAnalysis repeating columns (Example: /1 for first occurrence)
"NominalAnalysisTransactionValue/1",
"NominalAnalysisNominalAccountNumber/1",
"NominalAnalysisNominalCostCentre/1",
"NominalAnalysisNominalDepartment/1",
"NominalAnalysisNominalAnalysisNarrative/1",
"NominalAnalysisTransactionAnalysisCode/1",
# TaxAnalysis repeating columns (Example: /1 for first occurrence)
"TaxAnalysisTaxRate/1",
"TaxAnalysisGoodsValueBeforeDiscount/1",
"TaxAnalysisDiscountValue/1",
"TaxAnalysisDiscountPercentage/1",
"TaxAnalysisTaxOnGoodsValue/1",
]
csv_headers = ",".join(csv_headers_list)
csv_content_list = [
"12345", # AccountNumber
"54321", # CBAccountNumber
"30", # DaysDiscountValid
"10.5", # DiscountValue
"5.0", # DiscountPercentage
"2024-02-10", # DueDate
"1000", # GoodsValueInAccountCurrency
"950", # PurControlValueInBaseCurrency
"1.2", # DocumentToBaseCurrencyRate
"1.1", # DocumentToAccountCurrencyRate
"2024-02-05", # PostedDate
"Q1", # QueryCode
"TRX123", # TransactionReference
"SREF123", # SecondReference
"80", # Source
"4", # SYSTraderTranType
"2024-02-01", # TransactionDate
"UR123", # UniqueReferenceNumber
"42", # UserNumber
"10", # TaxValue
"1000", # SYSTraderGenerationReasonType
"1200", # GoodsValueInBaseCurrency
"BK123", # TransactionReference(Batch Number)
# NominalAnalysis repeating values (Example: /1)
"500.75", # NominalAnalysisTransactionValue/1
"ACC100", # NominalAnalysisNominalAccountNumber/1
"CC100", # NominalAnalysisNominalCostCentre/1
"DEP200", # NominalAnalysisNominalDepartment/1
"Sample Narrative", # NominalAnalysisNominalAnalysisNarrative/1
"TAC100", # NominalAnalysisTransactionAnalysisCode/1
# TaxAnalysis repeating values (Example: /1)
"20.5", # TaxAnalysisTaxRate/1
"900", # TaxAnalysisGoodsValueBeforeDiscount/1
"30", # TaxAnalysisDiscountValue/1
"3.5", # TaxAnalysisDiscountPercentage/1
"180", # TaxAnalysisTaxOnGoodsValue/1
]
csv_content = ",".join(csv_content_list)
return SimpleUploadedFile("test.csv", f"{csv_headers}\n{csv_content}".encode(), content_type="text/csv")
@pytest.fixture
def mock_meo_database(mocker: MagicMock)-> None:
"""Mock the meo database since it's read-only."""
fake = Faker()
# Mock MeoValidSuppliers
supplier_mock = MagicMock()
supplier_mock.all.return_value = [
MeoValidSuppliers(
supplier_account_number=str(fake.random_int(min=10000, max=99999)), supplier_account_name=fake.company()
),
MeoValidSuppliers(supplier_account_number="12345", supplier_account_name="Sample Narrative")
]
mocker.patch("sage_validation.file_validator.models.MeoValidSuppliers.objects.using", return_value=supplier_mock)
# Mock MeoCostCentres
cost_centre_mock = MagicMock()
cost_centre_mock.all.return_value = [
MeoCostCentres(cc="CC100", cc_type="Project", cc_name="CostCentreName", id=1),
MeoCostCentres(cc="CC200", cc_type="Overhead", cc_name="OverheadName", id=2),
MeoCostCentres(cc="CC300", cc_type="Overhead", cc_name="DepartmentName", id=3)
]
mocker.patch("sage_validation.file_validator.models.MeoCostCentres.objects.using", return_value=cost_centre_mock)
# Mock XxData
xx_data_mock = MagicMock()
xx_data_mock.all.return_value = [
XxData(xx_value="N100", project="ProjectCode", overhead="OverheadCode", description=fake.sentence()),
XxData(xx_value="N200", project="ProjectCode", overhead="OverheadCode", description=fake.sentence())
]
mocker.patch("sage_validation.file_validator.models.XxData.objects.using", return_value=xx_data_mock)
# Mock MeoValidSageAccounts
sage_account_mock = MagicMock()
sage_account_mock.filter.return_value.exists.return_value = True
mocker.patch(
"sage_validation.file_validator.models.MeoValidSageAccounts.objects.using", return_value=sage_account_mock
)
# Mock MeoNominal
nominal_mock = MagicMock()
nominal_mock.filter.return_value.exists.return_value = True
mocker.patch("sage_validation.file_validator.models.MeoNominal.objects.using", return_value=nominal_mock)
@pytest.fixture
def api_client() -> APIClient:
"""Fixture to return Django API test client."""
fake = Faker()
user = User.objects.create_user(username=fake.user_name(), password=fake.password())
client = APIClient()
client.force_authenticate(user=user)
return client
"""Settings for running tests."""
from sage_validation.settings import * # noqa: F403
if os.getenv("USE_SQLITE", "false").lower() == "true": # noqa: F405
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": "sage_validation",
}
}
else:
DATABASES = {
"default": DATABASES["default"],
}
from unittest.mock import MagicMock
import pytest
from django.core.files.uploadedfile import SimpleUploadedFile
from django.urls.base import reverse
from rest_framework.test import APIClient
from sage_validation.accounts.models import UserActivityLog
UPLOAD_FILE_URL = reverse("upload-file")
@pytest.mark.django_db
def test_csv_upload_unauthenticated(sample_input_file: SimpleUploadedFile) -> None:
"""Test that a valid CSV upload succeeds."""
api_client = APIClient()
response = api_client.post(UPLOAD_FILE_URL, {"file": sample_input_file}, format="multipart")
assert response.status_code == 403
assert response.json()["detail"] == "Authentication credentials were not provided."
@pytest.mark.django_db
def test_csv_upload_valid(
api_client: APIClient, sample_input_file: SimpleUploadedFile, mock_meo_database: MagicMock
) -> None:
"""Test that a valid CSV upload succeeds."""
response = api_client.post(UPLOAD_FILE_URL, {"file": sample_input_file}, format="multipart")
assert response.status_code == 200
assert response.json()["status"] == "success"
assert "download_url" in response.json()
@pytest.mark.django_db
def test_csv_upload_invalid_extension(api_client: APIClient) -> None:
"""Test that uploading a non-CSV file fails."""
bad_file = SimpleUploadedFile("test.txt", b"Invalid content", content_type="text/plain")
response = api_client.post(UPLOAD_FILE_URL, {"file": bad_file}, format="multipart")
assert response.status_code == 400
assert response.json()["status"] == "error"
assert "errors" in response.json()
@pytest.mark.django_db
def test_csv_export_with_data(api_client: APIClient) -> None:
"""Test exporting a processed CSV file."""
url = reverse("export-file")
# Simulate session data
session = api_client.session
session["validated_csv"] = [
{"AccountNumber": "12345", "TransactionDate": "01/03/2024", "TransactionReference": "BK1234"},
]
session["input_file_hash"] = "123456"
session.save()
response = api_client.get(url)
assert UserActivityLog.objects.count() == 1
assert response.status_code == 200
assert response["Content-Disposition"] == "attachment; filename=Validated_BK1234.csv"
assert b"AccountNumber,TransactionDate,TransactionReference" in response.content
assert b"12345,01/03/2024,BK1234" in response.content
@pytest.mark.django_db
def test_activity_log_creation_on_csv_export(api_client: APIClient) -> None:
"""Test that a UserActivityLog is created when exporting a CSV file."""
url = reverse("export-file")
# Simulate session data
session = api_client.session
session["validated_csv"] = [
{"AccountNumber": "12345", "TransactionDate": "01/03/2024", "TransactionReference": "BK1234"},
]
session["input_file_hash"] = "123456"
session.save()
response = api_client.get(url)
assert response.status_code == 200
log = UserActivityLog.objects.first()
assert log.action == "download"
assert log.input_file_hash == "123456"
assert log.output_file_hash == UserActivityLog.generate_file_hash(session["validated_csv"])
assert log.name == "Validated_BK1234.csv"
"""Tests for the file_validator forms."""
import csv
import io
from unittest.mock import MagicMock
from django.core.files.uploadedfile import SimpleUploadedFile
from sage_validation.file_validator.forms import CSVUploadForm
def create_modified_csv(sample_file: SimpleUploadedFile, modifications: dict[str, str]) -> SimpleUploadedFile:
"""
Modify specific fields in the first row of a CSV file and return a new SimpleUploadedFile.
Args:
sample_file (SimpleUploadedFile): The original CSV file.
modifications (dict): Dictionary of column names to modified values.
Returns:
SimpleUploadedFile: The modified CSV file.
"""
csv_content = sample_file.read().decode("utf-8").splitlines()
reader = csv.DictReader(csv_content)
rows = list(reader)
for key, value in modifications.items():
rows[0][key] = value
output = io.StringIO()
writer = csv.DictWriter(output, fieldnames=reader.fieldnames or [])
writer.writeheader()
writer.writerows(rows)
return SimpleUploadedFile("test_modified.csv", output.getvalue().encode("utf-8"), content_type="text/csv")
def test_valid_csv_upload(sample_input_file: SimpleUploadedFile, mock_meo_database: MagicMock) -> None:
"""Test CSV upload with valid data."""
form = CSVUploadForm(files={"file": sample_input_file})
assert form.is_valid(), f"Form errors: {form.errors}"
def test_invalid_file_extension() -> None:
"""Test form rejects non-CSV files."""
bad_file = SimpleUploadedFile("test.txt", b"Some text content", content_type="text/plain")
form = CSVUploadForm(files={"file": bad_file})
assert not form.is_valid()
assert "File must be in CSV format." in form.errors["file"]
def test_missing_required_columns() -> None:
"""Test form rejects CSV missing required headers."""
invalid_csv = SimpleUploadedFile("test.csv", b"AccountNumber,CBAccountNumber\n12345,54321", content_type="text/csv")
form = CSVUploadForm(files={"file": invalid_csv})
assert not form.is_valid()
assert "Missing required columns" in str(form.errors)
def test_source_and_trader_type_validation(sample_input_file: SimpleUploadedFile, mock_meo_database: MagicMock) -> None:
"""Test validation for Source and SYSTraderTranType columns."""
modified_file = create_modified_csv(sample_input_file, {"Source": "90", "SYSTraderTranType": "5"})
form = CSVUploadForm(files={"file": modified_file})
assert not form.is_valid()
assert "Row 1: 'Source' must be 80" in form.errors["file"][0]
assert "Row 1: 'SYSTraderTranType' must be 4" in form.errors["file"][1]
def test_validate_nominal_analysis_account(sample_input_file: SimpleUploadedFile, mock_meo_database: MagicMock) -> None:
"""Test validation for nominal analysis account."""
modified_file = create_modified_csv(sample_input_file,
{"NominalAnalysisNominalAnalysisNarrative/1": "Invalid Name"})
form = CSVUploadForm(files={"file": modified_file})
assert not form.is_valid()
assert form.errors["file"][0] == (
"Row 1: 'AccountNumber' must match 'Sample Narrative' in 'NominalAnalysisNominalAnalysisNarrative/1'"
", but found 'Invalid Name'.")
def test_validate_nc_cc_dep_combination_against_meo_sage_account(
sample_input_file: SimpleUploadedFile, mock_meo_database: MagicMock
) -> None:
"""Test validation for nominal analysis fields against MEO valid Sage accounts."""
modified_file = create_modified_csv(
sample_input_file,
{
"NominalAnalysisNominalCostCentre/1": "Invalid_CC",
"NominalAnalysisNominalAccountNumber/1": "Invalid_Account"
})
form = CSVUploadForm(files={"file": modified_file})
assert not form.is_valid()
assert ("Row 1: 'NominalAnalysisNominalCostCentre/1' (Invalid_CC) is not a valid cost centre."
in str(form.errors["file"][0]))
......@@ -5,6 +5,17 @@ envlist = py311
deps =
mypy
ruff
-r requirements.txt
setenv =
USE_SQLITE = true
commands =
ruff check .
mypy .
coverage erase
coverage run --source sage_validation -m pytest
coverage report --fail-under=90
coverage xml
coverage html
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment