erster versuch der KI mit Plan
This commit is contained in:
1 parent
c697c64f3a
commit
2faa1689dd
20 files changed
+1749
No files matched your search
Whitespace-only changes.
@@ -0,0 +1,278 @@
|
||||
"""Tests for src/excel_reader.py — file-independent business logic."""
|
||||
|
||||
import re
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from src.exceptions import ExcelDataError, ExcelSheetError, YearExtractionError
|
||||
from src.excel_reader import (
|
||||
_parse_cell_value,
|
||||
extract_year_from_filename,
|
||||
extract_persons,
|
||||
extract_attendance,
|
||||
)
|
||||
from src.models import Person
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Minimal config fixture (no real file needed)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.fixture
|
||||
def cfg():
|
||||
return {
|
||||
"excel_year_pattern": "(19\\d{2}|20\\d{2})",
|
||||
"excel_name_header": "Name, Vorname",
|
||||
"excel_name_column": 1,
|
||||
"excel_seq_column": 0,
|
||||
"excel_day_start_col": 9,
|
||||
"excel_day_end_col": 39,
|
||||
"month_mapping": {
|
||||
"Jan": "01", "Feb": "02", "Mär": "03", "Mrz": "03",
|
||||
"Apr": "04", "Mai": "05", "Jun": "06", "Jul": "07",
|
||||
"Aug": "08", "Sep": "09", "Okt": "10", "Nov": "11", "Dez": "12",
|
||||
},
|
||||
"valid_status_codes": ["K", "KK", "E", "F", "P", "U"],
|
||||
"plan_hours_value": "6",
|
||||
"pauschale_value": "8,60",
|
||||
"antragsnummer": "12345",
|
||||
"ort": "Teststadt",
|
||||
"template_pdf": "template/sab.pdf",
|
||||
"output_dir": "output",
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# extract_year_from_filename
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestExtractYearFromFilename:
|
||||
def test_standard_filename(self, cfg):
|
||||
assert extract_year_from_filename("Anwesenheit_2026.xlsx", cfg) == 2026
|
||||
|
||||
def test_year_embedded_in_path(self, cfg):
|
||||
assert extract_year_from_filename("/data/exports/Anwesenheit_2024.xlsx", cfg) == 2024
|
||||
|
||||
def test_no_year_raises(self, cfg):
|
||||
with pytest.raises(YearExtractionError):
|
||||
extract_year_from_filename("Anwesenheit.xlsx", cfg)
|
||||
|
||||
def test_out_of_range_raises(self, cfg):
|
||||
# Overwrite pattern to force out-of-range year
|
||||
bad_cfg = {**cfg, "excel_year_pattern": "(1800)"}
|
||||
with pytest.raises(YearExtractionError):
|
||||
extract_year_from_filename("data_1800.xlsx", bad_cfg)
|
||||
|
||||
def test_first_year_match_wins(self, cfg):
|
||||
# Two years in name — first one is used
|
||||
assert extract_year_from_filename("2023_to_2025_export.xlsx", cfg) == 2023
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _parse_cell_value
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestParseCellValue:
|
||||
VALID_CODES = ["K", "KK", "E", "F", "P", "U"]
|
||||
|
||||
def test_numeric_int(self):
|
||||
result = _parse_cell_value(6, self.VALID_CODES)
|
||||
assert result == ("X", 6.0)
|
||||
|
||||
def test_numeric_float(self):
|
||||
result = _parse_cell_value(6.5, self.VALID_CODES)
|
||||
assert result == ("X", 6.5)
|
||||
|
||||
def test_zero_skipped(self):
|
||||
assert _parse_cell_value(0, self.VALID_CODES) is None
|
||||
|
||||
def test_negative_skipped(self):
|
||||
assert _parse_cell_value(-1, self.VALID_CODES) is None
|
||||
|
||||
def test_none_skipped(self):
|
||||
assert _parse_cell_value(None, self.VALID_CODES) is None
|
||||
|
||||
def test_empty_string_skipped(self):
|
||||
assert _parse_cell_value("", self.VALID_CODES) is None
|
||||
assert _parse_cell_value(" ", self.VALID_CODES) is None
|
||||
|
||||
def test_status_codes(self):
|
||||
for code in self.VALID_CODES:
|
||||
result = _parse_cell_value(code, self.VALID_CODES)
|
||||
assert result is not None
|
||||
assert result[0] == code
|
||||
assert result[1] is None
|
||||
|
||||
def test_lowercase_status_code(self):
|
||||
result = _parse_cell_value("k", self.VALID_CODES)
|
||||
assert result is not None
|
||||
assert result[0] == "K"
|
||||
|
||||
def test_explicit_x_no_hours(self):
|
||||
result = _parse_cell_value("X", self.VALID_CODES)
|
||||
assert result == ("X", None)
|
||||
|
||||
def test_unknown_string_skipped(self):
|
||||
assert _parse_cell_value("UNBEKANNT", self.VALID_CODES) is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# extract_persons (mocked workbook)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _make_row(*values):
|
||||
"""Create a list of values as a mock row."""
|
||||
return list(values)
|
||||
|
||||
|
||||
def _sheet_data_persons():
|
||||
"""Simulated sheet data: header row + 3 persons."""
|
||||
return [
|
||||
[None, "Einrichtung XY", None], # row 0 – ignored
|
||||
[None, "Name, Vorname", None], # row 1 – header
|
||||
[1, "Mustermann, Max", None], # row 2 – person 1
|
||||
[2, "Beispiel, Anna", None], # row 3 – person 2
|
||||
[3, "Test, Peter", None], # row 4 – person 3
|
||||
[None, None, None], # row 5 – empty row
|
||||
]
|
||||
|
||||
|
||||
class TestExtractPersons:
|
||||
def test_returns_correct_persons(self, cfg):
|
||||
with patch("src.excel_reader._open_workbook") as mock_open, \
|
||||
patch("src.excel_reader._sheet_data", return_value=_sheet_data_persons()):
|
||||
mock_wb = MagicMock()
|
||||
mock_wb.sheetnames = ["Feb"]
|
||||
mock_open.return_value = mock_wb
|
||||
|
||||
persons = extract_persons("dummy.xlsx", "Feb", cfg)
|
||||
|
||||
assert len(persons) == 3
|
||||
assert persons[0].seq_num == 1
|
||||
assert persons[1].name == "Beispiel, Anna"
|
||||
assert persons[2].seq_num == 3
|
||||
|
||||
def test_empty_rows_ignored(self, cfg):
|
||||
with patch("src.excel_reader._open_workbook") as mock_open, \
|
||||
patch("src.excel_reader._sheet_data", return_value=_sheet_data_persons()):
|
||||
mock_open.return_value = MagicMock(sheetnames=["Feb"])
|
||||
persons = extract_persons("dummy.xlsx", "Feb", cfg)
|
||||
|
||||
assert all(p.name for p in persons)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# extract_attendance (mocked workbook)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _sheet_data_attendance():
|
||||
"""
|
||||
Simulated month sheet with one person row.
|
||||
Columns 0-8: seq, name, and spare cols.
|
||||
Columns 9-39: days 1-31.
|
||||
Day 1 (col 9): 6.0 → present 6h
|
||||
Day 2 (col 10): "K" → sick
|
||||
Day 3 (col 11): "KK" → child sick
|
||||
Day 4 (col 12): None → skip
|
||||
Day 5 (col 13): "F" → unexcused absence
|
||||
"""
|
||||
person_row = [None] * 40
|
||||
person_row[0] = 1
|
||||
person_row[1] = "Mustermann, Max"
|
||||
person_row[9] = 6.0
|
||||
person_row[10] = "K"
|
||||
person_row[11] = "KK"
|
||||
person_row[12] = None
|
||||
person_row[13] = "F"
|
||||
|
||||
return [
|
||||
[None, "Name, Vorname"] + [None] * 38, # header row
|
||||
person_row,
|
||||
]
|
||||
|
||||
|
||||
class TestExtractAttendance:
|
||||
def test_present_day(self, cfg):
|
||||
with patch("src.excel_reader._open_workbook") as mock_open, \
|
||||
patch("src.excel_reader._sheet_data", return_value=_sheet_data_attendance()):
|
||||
mock_open.return_value = MagicMock(sheetnames=["Feb"])
|
||||
person = Person(seq_num=1, name="Mustermann, Max")
|
||||
attendance = extract_attendance("dummy.xlsx", "Feb", person, 2026, cfg)
|
||||
|
||||
entry = attendance.entries[0]
|
||||
assert entry.status == "X"
|
||||
assert entry.hours == 6.0
|
||||
assert entry.date == date(2026, 2, 1)
|
||||
|
||||
def test_sick_day(self, cfg):
|
||||
with patch("src.excel_reader._open_workbook") as mock_open, \
|
||||
patch("src.excel_reader._sheet_data", return_value=_sheet_data_attendance()):
|
||||
mock_open.return_value = MagicMock(sheetnames=["Feb"])
|
||||
person = Person(seq_num=1, name="Mustermann, Max")
|
||||
attendance = extract_attendance("dummy.xlsx", "Feb", person, 2026, cfg)
|
||||
|
||||
entry = attendance.entries[1]
|
||||
assert entry.status == "K"
|
||||
assert entry.hours is None
|
||||
|
||||
def test_empty_day_skipped(self, cfg):
|
||||
with patch("src.excel_reader._open_workbook") as mock_open, \
|
||||
patch("src.excel_reader._sheet_data", return_value=_sheet_data_attendance()):
|
||||
mock_open.return_value = MagicMock(sheetnames=["Feb"])
|
||||
person = Person(seq_num=1, name="Mustermann, Max")
|
||||
attendance = extract_attendance("dummy.xlsx", "Feb", person, 2026, cfg)
|
||||
|
||||
# Day 4 (None) is skipped, so entry index 2 is Day 3 (KK) and index 3 is Day 5 (F)
|
||||
assert len(attendance.entries) == 4
|
||||
assert attendance.entries[2].status == "KK"
|
||||
assert attendance.entries[3].status == "F"
|
||||
|
||||
def test_max_23_entries(self, cfg):
|
||||
"""More than 23 valid entries must be truncated to 23."""
|
||||
person_row = [None] * 40
|
||||
person_row[0] = 1
|
||||
person_row[1] = "Viel, Anwesend"
|
||||
for col in range(9, 40): # 31 days all present
|
||||
person_row[col] = 6.0
|
||||
|
||||
sheet_data = [
|
||||
[None, "Name, Vorname"] + [None] * 38,
|
||||
person_row,
|
||||
]
|
||||
with patch("src.excel_reader._open_workbook") as mock_open, \
|
||||
patch("src.excel_reader._sheet_data", return_value=sheet_data):
|
||||
mock_open.return_value = MagicMock(sheetnames=["Jan"])
|
||||
person = Person(seq_num=1, name="Viel, Anwesend")
|
||||
attendance = extract_attendance("dummy.xlsx", "Jan", person, 2026, cfg)
|
||||
|
||||
assert len(attendance.entries) == 23
|
||||
|
||||
def test_invalid_month_raises(self, cfg):
|
||||
with pytest.raises(ExcelSheetError):
|
||||
extract_attendance("dummy.xlsx", "UNGÜLTIG", Person(1, "X, Y"), 2026, cfg)
|
||||
|
||||
def test_february_day_29_invalid_year(self, cfg):
|
||||
"""Day 29 in Feb of a non-leap year must be skipped."""
|
||||
person_row = [None] * 40
|
||||
person_row[0] = 1
|
||||
person_row[1] = "Schaltjahr, Test"
|
||||
person_row[9 + 27] = 6.0 # day 28 = col 36 → valid
|
||||
person_row[9 + 28] = 6.0 # day 29 = col 37 → invalid (2025 not leap year)
|
||||
person_row[9 + 29] = 6.0 # day 30 = col 38 → invalid
|
||||
|
||||
sheet_data = [
|
||||
[None, "Name, Vorname"] + [None] * 38,
|
||||
person_row,
|
||||
]
|
||||
with patch("src.excel_reader._open_workbook") as mock_open, \
|
||||
patch("src.excel_reader._sheet_data", return_value=sheet_data):
|
||||
mock_open.return_value = MagicMock(sheetnames=["Feb"])
|
||||
person = Person(seq_num=1, name="Schaltjahr, Test")
|
||||
attendance = extract_attendance("dummy.xlsx", "Feb", person, 2025, cfg)
|
||||
|
||||
# Only day 28 is valid in Feb 2025
|
||||
assert len(attendance.entries) == 1
|
||||
assert attendance.entries[0].date == date(2025, 2, 28)
|
||||
@@ -0,0 +1,172 @@
|
||||
"""Tests for src/pdf_filler.py — field dict building and pauschale/plan logic."""
|
||||
|
||||
from datetime import date
|
||||
|
||||
import pytest
|
||||
|
||||
from src.models import AttendanceDate, MonthlyAttendance, Person
|
||||
from src.pdf_filler import build_field_dict
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.fixture
|
||||
def cfg():
|
||||
return {
|
||||
"antragsnummer": "SAB-12345",
|
||||
"ort": "Leipzig",
|
||||
"template_pdf": "template/sab.pdf",
|
||||
"output_dir": "output",
|
||||
"plan_hours_value": "6",
|
||||
"pauschale_value": "8,60",
|
||||
"excel_year_pattern": "(20\\d{2})",
|
||||
"excel_name_header": "Name, Vorname",
|
||||
"excel_name_column": 1,
|
||||
"excel_seq_column": 0,
|
||||
"excel_day_start_col": 9,
|
||||
"excel_day_end_col": 39,
|
||||
"month_mapping": {"Feb": "02"},
|
||||
"valid_status_codes": ["K", "KK", "E", "F", "P", "U"],
|
||||
}
|
||||
|
||||
|
||||
def _make_attendance(*entries) -> MonthlyAttendance:
|
||||
person = Person(seq_num=1, name="Teilnehmer, Test")
|
||||
return MonthlyAttendance(
|
||||
person=person,
|
||||
year=2026,
|
||||
month=2,
|
||||
month_name="Feb",
|
||||
entries=list(entries),
|
||||
)
|
||||
|
||||
|
||||
def _present(day: int, hours: float) -> AttendanceDate:
|
||||
return AttendanceDate(date=date(2026, 2, day), status="X", hours=hours)
|
||||
|
||||
|
||||
def _absent(day: int, status: str) -> AttendanceDate:
|
||||
return AttendanceDate(date=date(2026, 2, day), status=status, hours=None)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Header fields
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestHeaderFields:
|
||||
def test_person_name(self, cfg):
|
||||
fields = build_field_dict(_make_attendance(), cfg, date(2026, 2, 1))
|
||||
assert fields["person.name"] == "Teilnehmer, Test"
|
||||
|
||||
def test_monat_jahr(self, cfg):
|
||||
fields = build_field_dict(_make_attendance(), cfg, date(2026, 2, 1))
|
||||
assert fields["monat_jahr"] == "02/2026"
|
||||
|
||||
def test_antragsnummer(self, cfg):
|
||||
fields = build_field_dict(_make_attendance(), cfg, date(2026, 2, 1))
|
||||
assert fields["antragsnummer"] == "SAB-12345"
|
||||
|
||||
def test_ort(self, cfg):
|
||||
fields = build_field_dict(_make_attendance(), cfg, date(2026, 2, 1))
|
||||
assert fields["ort"] == "Leipzig"
|
||||
|
||||
def test_erstellungsdatum(self, cfg):
|
||||
fields = build_field_dict(_make_attendance(), cfg, date(2026, 3, 27))
|
||||
assert fields["erstellungsdatum"] == "27.03.2026"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Present day (status X)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestPresentDay:
|
||||
def test_datum_field(self, cfg):
|
||||
fields = build_field_dict(_make_attendance(_present(3, 6.0)), cfg, date(2026, 2, 1))
|
||||
assert fields["datum.0"] == "03.02.2026"
|
||||
|
||||
def test_anwesend_field(self, cfg):
|
||||
fields = build_field_dict(_make_attendance(_present(3, 6.0)), cfg, date(2026, 2, 1))
|
||||
assert fields["anwesend.0"] == "X"
|
||||
|
||||
def test_ist_field_whole_hours(self, cfg):
|
||||
fields = build_field_dict(_make_attendance(_present(3, 6.0)), cfg, date(2026, 2, 1))
|
||||
assert fields["ist.0"] == "6"
|
||||
|
||||
def test_ist_field_decimal_hours(self, cfg):
|
||||
fields = build_field_dict(_make_attendance(_present(3, 6.5)), cfg, date(2026, 2, 1))
|
||||
assert fields["ist.0"] == "6,5"
|
||||
|
||||
def test_plan_always_6(self, cfg):
|
||||
fields = build_field_dict(_make_attendance(_present(3, 3.0)), cfg, date(2026, 2, 1))
|
||||
assert fields["plan.0"] == "6"
|
||||
|
||||
def test_pauschale_set(self, cfg):
|
||||
fields = build_field_dict(_make_attendance(_present(3, 6.0)), cfg, date(2026, 2, 1))
|
||||
assert fields["pauschale.0"] == "8,60"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Absent days (K, KK, E, F, P, U)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestAbsentDay:
|
||||
@pytest.mark.parametrize("status", ["K", "KK", "E", "F", "P", "U"])
|
||||
def test_anwesend_field(self, cfg, status):
|
||||
fields = build_field_dict(_make_attendance(_absent(4, status)), cfg, date(2026, 2, 1))
|
||||
assert fields["anwesend.0"] == status
|
||||
|
||||
@pytest.mark.parametrize("status", ["K", "KK", "E", "F", "P", "U"])
|
||||
def test_ist_empty(self, cfg, status):
|
||||
fields = build_field_dict(_make_attendance(_absent(4, status)), cfg, date(2026, 2, 1))
|
||||
assert fields["ist.0"] == ""
|
||||
|
||||
@pytest.mark.parametrize("status", ["K", "KK", "E", "F", "P", "U"])
|
||||
def test_plan_empty(self, cfg, status):
|
||||
fields = build_field_dict(_make_attendance(_absent(4, status)), cfg, date(2026, 2, 1))
|
||||
assert fields["plan.0"] == ""
|
||||
|
||||
@pytest.mark.parametrize("status", ["K", "KK", "E", "F", "P", "U"])
|
||||
def test_pauschale_empty(self, cfg, status):
|
||||
fields = build_field_dict(_make_attendance(_absent(4, status)), cfg, date(2026, 2, 1))
|
||||
assert fields["pauschale.0"] == ""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Multi-row indexing
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestMultiRow:
|
||||
def test_second_entry_indexed_as_1(self, cfg):
|
||||
entries = [_present(1, 6.0), _absent(2, "K")]
|
||||
fields = build_field_dict(_make_attendance(*entries), cfg, date(2026, 2, 1))
|
||||
assert fields["datum.1"] == "02.02.2026"
|
||||
assert fields["anwesend.1"] == "K"
|
||||
|
||||
def test_23_entries_produce_index_0_to_22(self, cfg):
|
||||
entries = [_present(i, 6.0) for i in range(1, 24)] # 23 entries max
|
||||
fields = build_field_dict(_make_attendance(*entries), cfg, date(2026, 2, 1))
|
||||
# Index 22 should exist
|
||||
assert "datum.22" in fields
|
||||
assert "anwesend.22" in fields
|
||||
# Index 23 must NOT exist
|
||||
assert "datum.23" not in fields
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hours formatting
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestHoursFormatting:
|
||||
def test_whole_number_no_decimal(self):
|
||||
entry = AttendanceDate(date=date(2026, 2, 1), status="X", hours=6.0)
|
||||
assert entry.hours_german() == "6"
|
||||
|
||||
def test_decimal_comma_separator(self):
|
||||
entry = AttendanceDate(date=date(2026, 2, 1), status="X", hours=6.5)
|
||||
assert entry.hours_german() == "6,5"
|
||||
|
||||
def test_none_returns_empty(self):
|
||||
entry = AttendanceDate(date=date(2026, 2, 1), status="K", hours=None)
|
||||
assert entry.hours_german() == ""
|
||||
@@ -0,0 +1,155 @@
|
||||
"""Tests for src/validators.py — pre-generation validation logic."""
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from src.validators import validate
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _write_config(tmp_path: Path, overrides: dict = None) -> Path:
|
||||
"""Write a valid config.json into tmp_path, optionally applying overrides."""
|
||||
cfg = {
|
||||
"antragsnummer": "12345",
|
||||
"ort": "Teststadt",
|
||||
"template_pdf": "template/sab.pdf",
|
||||
"output_dir": "output",
|
||||
"excel_year_pattern": "(19\\d{2}|20\\d{2})",
|
||||
"excel_name_header": "Name, Vorname",
|
||||
"excel_name_column": 1,
|
||||
"excel_seq_column": 0,
|
||||
"excel_day_start_col": 9,
|
||||
"excel_day_end_col": 39,
|
||||
"month_mapping": {"Feb": "02", "Jan": "01"},
|
||||
"valid_status_codes": ["K", "KK", "E", "F", "P", "U"],
|
||||
"plan_hours_value": "6",
|
||||
"pauschale_value": "8,60",
|
||||
}
|
||||
if overrides:
|
||||
cfg.update(overrides)
|
||||
cfg_path = tmp_path / "config.json"
|
||||
cfg_path.write_text(json.dumps(cfg), encoding="utf-8")
|
||||
return cfg_path
|
||||
|
||||
|
||||
def _write_dummy_xlsx(tmp_path: Path, name: str = "Anwesenheit_2026.xlsx") -> Path:
|
||||
"""Create a minimal valid xlsx file using openpyxl."""
|
||||
import openpyxl
|
||||
wb = openpyxl.Workbook()
|
||||
ws = wb.active
|
||||
ws.title = "Feb"
|
||||
# Header row
|
||||
ws.cell(row=1, column=2, value="Name, Vorname")
|
||||
# Day headers (columns J–AN = col 10–40 in 1-based)
|
||||
for col in range(10, 41):
|
||||
ws.cell(row=1, column=col, value=col - 9) # day number as header
|
||||
# Person row
|
||||
ws.cell(row=2, column=1, value=1)
|
||||
ws.cell(row=2, column=2, value="Mustermann, Max")
|
||||
ws.cell(row=2, column=10, value=6.0) # day 1 → present 6h
|
||||
xlsx_path = tmp_path / name
|
||||
wb.save(str(xlsx_path))
|
||||
return xlsx_path
|
||||
|
||||
|
||||
def _write_dummy_pdf(tmp_path: Path) -> Path:
|
||||
"""Create a 1-byte placeholder file that passes existence checks."""
|
||||
pdf_path = tmp_path / "template" / "sab.pdf"
|
||||
pdf_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
pdf_path.write_bytes(b"%PDF-1.4 placeholder")
|
||||
return pdf_path
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestValidate:
|
||||
def test_all_checks_pass(self, tmp_path):
|
||||
cfg_path = _write_config(tmp_path)
|
||||
xlsx_path = _write_dummy_xlsx(tmp_path)
|
||||
_write_dummy_pdf(tmp_path)
|
||||
|
||||
# Reset config cache so the test config is used
|
||||
from src.config_loader import reset_config_cache
|
||||
reset_config_cache()
|
||||
|
||||
errors = validate(xlsx_path, "Feb", cfg_path)
|
||||
assert errors == []
|
||||
|
||||
def test_excel_file_missing(self, tmp_path):
|
||||
cfg_path = _write_config(tmp_path)
|
||||
_write_dummy_pdf(tmp_path)
|
||||
|
||||
from src.config_loader import reset_config_cache
|
||||
reset_config_cache()
|
||||
|
||||
errors = validate(tmp_path / "nonexistent.xlsx", "Feb", cfg_path)
|
||||
assert any("nicht gefunden" in e for e in errors)
|
||||
|
||||
def test_year_not_in_filename(self, tmp_path):
|
||||
cfg_path = _write_config(tmp_path)
|
||||
xlsx_path = _write_dummy_xlsx(tmp_path, name="Anwesenheit_kein_jahr.xlsx")
|
||||
_write_dummy_pdf(tmp_path)
|
||||
|
||||
from src.config_loader import reset_config_cache
|
||||
reset_config_cache()
|
||||
|
||||
errors = validate(xlsx_path, "Feb", cfg_path)
|
||||
assert any("Jahr" in e for e in errors)
|
||||
|
||||
def test_unknown_sheet_name(self, tmp_path):
|
||||
cfg_path = _write_config(tmp_path)
|
||||
xlsx_path = _write_dummy_xlsx(tmp_path)
|
||||
_write_dummy_pdf(tmp_path)
|
||||
|
||||
from src.config_loader import reset_config_cache
|
||||
reset_config_cache()
|
||||
|
||||
errors = validate(xlsx_path, "Xyz", cfg_path)
|
||||
assert any("month_mapping" in e or "Sheet" in e for e in errors)
|
||||
|
||||
def test_pdf_template_missing(self, tmp_path):
|
||||
# No _write_dummy_pdf call → template missing
|
||||
cfg_path = _write_config(tmp_path)
|
||||
xlsx_path = _write_dummy_xlsx(tmp_path)
|
||||
|
||||
from src.config_loader import reset_config_cache
|
||||
reset_config_cache()
|
||||
|
||||
errors = validate(xlsx_path, "Feb", cfg_path)
|
||||
assert any("Template" in e or "template" in e.lower() for e in errors)
|
||||
|
||||
def test_config_missing_key(self, tmp_path):
|
||||
cfg = {
|
||||
"antragsnummer": "123",
|
||||
# omit 'ort' and other required keys
|
||||
}
|
||||
cfg_path = tmp_path / "config.json"
|
||||
cfg_path.write_text(json.dumps(cfg), encoding="utf-8")
|
||||
|
||||
from src.config_loader import reset_config_cache
|
||||
reset_config_cache()
|
||||
|
||||
errors = validate(tmp_path / "file.xlsx", "Feb", cfg_path)
|
||||
assert any("Konfigurationsfehler" in e or "fehlen" in e.lower() for e in errors)
|
||||
|
||||
def test_non_xlsx_extension_flagged(self, tmp_path):
|
||||
cfg_path = _write_config(tmp_path)
|
||||
_write_dummy_pdf(tmp_path)
|
||||
|
||||
csv_path = tmp_path / "Anwesenheit_2026.csv"
|
||||
csv_path.write_text("dummy")
|
||||
|
||||
from src.config_loader import reset_config_cache
|
||||
reset_config_cache()
|
||||
|
||||
errors = validate(csv_path, "Feb", cfg_path)
|
||||
assert any(".xlsx" in e or "Excel" in e for e in errors)
|
||||
Reference in new issue
Block a user