BoilerWorks

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

Keyboard Shortcuts

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