BoilerWorks

boilerworks / tests / test_wizard.py
Source Blame History 177 lines
0cb4a5e… anonymous 1 """Tests for boilerworks.wizard."""
0cb4a5e… anonymous 2
0cb4a5e… anonymous 3 from __future__ import annotations
0cb4a5e… anonymous 4
0cb4a5e… anonymous 5 from pathlib import Path
0cb4a5e… anonymous 6 from unittest.mock import patch
0cb4a5e… anonymous 7
0cb4a5e… anonymous 8 import pytest
0cb4a5e… anonymous 9
0cb4a5e… anonymous 10 from boilerworks.wizard import _template_choices, _validate_slug
0cb4a5e… anonymous 11
0cb4a5e… anonymous 12
0cb4a5e… anonymous 13 class TestValidateSlug:
0cb4a5e… anonymous 14 def test_valid_slugs(self) -> None:
0cb4a5e… anonymous 15 for slug in ["my-app", "app", "a1b2", "my-project-v2"]:
0cb4a5e… anonymous 16 assert _validate_slug(slug) is True
0cb4a5e… anonymous 17
0cb4a5e… anonymous 18 def test_empty_returns_message(self) -> None:
0cb4a5e… anonymous 19 result = _validate_slug("")
0cb4a5e… anonymous 20 assert isinstance(result, str)
0cb4a5e… anonymous 21 assert "required" in result.lower()
0cb4a5e… anonymous 22
0cb4a5e… anonymous 23 def test_spaces_returns_message(self) -> None:
0cb4a5e… anonymous 24 result = _validate_slug("my app")
0cb4a5e… anonymous 25 assert isinstance(result, str)
0cb4a5e… anonymous 26
0cb4a5e… anonymous 27 def test_uppercase_returns_message(self) -> None:
0cb4a5e… anonymous 28 result = _validate_slug("MyApp")
0cb4a5e… anonymous 29 assert isinstance(result, str)
0cb4a5e… anonymous 30
0cb4a5e… anonymous 31 def test_leading_digit_returns_message(self) -> None:
0cb4a5e… anonymous 32 result = _validate_slug("1app")
0cb4a5e… anonymous 33 assert isinstance(result, str)
0cb4a5e… anonymous 34
0cb4a5e… anonymous 35
0cb4a5e… anonymous 36 class TestTemplateChoices:
0cb4a5e… anonymous 37 def test_returns_choices_list(self) -> None:
0cb4a5e… anonymous 38 from boilerworks.registry import Registry
0cb4a5e… anonymous 39
0cb4a5e… anonymous 40 registry = Registry()
0cb4a5e… anonymous 41 templates = registry.filter_by_size("full")
0cb4a5e… anonymous 42 choices = _template_choices(templates)
0cb4a5e… anonymous 43 assert len(choices) > 0
0cb4a5e… anonymous 44
0cb4a5e… anonymous 45 def test_separators_per_language(self) -> None:
0cb4a5e… anonymous 46 import questionary
0cb4a5e… anonymous 47
0cb4a5e… anonymous 48 from boilerworks.registry import Registry
0cb4a5e… anonymous 49
0cb4a5e… anonymous 50 registry = Registry()
0cb4a5e… anonymous 51 templates = registry.list_all()
0cb4a5e… anonymous 52 choices = _template_choices(templates)
0cb4a5e… anonymous 53 separators = [c for c in choices if isinstance(c, questionary.Separator)]
0cb4a5e… anonymous 54 # Should have at least one separator per language group
0cb4a5e… anonymous 55 assert len(separators) >= 5
0cb4a5e… anonymous 56
0cb4a5e… anonymous 57 def test_choice_values_are_template_names(self) -> None:
0cb4a5e… anonymous 58 import questionary
0cb4a5e… anonymous 59
0cb4a5e… anonymous 60 from boilerworks.registry import Registry
0cb4a5e… anonymous 61
0cb4a5e… anonymous 62 registry = Registry()
0cb4a5e… anonymous 63 templates = registry.filter_by_size("micro")
0cb4a5e… anonymous 64 choices = _template_choices(templates)
0cb4a5e… anonymous 65 # Separator is a subclass of Choice in questionary, so exclude them explicitly
0cb4a5e… anonymous 66 real_choices = [
0cb4a5e… anonymous 67 c for c in choices if isinstance(c, questionary.Choice) and not isinstance(c, questionary.Separator)
0cb4a5e… anonymous 68 ]
0cb4a5e… anonymous 69 names = {c.value for c in real_choices}
0cb4a5e… anonymous 70 expected = {t.name for t in templates}
0cb4a5e… anonymous 71 assert names == expected
0cb4a5e… anonymous 72
0cb4a5e… anonymous 73
0cb4a5e… anonymous 74 class TestRunWizard:
0cb4a5e… anonymous 75 """Integration tests for run_wizard using mocked questionary."""
0cb4a5e… anonymous 76
0cb4a5e… anonymous 77 def test_wizard_writes_manifest(self, tmp_path: Path) -> None:
0cb4a5e… anonymous 78 output_file = tmp_path / "boilerworks.yaml"
0cb4a5e… anonymous 79
0cb4a5e… anonymous 80 with (
0cb4a5e… anonymous 81 patch("questionary.text") as mock_text,
0cb4a5e… anonymous 82 patch("questionary.select") as mock_select,
0cb4a5e… anonymous 83 patch("questionary.confirm") as mock_confirm,
0cb4a5e… anonymous 84 patch("questionary.checkbox") as mock_checkbox,
0cb4a5e… anonymous 85 ):
0cb4a5e… anonymous 86 mock_text.return_value.ask.side_effect = [
0cb4a5e… anonymous 87 "my-test-app", # project name
0cb4a5e… anonymous 88 "", # region (empty → None)
0cb4a5e… anonymous 89 "", # domain (empty → None)
0cb4a5e… anonymous 90 ]
0cb4a5e… anonymous 91 mock_select.return_value.ask.side_effect = [
0cb4a5e… anonymous 92 "full", # size
0cb4a5e… anonymous 93 "django-nextjs", # family
0cb4a5e… anonymous 94 "standard", # topology
0cb4a5e… anonymous 95 "none", # cloud
0cb4a5e… anonymous 96 "none", # email
0cb4a5e… anonymous 97 "none", # e2e
0cb4a5e… anonymous 98 ]
0cb4a5e… anonymous 99 mock_confirm.return_value.ask.side_effect = [
0cb4a5e… anonymous 100 False, # mobile
0cb4a5e… anonymous 101 False, # web_presence
0cb4a5e… anonymous 102 True, # confirm write
0cb4a5e… anonymous 103 ]
0cb4a5e… anonymous 104 mock_checkbox.return_value.ask.return_value = [] # compliance
0cb4a5e… anonymous 105
0cb4a5e… anonymous 106 from boilerworks.wizard import run_wizard
0cb4a5e… anonymous 107
0cb4a5e… anonymous 108 run_wizard(output_path=output_file)
0cb4a5e… anonymous 109
0cb4a5e… anonymous 110 assert output_file.exists()
0cb4a5e… anonymous 111 content = output_file.read_text()
0cb4a5e… anonymous 112 assert "my-test-app" in content
0cb4a5e… anonymous 113 assert "django-nextjs" in content
0cb4a5e… anonymous 114
0cb4a5e… anonymous 115 def test_wizard_cancelled_on_project_name(self, tmp_path: Path) -> None:
0cb4a5e… anonymous 116 output_file = tmp_path / "boilerworks.yaml"
0cb4a5e… anonymous 117 with patch("questionary.text") as mock_text:
0cb4a5e… anonymous 118 mock_text.return_value.ask.return_value = None # user hit Ctrl+C
0cb4a5e… anonymous 119 from boilerworks.wizard import run_wizard
0cb4a5e… anonymous 120
0cb4a5e… anonymous 121 with pytest.raises(SystemExit):
0cb4a5e… anonymous 122 run_wizard(output_path=output_file)
0cb4a5e… anonymous 123
0cb4a5e… anonymous 124 assert not output_file.exists()
0cb4a5e… anonymous 125
0cb4a5e… anonymous 126 def test_wizard_cancelled_on_confirm(self, tmp_path: Path) -> None:
0cb4a5e… anonymous 127 output_file = tmp_path / "boilerworks.yaml"
0cb4a5e… anonymous 128 with (
0cb4a5e… anonymous 129 patch("questionary.text") as mock_text,
0cb4a5e… anonymous 130 patch("questionary.select") as mock_select,
0cb4a5e… anonymous 131 patch("questionary.confirm") as mock_confirm,
0cb4a5e… anonymous 132 patch("questionary.checkbox") as mock_checkbox,
0cb4a5e… anonymous 133 ):
0cb4a5e… anonymous 134 mock_text.return_value.ask.side_effect = ["my-app", "", ""]
0cb4a5e… anonymous 135 mock_select.return_value.ask.side_effect = ["full", "django-nextjs", "standard", "none", "none", "none"]
0cb4a5e… anonymous 136 mock_confirm.return_value.ask.side_effect = [False, False, False] # all confirms → False
0cb4a5e… anonymous 137 mock_checkbox.return_value.ask.return_value = []
0cb4a5e… anonymous 138
0cb4a5e… anonymous 139 from boilerworks.wizard import run_wizard
0cb4a5e… anonymous 140
0cb4a5e… anonymous 141 with pytest.raises(SystemExit):
0cb4a5e… anonymous 142 run_wizard(output_path=output_file)
0cb4a5e… anonymous 143
0cb4a5e… anonymous 144 assert not output_file.exists()
0cb4a5e… anonymous 145
0cb4a5e… anonymous 146 def test_wizard_with_cloud_and_region(self, tmp_path: Path) -> None:
0cb4a5e… anonymous 147 output_file = tmp_path / "boilerworks.yaml"
0cb4a5e… anonymous 148 with (
0cb4a5e… anonymous 149 patch("questionary.text") as mock_text,
0cb4a5e… anonymous 150 patch("questionary.select") as mock_select,
0cb4a5e… anonymous 151 patch("questionary.confirm") as mock_confirm,
0cb4a5e… anonymous 152 patch("questionary.checkbox") as mock_checkbox,
0cb4a5e… anonymous 153 ):
0cb4a5e… anonymous 154 mock_text.return_value.ask.side_effect = [
0cb4a5e… anonymous 155 "cloud-app", # project name
0cb4a5e… anonymous 156 "us-east-1", # region
0cb4a5e… anonymous 157 "myapp.com", # domain
0cb4a5e… anonymous 158 ]
0cb4a5e… anonymous 159 # fastapi-micro has only one topology, so topology select is skipped
0cb4a5e… anonymous 160 mock_select.return_value.ask.side_effect = [
0cb4a5e… anonymous 161 "micro", # size
0cb4a5e… anonymous 162 "fastapi-micro", # family
0cb4a5e… anonymous 163 "aws", # cloud (topology prompt skipped — single option)
0cb4a5e… anonymous 164 "ses", # email
0cb4a5e… anonymous 165 "playwright", # e2e
0cb4a5e… anonymous 166 ]
0cb4a5e… anonymous 167 mock_confirm.return_value.ask.return_value = True
0cb4a5e… anonymous 168 mock_checkbox.return_value.ask.return_value = ["soc2"]
0cb4a5e… anonymous 169
0cb4a5e… anonymous 170 from boilerworks.wizard import run_wizard
0cb4a5e… anonymous 171
0cb4a5e… anonymous 172 run_wizard(output_path=output_file)
0cb4a5e… anonymous 173
0cb4a5e… anonymous 174 assert output_file.exists()
0cb4a5e… anonymous 175 content = output_file.read_text()
0cb4a5e… anonymous 176 assert "cloud-app" in content
0cb4a5e… anonymous 177 assert "fastapi-micro" in content

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button