Some checks failed
Build & Push Docker / build (push) Has been cancelled
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
533 lines
14 KiB
Python
533 lines
14 KiB
Python
"""
|
|
Core business logic tests — adapted from original test_read_excel.py and test_inline_builder.py.
|
|
Tests the refactored app.core modules.
|
|
"""
|
|
|
|
import io
|
|
import pandas as pd
|
|
import pytest
|
|
from datetime import date, time
|
|
|
|
from app.core import (
|
|
read_excel, create_timetable, get_program_types, ScenarsError,
|
|
parse_inline_schedule, parse_inline_types, ValidationError,
|
|
validate_inputs, normalize_time,
|
|
)
|
|
|
|
|
|
def make_excel_bytes(df: pd.DataFrame) -> bytes:
|
|
bio = io.BytesIO()
|
|
with pd.ExcelWriter(bio, engine='openpyxl') as writer:
|
|
df.to_excel(writer, index=False)
|
|
return bio.getvalue()
|
|
|
|
|
|
# --- Validator tests ---
|
|
|
|
def test_validate_inputs_valid():
|
|
validate_inputs("Title", "Detail", 100)
|
|
|
|
|
|
def test_validate_inputs_empty_title():
|
|
with pytest.raises(ValidationError):
|
|
validate_inputs("", "Detail", 100)
|
|
|
|
|
|
def test_validate_inputs_long_title():
|
|
with pytest.raises(ValidationError):
|
|
validate_inputs("x" * 201, "Detail", 100)
|
|
|
|
|
|
def test_validate_inputs_file_too_large():
|
|
with pytest.raises(ValidationError):
|
|
validate_inputs("Title", "Detail", 11 * 1024 * 1024)
|
|
|
|
|
|
def test_normalize_time_hhmm():
|
|
t = normalize_time("09:00")
|
|
assert t == time(9, 0)
|
|
|
|
|
|
def test_normalize_time_hhmmss():
|
|
t = normalize_time("09:00:00")
|
|
assert t == time(9, 0)
|
|
|
|
|
|
def test_normalize_time_invalid():
|
|
assert normalize_time("invalid") is None
|
|
|
|
|
|
# --- Excel reader tests ---
|
|
|
|
def test_read_excel_happy_path():
|
|
df = pd.DataFrame({
|
|
'Datum': [pd.Timestamp('2025-11-13').date()],
|
|
'Zacatek': ['09:00'],
|
|
'Konec': ['10:00'],
|
|
'Program': ['Test program'],
|
|
'Typ': ['WORKSHOP'],
|
|
'Garant': ['Garant Name'],
|
|
'Poznamka': ['Pozn']
|
|
})
|
|
|
|
content = make_excel_bytes(df)
|
|
valid, errors = read_excel(content)
|
|
|
|
assert isinstance(valid, pd.DataFrame)
|
|
assert len(errors) == 0
|
|
assert len(valid) == 1
|
|
assert valid.iloc[0]['Program'] == 'Test program'
|
|
|
|
|
|
def test_read_excel_invalid_time():
|
|
df = pd.DataFrame({
|
|
'Datum': [pd.Timestamp('2025-11-13').date()],
|
|
'Zacatek': ['not-a-time'],
|
|
'Konec': ['10:00'],
|
|
'Program': ['Bad Time'],
|
|
'Typ': ['LECTURE'],
|
|
'Garant': [None],
|
|
'Poznamka': [None]
|
|
})
|
|
|
|
content = make_excel_bytes(df)
|
|
valid, errors = read_excel(content)
|
|
|
|
assert isinstance(errors, list)
|
|
assert len(errors) >= 1
|
|
|
|
|
|
def test_get_program_types():
|
|
form_data = {
|
|
'type_code_0': 'WORKSHOP',
|
|
'type_code_1': 'LECTURE',
|
|
'desc_0': 'Workshop description',
|
|
'desc_1': 'Lecture description',
|
|
'color_0': '#FF0000',
|
|
'color_1': '#00FF00',
|
|
}
|
|
|
|
descriptions, colors = get_program_types(form_data)
|
|
|
|
assert len(descriptions) == 2
|
|
assert descriptions['WORKSHOP'] == 'Workshop description'
|
|
assert descriptions['LECTURE'] == 'Lecture description'
|
|
assert colors['WORKSHOP'] == 'FFFF0000'
|
|
assert colors['LECTURE'] == 'FF00FF00'
|
|
|
|
|
|
# --- Timetable tests ---
|
|
|
|
def test_create_timetable():
|
|
df = pd.DataFrame({
|
|
'Datum': [pd.Timestamp('2025-11-13').date()],
|
|
'Zacatek': [time(9, 0)],
|
|
'Konec': [time(10, 0)],
|
|
'Program': ['Test Program'],
|
|
'Typ': ['WORKSHOP'],
|
|
'Garant': ['John Doe'],
|
|
'Poznamka': ['Test note'],
|
|
})
|
|
|
|
program_descriptions = {'WORKSHOP': 'Workshop Type'}
|
|
program_colors = {'WORKSHOP': 'FF0070C0'}
|
|
|
|
wb = create_timetable(
|
|
df, title="Test Timetable", detail="Test Detail",
|
|
program_descriptions=program_descriptions,
|
|
program_colors=program_colors
|
|
)
|
|
|
|
assert wb is not None
|
|
assert len(wb.sheetnames) > 0
|
|
ws = wb.active
|
|
assert ws['A1'].value == "Test Timetable"
|
|
assert ws['A2'].value == "Test Detail"
|
|
|
|
|
|
def test_create_timetable_with_color_dict():
|
|
df = pd.DataFrame({
|
|
'Datum': [pd.Timestamp('2025-11-13').date()],
|
|
'Zacatek': [time(9, 0)],
|
|
'Konec': [time(10, 0)],
|
|
'Program': ['Lecture 101'],
|
|
'Typ': ['LECTURE'],
|
|
'Garant': ['Dr. Smith'],
|
|
'Poznamka': [None],
|
|
})
|
|
|
|
program_descriptions = {'LECTURE': 'Standard Lecture'}
|
|
program_colors = {'LECTURE': 'FFFF6600'}
|
|
|
|
wb = create_timetable(
|
|
df, title="Advanced Timetable", detail="With color dict",
|
|
program_descriptions=program_descriptions,
|
|
program_colors=program_colors
|
|
)
|
|
|
|
assert wb is not None
|
|
ws = wb.active
|
|
assert ws['A1'].value == "Advanced Timetable"
|
|
|
|
|
|
# --- Inline schedule tests ---
|
|
|
|
def test_parse_inline_schedule():
|
|
form_data = {
|
|
'datum_0': '2025-11-13',
|
|
'zacatek_0': '09:00',
|
|
'konec_0': '10:00',
|
|
'program_0': 'Test Program',
|
|
'typ_0': 'WORKSHOP',
|
|
'garant_0': 'John Doe',
|
|
'poznamka_0': 'Test note',
|
|
'datum_1': '2025-11-13',
|
|
'zacatek_1': '10:30',
|
|
'konec_1': '11:30',
|
|
'program_1': 'Another Program',
|
|
'typ_1': 'LECTURE',
|
|
'garant_1': 'Jane Smith',
|
|
'poznamka_1': '',
|
|
}
|
|
|
|
df = parse_inline_schedule(form_data)
|
|
|
|
assert isinstance(df, pd.DataFrame)
|
|
assert len(df) == 2
|
|
assert df.iloc[0]['Program'] == 'Test Program'
|
|
assert df.iloc[0]['Typ'] == 'WORKSHOP'
|
|
assert df.iloc[1]['Program'] == 'Another Program'
|
|
assert df.iloc[1]['Typ'] == 'LECTURE'
|
|
|
|
|
|
def test_parse_inline_schedule_missing_required():
|
|
form_data = {
|
|
'datum_0': '2025-11-13',
|
|
'zacatek_0': '09:00',
|
|
}
|
|
|
|
with pytest.raises(ValidationError):
|
|
parse_inline_schedule(form_data)
|
|
|
|
|
|
def test_parse_inline_types():
|
|
form_data = {
|
|
'type_name_0': 'WORKSHOP',
|
|
'type_desc_0': 'Workshop Type',
|
|
'type_color_0': '#0070C0',
|
|
'type_name_1': 'LECTURE',
|
|
'type_desc_1': 'Lecture Type',
|
|
'type_color_1': '#FF6600',
|
|
}
|
|
|
|
descriptions, colors = parse_inline_types(form_data)
|
|
|
|
assert len(descriptions) == 2
|
|
assert descriptions['WORKSHOP'] == 'Workshop Type'
|
|
assert descriptions['LECTURE'] == 'Lecture Type'
|
|
assert colors['WORKSHOP'] == 'FF0070C0'
|
|
assert colors['LECTURE'] == 'FFFF6600'
|
|
|
|
|
|
# --- Integration tests ---
|
|
|
|
def test_inline_workflow_integration():
|
|
schedule_form = {
|
|
'datum_0': '2025-11-13',
|
|
'zacatek_0': '09:00',
|
|
'konec_0': '10:00',
|
|
'program_0': 'Opening',
|
|
'typ_0': 'KEYNOTE',
|
|
'garant_0': 'Dr. Smith',
|
|
'poznamka_0': 'Start of event',
|
|
}
|
|
|
|
types_form = {
|
|
'type_name_0': 'KEYNOTE',
|
|
'type_desc_0': 'Keynote Speech',
|
|
'type_color_0': '#FF0000',
|
|
}
|
|
|
|
form_data = {**schedule_form, **types_form}
|
|
|
|
df = parse_inline_schedule(form_data)
|
|
descriptions, colors = parse_inline_types(form_data)
|
|
|
|
wb = create_timetable(
|
|
df, title="Integration Test Event", detail="Testing inline workflow",
|
|
program_descriptions=descriptions, program_colors=colors
|
|
)
|
|
|
|
assert wb is not None
|
|
ws = wb.active
|
|
assert ws['A1'].value == "Integration Test Event"
|
|
assert ws['A2'].value == "Testing inline workflow"
|
|
|
|
|
|
def test_excel_import_to_step2_workflow():
|
|
import base64
|
|
|
|
df = pd.DataFrame({
|
|
'Datum': [pd.Timestamp('2025-11-13').date()],
|
|
'Zacatek': ['09:00'],
|
|
'Konec': ['10:00'],
|
|
'Program': ['Opening Keynote'],
|
|
'Typ': ['KEYNOTE'],
|
|
'Garant': ['John Smith'],
|
|
'Poznamka': ['Welcome speech']
|
|
})
|
|
|
|
file_content = make_excel_bytes(df)
|
|
|
|
valid_data, errors = read_excel(file_content)
|
|
assert len(errors) == 0
|
|
assert len(valid_data) == 1
|
|
|
|
program_types = sorted([str(t).strip() for t in valid_data["Typ"].dropna().unique()])
|
|
assert program_types == ['KEYNOTE']
|
|
|
|
file_content_base64 = base64.b64encode(file_content).decode('utf-8')
|
|
|
|
form_data = {
|
|
'title': 'Test Event',
|
|
'detail': 'Test Detail',
|
|
'file_content_base64': file_content_base64,
|
|
'type_code_0': 'KEYNOTE',
|
|
'desc_0': 'Main keynote presentation',
|
|
'color_0': '#FF0000',
|
|
'step': '3'
|
|
}
|
|
|
|
descriptions, colors = get_program_types(form_data)
|
|
|
|
assert descriptions['KEYNOTE'] == 'Main keynote presentation'
|
|
assert colors['KEYNOTE'] == 'FFFF0000'
|
|
|
|
file_content_decoded = base64.b64decode(form_data['file_content_base64'])
|
|
data, _ = read_excel(file_content_decoded)
|
|
|
|
wb = create_timetable(
|
|
data, title=form_data['title'], detail=form_data['detail'],
|
|
program_descriptions=descriptions, program_colors=colors
|
|
)
|
|
|
|
assert wb is not None
|
|
ws = wb.active
|
|
assert ws['A1'].value == 'Test Event'
|
|
assert ws['A2'].value == 'Test Detail'
|
|
|
|
|
|
def test_excel_import_to_inline_editor_workflow():
|
|
import base64
|
|
|
|
df = pd.DataFrame({
|
|
'Datum': [pd.Timestamp('2025-11-13').date(), pd.Timestamp('2025-11-14').date()],
|
|
'Zacatek': ['09:00', '14:00'],
|
|
'Konec': ['10:00', '15:30'],
|
|
'Program': ['Morning Session', 'Afternoon Workshop'],
|
|
'Typ': ['KEYNOTE', 'WORKSHOP'],
|
|
'Garant': ['Alice', 'Bob'],
|
|
'Poznamka': ['', 'Hands-on']
|
|
})
|
|
|
|
file_content = make_excel_bytes(df)
|
|
|
|
valid_data, errors = read_excel(file_content)
|
|
assert len(errors) == 0
|
|
assert len(valid_data) == 2
|
|
|
|
file_content_base64 = base64.b64encode(file_content).decode('utf-8')
|
|
|
|
file_content_decoded = base64.b64decode(file_content_base64)
|
|
data_in_editor, _ = read_excel(file_content_decoded)
|
|
|
|
assert len(data_in_editor) == 2
|
|
assert data_in_editor.iloc[0]['Program'] == 'Morning Session'
|
|
assert data_in_editor.iloc[1]['Program'] == 'Afternoon Workshop'
|
|
|
|
program_types = sorted([str(t).strip() for t in data_in_editor["Typ"].dropna().unique()])
|
|
assert set(program_types) == {'KEYNOTE', 'WORKSHOP'}
|
|
|
|
|
|
# --- Inline builder tests (from test_inline_builder.py) ---
|
|
|
|
def test_inline_builder_valid_form():
|
|
form_data = {
|
|
'title': 'Test Conference',
|
|
'detail': 'Testing inline builder',
|
|
'step': 'builder',
|
|
'datum_0': '2025-11-13',
|
|
'zacatek_0': '09:00',
|
|
'konec_0': '10:00',
|
|
'program_0': 'Opening Keynote',
|
|
'typ_0': 'KEYNOTE',
|
|
'garant_0': 'Dr. Smith',
|
|
'poznamka_0': 'Welcome speech',
|
|
'datum_1': '2025-11-13',
|
|
'zacatek_1': '10:30',
|
|
'konec_1': '11:30',
|
|
'program_1': 'Workshop: Python',
|
|
'typ_1': 'WORKSHOP',
|
|
'garant_1': 'Jane Doe',
|
|
'poznamka_1': 'Hands-on coding',
|
|
'type_name_0': 'KEYNOTE',
|
|
'type_desc_0': 'Main Address',
|
|
'type_color_0': '#FF0000',
|
|
'type_name_1': 'WORKSHOP',
|
|
'type_desc_1': 'Interactive Session',
|
|
'type_color_1': '#0070C0',
|
|
}
|
|
|
|
schedule = parse_inline_schedule(form_data)
|
|
assert len(schedule) == 2
|
|
assert schedule.iloc[0]['Program'] == 'Opening Keynote'
|
|
|
|
descriptions, colors = parse_inline_types(form_data)
|
|
assert len(descriptions) == 2
|
|
assert descriptions['KEYNOTE'] == 'Main Address'
|
|
assert colors['KEYNOTE'] == 'FFFF0000'
|
|
|
|
wb = create_timetable(
|
|
schedule, title=form_data['title'], detail=form_data['detail'],
|
|
program_descriptions=descriptions, program_colors=colors
|
|
)
|
|
|
|
assert wb is not None
|
|
ws = wb.active
|
|
assert ws['A1'].value == 'Test Conference'
|
|
|
|
|
|
def test_inline_builder_missing_type_definition():
|
|
form_data = {
|
|
'datum_0': '2025-11-13',
|
|
'zacatek_0': '09:00',
|
|
'konec_0': '10:00',
|
|
'program_0': 'Unknown Program',
|
|
'typ_0': 'UNKNOWN_TYPE',
|
|
'garant_0': 'Someone',
|
|
'poznamka_0': '',
|
|
'type_name_0': 'LECTURE',
|
|
'type_desc_0': 'Standard Lecture',
|
|
'type_color_0': '#3498db',
|
|
}
|
|
|
|
schedule = parse_inline_schedule(form_data)
|
|
descriptions, colors = parse_inline_types(form_data)
|
|
|
|
with pytest.raises(Exception):
|
|
create_timetable(schedule, 'Bad', 'Bad', descriptions, colors)
|
|
|
|
|
|
def test_inline_builder_empty_type_definition():
|
|
form_data = {
|
|
'type_name_0': '',
|
|
'type_desc_0': 'Empty type',
|
|
'type_color_0': '#FF0000',
|
|
'type_name_1': 'WORKSHOP',
|
|
'type_desc_1': 'Workshop Type',
|
|
'type_color_1': '#0070C0',
|
|
}
|
|
|
|
descriptions, colors = parse_inline_types(form_data)
|
|
|
|
assert 'WORKSHOP' in descriptions
|
|
assert '' not in descriptions
|
|
|
|
|
|
def test_inline_builder_overlapping_times():
|
|
form_data = {
|
|
'datum_0': '2025-11-13',
|
|
'zacatek_0': '09:00',
|
|
'konec_0': '10:00',
|
|
'program_0': 'Program A',
|
|
'typ_0': 'WORKSHOP',
|
|
'garant_0': 'John',
|
|
'poznamka_0': '',
|
|
'datum_1': '2025-11-13',
|
|
'zacatek_1': '09:30',
|
|
'konec_1': '10:30',
|
|
'program_1': 'Program B',
|
|
'typ_1': 'WORKSHOP',
|
|
'garant_1': 'Jane',
|
|
'poznamka_1': '',
|
|
'type_name_0': 'WORKSHOP',
|
|
'type_desc_0': 'Workshop',
|
|
'type_color_0': '#0070C0',
|
|
}
|
|
|
|
schedule = parse_inline_schedule(form_data)
|
|
descriptions, colors = parse_inline_types(form_data)
|
|
|
|
assert len(schedule) == 2
|
|
|
|
wb = create_timetable(schedule, 'Conf', 'Test', descriptions, colors)
|
|
assert wb is not None
|
|
|
|
|
|
def test_inline_builder_multiday():
|
|
form_data = {
|
|
'datum_0': '2025-11-13',
|
|
'zacatek_0': '09:00',
|
|
'konec_0': '10:00',
|
|
'program_0': 'Day 1 Opening',
|
|
'typ_0': 'KEYNOTE',
|
|
'garant_0': 'Dr. A',
|
|
'poznamka_0': '',
|
|
'datum_1': '2025-11-14',
|
|
'zacatek_1': '09:00',
|
|
'konec_1': '10:00',
|
|
'program_1': 'Day 2 Opening',
|
|
'typ_1': 'KEYNOTE',
|
|
'garant_1': 'Dr. B',
|
|
'poznamka_1': '',
|
|
'type_name_0': 'KEYNOTE',
|
|
'type_desc_0': 'Keynote Speech',
|
|
'type_color_0': '#FF6600',
|
|
}
|
|
|
|
schedule = parse_inline_schedule(form_data)
|
|
descriptions, colors = parse_inline_types(form_data)
|
|
|
|
wb = create_timetable(schedule, 'Multi-day', 'Test', descriptions, colors)
|
|
|
|
assert wb is not None
|
|
ws = wb.active
|
|
assert ws['A1'].value == 'Multi-day'
|
|
|
|
|
|
def test_inline_builder_validation_errors():
|
|
form_data_missing = {
|
|
'datum_0': '2025-11-13',
|
|
'garant_0': 'John',
|
|
}
|
|
|
|
with pytest.raises(ValidationError):
|
|
parse_inline_schedule(form_data_missing)
|
|
|
|
|
|
def test_inline_builder_with_empty_rows():
|
|
form_data = {
|
|
'datum_0': '2025-11-13',
|
|
'zacatek_0': '09:00',
|
|
'konec_0': '10:00',
|
|
'program_0': 'Program 1',
|
|
'typ_0': 'WORKSHOP',
|
|
'garant_0': 'John',
|
|
'poznamka_0': '',
|
|
'datum_1': '',
|
|
'zacatek_1': '',
|
|
'konec_1': '',
|
|
'program_1': '',
|
|
'typ_1': '',
|
|
'garant_1': '',
|
|
'poznamka_1': '',
|
|
'type_name_0': 'WORKSHOP',
|
|
'type_desc_0': 'Workshop',
|
|
'type_color_0': '#0070C0',
|
|
}
|
|
|
|
schedule = parse_inline_schedule(form_data)
|
|
|
|
assert len(schedule) == 1
|
|
assert schedule.iloc[0]['Program'] == 'Program 1'
|