several updates to functionality
This commit is contained in:
1
tests/__init__.py
Normal file
1
tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Test package."""
|
||||
126
tests/test_cli_options.py
Normal file
126
tests/test_cli_options.py
Normal file
@@ -0,0 +1,126 @@
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import generator
|
||||
|
||||
|
||||
class CliOptionsTest(unittest.TestCase):
|
||||
def test_output_dir_and_duration_minutes(self) -> None:
|
||||
config_date = "2026-02-06"
|
||||
config_time = "15:00"
|
||||
story_data = {
|
||||
"meta": {"name": "CLI Options Story", "log_prefix": "LOG"},
|
||||
"events": [
|
||||
{"id": "log_01", "title": "Kickoff", "story": "Hello"},
|
||||
],
|
||||
}
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
previous_cwd = os.getcwd()
|
||||
os.chdir(tmpdir)
|
||||
try:
|
||||
with open("config.toml", "w", encoding="utf-8") as f:
|
||||
f.write(
|
||||
"\n".join(
|
||||
[
|
||||
f'start_date = "{config_date}"',
|
||||
f'start_time = "{config_time}"',
|
||||
"blocked_weeks = []",
|
||||
"blocked_dates = []",
|
||||
"skip_day_after_ascension = false",
|
||||
]
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
story_path = os.path.join(tmpdir, "story.json")
|
||||
with open(story_path, "w", encoding="utf-8") as f:
|
||||
json.dump(story_data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
output_dir = "custom_output"
|
||||
generator.generate_single_file(
|
||||
story_path,
|
||||
preview_html=True,
|
||||
output_dir=output_dir,
|
||||
duration_minutes=90,
|
||||
)
|
||||
|
||||
slug = generator.sanitize_filename(story_data["meta"]["name"])
|
||||
preview_path = os.path.join(output_dir, f"PREVIEW_{slug}.html")
|
||||
ics_path = os.path.join(output_dir, f"FULL_SERIES_{slug}.ics")
|
||||
self.assertTrue(os.path.exists(preview_path))
|
||||
self.assertTrue(os.path.exists(ics_path))
|
||||
|
||||
with open(preview_path, "r", encoding="utf-8") as f:
|
||||
preview_html = f.read()
|
||||
|
||||
self.assertIn("15:00-16:30", preview_html)
|
||||
finally:
|
||||
os.chdir(previous_cwd)
|
||||
|
||||
def test_config_path_override_and_timezone(self) -> None:
|
||||
story_data = {
|
||||
"meta": {"name": "CLI Config Story", "log_prefix": "LOG"},
|
||||
"events": [
|
||||
{"id": "log_01", "title": "Kickoff", "story": "Hello"},
|
||||
],
|
||||
}
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
previous_cwd = os.getcwd()
|
||||
os.chdir(tmpdir)
|
||||
try:
|
||||
with open("config.toml", "w", encoding="utf-8") as f:
|
||||
f.write(
|
||||
"\n".join(
|
||||
[
|
||||
'start_date = "2026-02-06"',
|
||||
'start_time = "15:00"',
|
||||
"blocked_weeks = []",
|
||||
"blocked_dates = []",
|
||||
"skip_day_after_ascension = false",
|
||||
]
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
override_path = os.path.join(tmpdir, "override.toml")
|
||||
with open(override_path, "w", encoding="utf-8") as f:
|
||||
f.write(
|
||||
"\n".join(
|
||||
[
|
||||
'start_date = "2026-02-06"',
|
||||
'start_time = "14:00"',
|
||||
"blocked_weeks = []",
|
||||
"blocked_dates = []",
|
||||
"skip_day_after_ascension = false",
|
||||
]
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
story_path = os.path.join(tmpdir, "story.json")
|
||||
with open(story_path, "w", encoding="utf-8") as f:
|
||||
json.dump(story_data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
output_dir = "custom_output"
|
||||
generator.generate_single_file(
|
||||
story_path,
|
||||
preview_html=True,
|
||||
output_dir=output_dir,
|
||||
config_path=override_path,
|
||||
timezone_name="UTC",
|
||||
)
|
||||
|
||||
slug = generator.sanitize_filename(story_data["meta"]["name"])
|
||||
preview_path = os.path.join(output_dir, f"PREVIEW_{slug}.html")
|
||||
self.assertTrue(os.path.exists(preview_path))
|
||||
|
||||
with open(preview_path, "r", encoding="utf-8") as f:
|
||||
preview_html = f.read()
|
||||
|
||||
self.assertIn("14:00-15:00", preview_html)
|
||||
self.assertIn("(UTC)", preview_html)
|
||||
finally:
|
||||
os.chdir(previous_cwd)
|
||||
38
tests/test_config_validation.py
Normal file
38
tests/test_config_validation.py
Normal file
@@ -0,0 +1,38 @@
|
||||
import unittest
|
||||
|
||||
from validation import validate_config
|
||||
|
||||
|
||||
class ConfigValidationTest(unittest.TestCase):
|
||||
def test_optional_fields_must_be_strings(self) -> None:
|
||||
config = {
|
||||
"start_date": "2026-02-04",
|
||||
"start_time": "15:00",
|
||||
"repo_url": 123,
|
||||
"organizer_email": ["not", "a", "string"],
|
||||
"uid_namespace": {"bad": "type"},
|
||||
"blocked_weeks": [],
|
||||
"blocked_dates": [],
|
||||
"skip_day_after_ascension": False,
|
||||
}
|
||||
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
validate_config(config)
|
||||
|
||||
message = str(ctx.exception)
|
||||
self.assertIn("repo_url skal være en streng", message)
|
||||
self.assertIn("organizer_email skal være en streng", message)
|
||||
self.assertIn("uid_namespace skal være en streng", message)
|
||||
|
||||
def test_optional_fields_accept_none_or_missing(self) -> None:
|
||||
config = {
|
||||
"start_date": "2026-02-04",
|
||||
"start_time": "15:00",
|
||||
"blocked_weeks": [],
|
||||
"blocked_dates": [],
|
||||
"skip_day_after_ascension": False,
|
||||
"repo_url": None,
|
||||
}
|
||||
|
||||
validated = validate_config(config)
|
||||
self.assertEqual(validated["start_date"], "2026-02-04")
|
||||
56
tests/test_date_skipping.py
Normal file
56
tests/test_date_skipping.py
Normal file
@@ -0,0 +1,56 @@
|
||||
import unittest
|
||||
from datetime import date
|
||||
|
||||
from validation import validate_config
|
||||
from generator import is_blocked_date
|
||||
|
||||
|
||||
class DateSkippingTest(unittest.TestCase):
|
||||
def test_blocked_week_is_skipped(self) -> None:
|
||||
target_date = date(2026, 2, 6)
|
||||
config = validate_config(
|
||||
{
|
||||
"start_date": "2026-02-04",
|
||||
"start_time": "15:00",
|
||||
"blocked_weeks": [target_date.isocalendar()[1]],
|
||||
"blocked_dates": [],
|
||||
"skip_day_after_ascension": False,
|
||||
}
|
||||
)
|
||||
|
||||
blocked, reason = is_blocked_date(target_date, {}, config)
|
||||
self.assertTrue(blocked)
|
||||
self.assertIn("Ferieuge", reason or "")
|
||||
|
||||
def test_blocked_date_is_skipped(self) -> None:
|
||||
target_date = date(2026, 2, 6)
|
||||
config = validate_config(
|
||||
{
|
||||
"start_date": "2026-02-04",
|
||||
"start_time": "15:00",
|
||||
"blocked_weeks": [],
|
||||
"blocked_dates": ["02-06"],
|
||||
"skip_day_after_ascension": False,
|
||||
}
|
||||
)
|
||||
|
||||
blocked, reason = is_blocked_date(target_date, {}, config)
|
||||
self.assertTrue(blocked)
|
||||
self.assertEqual(reason, "Manuelt blokeret dato")
|
||||
|
||||
def test_day_after_ascension_is_skipped(self) -> None:
|
||||
target_date = date(2026, 5, 15)
|
||||
config = validate_config(
|
||||
{
|
||||
"start_date": "2026-02-04",
|
||||
"start_time": "15:00",
|
||||
"blocked_weeks": [],
|
||||
"blocked_dates": [],
|
||||
"skip_day_after_ascension": True,
|
||||
}
|
||||
)
|
||||
holidays = {date(2026, 5, 14): "Kristi Himmelfart"}
|
||||
|
||||
blocked, reason = is_blocked_date(target_date, holidays, config)
|
||||
self.assertTrue(blocked)
|
||||
self.assertEqual(reason, "Dagen efter Kr. Himmelfart")
|
||||
25
tests/test_html_injection.py
Normal file
25
tests/test_html_injection.py
Normal file
@@ -0,0 +1,25 @@
|
||||
import unittest
|
||||
|
||||
from generator import inject_outlook_hacks
|
||||
|
||||
|
||||
class HtmlInjectionTest(unittest.TestCase):
|
||||
def test_html_injection_adds_folded_alt_desc(self) -> None:
|
||||
ical_text = "\n".join(
|
||||
[
|
||||
"BEGIN:VCALENDAR",
|
||||
"BEGIN:VEVENT",
|
||||
"UID:abc123",
|
||||
"END:VEVENT",
|
||||
"END:VCALENDAR",
|
||||
]
|
||||
)
|
||||
long_html = "A" * 200
|
||||
result = inject_outlook_hacks(ical_text, {"abc123": long_html})
|
||||
|
||||
self.assertIn("X-ALT-DESC;FMTTYPE=text/html:", result)
|
||||
self.assertIn("X-MICROSOFT-CDO-BUSYSTATUS:BUSY", result)
|
||||
self.assertIn("TRANSP:OPAQUE", result)
|
||||
alt_index = result.find("X-ALT-DESC;FMTTYPE=text/html:")
|
||||
self.assertNotEqual(alt_index, -1)
|
||||
self.assertIn("\r\n ", result[alt_index : alt_index + 200])
|
||||
60
tests/test_no_color.py
Normal file
60
tests/test_no_color.py
Normal file
@@ -0,0 +1,60 @@
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from contextlib import redirect_stdout
|
||||
|
||||
import generator
|
||||
|
||||
|
||||
class NoColorTest(unittest.TestCase):
|
||||
def test_disable_colors_removes_ansi_codes(self) -> None:
|
||||
color_snapshot = {
|
||||
name: getattr(generator.Colors, name)
|
||||
for name in ("HEADER", "OKBLUE", "OKGREEN", "WARNING", "FAIL", "ENDC")
|
||||
}
|
||||
config_date = "2026-02-06"
|
||||
config_time = "15:00"
|
||||
story_data = {
|
||||
"meta": {"name": "No Color Story", "log_prefix": "LOG"},
|
||||
"events": [
|
||||
{"id": "log_01", "title": "Kickoff", "story": "Hello"},
|
||||
],
|
||||
}
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
previous_cwd = os.getcwd()
|
||||
os.chdir(tmpdir)
|
||||
try:
|
||||
with open("config.toml", "w", encoding="utf-8") as f:
|
||||
f.write(
|
||||
"\n".join(
|
||||
[
|
||||
f'start_date = "{config_date}"',
|
||||
f'start_time = "{config_time}"',
|
||||
"blocked_weeks = []",
|
||||
"blocked_dates = []",
|
||||
"skip_day_after_ascension = false",
|
||||
]
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
story_path = os.path.join(tmpdir, "story.json")
|
||||
with open(story_path, "w", encoding="utf-8") as f:
|
||||
json.dump(story_data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
generator.disable_colors()
|
||||
buffer = io.StringIO()
|
||||
with redirect_stdout(buffer):
|
||||
generator.generate_single_file(story_path)
|
||||
|
||||
output = buffer.getvalue()
|
||||
self.assertNotIn("\x1b[", output)
|
||||
for name in ("HEADER", "OKBLUE", "OKGREEN", "WARNING", "FAIL", "ENDC"):
|
||||
self.assertEqual(getattr(generator.Colors, name), "")
|
||||
finally:
|
||||
for name, value in color_snapshot.items():
|
||||
setattr(generator.Colors, name, value)
|
||||
os.chdir(previous_cwd)
|
||||
132
tests/test_preview_html.py
Normal file
132
tests/test_preview_html.py
Normal file
@@ -0,0 +1,132 @@
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import generator
|
||||
|
||||
|
||||
class PreviewHtmlTest(unittest.TestCase):
|
||||
def test_preview_html_output(self) -> None:
|
||||
config_date = "2026-02-06"
|
||||
config_time = "15:00"
|
||||
story_data = {
|
||||
"meta": {
|
||||
"name": "Preview Test Story",
|
||||
"log_prefix": "LOG",
|
||||
},
|
||||
"events": [
|
||||
{
|
||||
"id": "log_01",
|
||||
"title": "Kickoff",
|
||||
"story": "<b>HELLO</b><br>World",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
previous_cwd = os.getcwd()
|
||||
os.chdir(tmpdir)
|
||||
try:
|
||||
with open("config.toml", "w", encoding="utf-8") as f:
|
||||
f.write(
|
||||
"\n".join(
|
||||
[
|
||||
f'start_date = "{config_date}"',
|
||||
f'start_time = "{config_time}"',
|
||||
'repo_url = "https://example.com/repo"',
|
||||
"blocked_weeks = []",
|
||||
"blocked_dates = []",
|
||||
"skip_day_after_ascension = false",
|
||||
]
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
story_path = os.path.join(tmpdir, "story.json")
|
||||
with open(story_path, "w", encoding="utf-8") as f:
|
||||
json.dump(story_data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
generator.generate_single_file(story_path, preview_html=True)
|
||||
|
||||
slug = generator.sanitize_filename(story_data["meta"]["name"])
|
||||
preview_path = os.path.join(
|
||||
"fredagsbar_output",
|
||||
f"PREVIEW_{slug}.html",
|
||||
)
|
||||
self.assertTrue(os.path.exists(preview_path))
|
||||
|
||||
with open(preview_path, "r", encoding="utf-8") as f:
|
||||
preview_html = f.read()
|
||||
|
||||
expected_date = generator.next_or_same_friday(
|
||||
datetime.strptime(config_date, "%Y-%m-%d").date()
|
||||
)
|
||||
self.assertIn(expected_date.strftime("%Y-%m-%d"), preview_html)
|
||||
self.assertIn("15:00-16:00", preview_html)
|
||||
self.assertIn(story_data["events"][0]["title"], preview_html)
|
||||
self.assertIn(story_data["events"][0]["story"], preview_html)
|
||||
finally:
|
||||
os.chdir(previous_cwd)
|
||||
|
||||
def test_preview_includes_skipped_dates(self) -> None:
|
||||
config_date = "2026-02-06"
|
||||
config_time = "15:00"
|
||||
story_data = {
|
||||
"meta": {
|
||||
"name": "Preview Skip Story",
|
||||
"log_prefix": "LOG",
|
||||
},
|
||||
"events": [
|
||||
{
|
||||
"id": "log_01",
|
||||
"title": "Kickoff",
|
||||
"story": "<b>HELLO</b><br>World",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
previous_cwd = os.getcwd()
|
||||
os.chdir(tmpdir)
|
||||
try:
|
||||
with open("config.toml", "w", encoding="utf-8") as f:
|
||||
f.write(
|
||||
"\n".join(
|
||||
[
|
||||
f'start_date = "{config_date}"',
|
||||
f'start_time = "{config_time}"',
|
||||
'repo_url = "https://example.com/repo"',
|
||||
"blocked_weeks = []",
|
||||
'blocked_dates = ["02-06"]',
|
||||
"skip_day_after_ascension = false",
|
||||
]
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
story_path = os.path.join(tmpdir, "story.json")
|
||||
with open(story_path, "w", encoding="utf-8") as f:
|
||||
json.dump(story_data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
generator.generate_single_file(story_path, preview_html=True)
|
||||
|
||||
slug = generator.sanitize_filename(story_data["meta"]["name"])
|
||||
preview_path = os.path.join(
|
||||
"fredagsbar_output",
|
||||
f"PREVIEW_{slug}.html",
|
||||
)
|
||||
self.assertTrue(os.path.exists(preview_path))
|
||||
|
||||
with open(preview_path, "r", encoding="utf-8") as f:
|
||||
preview_html = f.read()
|
||||
|
||||
start_date = datetime.strptime(config_date, "%Y-%m-%d").date()
|
||||
expected_date = generator.next_or_same_friday(start_date) + timedelta(weeks=1)
|
||||
self.assertIn("Skipped dates", preview_html)
|
||||
self.assertIn(start_date.strftime("%Y-%m-%d"), preview_html)
|
||||
self.assertIn("Manuelt blokeret dato", preview_html)
|
||||
self.assertIn(expected_date.strftime("%Y-%m-%d"), preview_html)
|
||||
finally:
|
||||
os.chdir(previous_cwd)
|
||||
27
tests/test_uid_mapping.py
Normal file
27
tests/test_uid_mapping.py
Normal file
@@ -0,0 +1,27 @@
|
||||
import unittest
|
||||
|
||||
from validation import validate_uid_mapping
|
||||
|
||||
|
||||
class UidMappingTest(unittest.TestCase):
|
||||
def test_ambiguous_mapping_requires_ids(self) -> None:
|
||||
events = [
|
||||
{"title": "Event A"},
|
||||
{"title": "Event B"},
|
||||
]
|
||||
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
validate_uid_mapping(events, {"Event A"}, {2})
|
||||
|
||||
message = str(ctx.exception)
|
||||
self.assertIn("Ambivalent UID-mapping", message)
|
||||
self.assertIn("events[].id", message)
|
||||
|
||||
def test_ambiguous_mapping_with_ids_returns_false(self) -> None:
|
||||
events = [
|
||||
{"id": "log_01", "title": "Event A"},
|
||||
{"id": "log_02", "title": "Event B"},
|
||||
]
|
||||
|
||||
allow = validate_uid_mapping(events, {"Event A"}, {2})
|
||||
self.assertFalse(allow)
|
||||
Reference in New Issue
Block a user