Skip to content

Commit 124bde6

Browse files
authored
test: raise fastapi_startkit coverage to >=80% and enforce threshold (#193)
Add real unit tests for previously under-covered pure-logic modules and raise the coverage gate from 68 to 80 now that actual coverage supports it. Modules gaining coverage: - masoniteorm/expressions/expressions.py (67% -> ~100%): alias splitting, order-by direction inference, join ON-clause construction, operator validation - support/string.py (82% -> 100%): Str/Stringable slug/trim/camel/snake helpers - utils/structures.py (23% -> ~96%): dotty data get/set, wildcard, module loader - storage/drivers/local.py (77% -> ~96%): put_file, store, stream, path resolution - masoniteorm/schema/Column.py (72% -> 100%): fluent column builder methods Total coverage 79.87% -> 80.93%; fail_under 68 -> 80.
1 parent a3a12f9 commit 124bde6

7 files changed

Lines changed: 569 additions & 1 deletion

File tree

fastapi_startkit/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ omit = [
149149
[tool.coverage.report]
150150
show_missing = true
151151
skip_covered = false
152-
fail_under = 68
152+
fail_under = 80
153153
exclude_lines = [
154154
"pragma: no cover",
155155
"if TYPE_CHECKING:",
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
"""Unit tests for ORM query expression helper classes (task #1214).
2+
3+
These classes carry the parsing/normalisation logic the grammars rely on
4+
(alias splitting, direction inference, ON-clause construction), so the tests
5+
assert on that behaviour rather than merely instantiating the objects.
6+
"""
7+
8+
import warnings
9+
10+
import pytest
11+
12+
from fastapi_startkit.masoniteorm.expressions.expressions import (
13+
AggregateExpression,
14+
BetweenExpression,
15+
GroupByExpression,
16+
HavingExpression,
17+
JoinClause,
18+
OnClause,
19+
OnValueClause,
20+
OrderByExpression,
21+
QueryExpression,
22+
Raw,
23+
SelectExpression,
24+
SubGroupExpression,
25+
SubSelectExpression,
26+
UpdateQueryExpression,
27+
)
28+
29+
30+
class TestQueryExpression:
31+
def test_stores_all_attributes(self):
32+
expr = QueryExpression("age", ">", 18, value_type="value", keyword="where")
33+
assert expr.column == "age"
34+
assert expr.equality == ">"
35+
assert expr.value == 18
36+
assert expr.value_type == "value"
37+
assert expr.keyword == "where"
38+
assert expr.raw is False
39+
assert expr.bindings == ()
40+
41+
42+
class TestHavingExpression:
43+
def test_infers_equality_when_only_value_given(self):
44+
expr = HavingExpression("total", 100)
45+
assert expr.equality == "="
46+
assert expr.value == 100
47+
assert expr.value_type == "having"
48+
49+
def test_keeps_explicit_equality_and_value(self):
50+
expr = HavingExpression("total", ">=", 100)
51+
assert expr.equality == ">="
52+
assert expr.value == 100
53+
54+
55+
class TestBetweenExpression:
56+
def test_defaults(self):
57+
expr = BetweenExpression("age", 18, 30)
58+
assert expr.low == 18
59+
assert expr.high == 30
60+
assert expr.equality == "BETWEEN"
61+
assert expr.value_type == "BETWEEN"
62+
assert expr.value is None
63+
assert expr.raw is False
64+
65+
66+
class TestSelectExpression:
67+
def test_splits_column_and_alias(self):
68+
expr = SelectExpression("name as full_name")
69+
assert expr.column == "name"
70+
assert expr.alias == "full_name"
71+
72+
def test_strips_surrounding_whitespace(self):
73+
expr = SelectExpression(" email ")
74+
assert expr.column == "email"
75+
assert expr.alias is None
76+
77+
def test_raw_column_is_not_split(self):
78+
expr = SelectExpression("count(*) as total", raw=True)
79+
assert expr.column == "count(*) as total"
80+
assert expr.alias is None
81+
82+
83+
class TestOrderByExpression:
84+
def test_defaults_to_ascending(self):
85+
expr = OrderByExpression("name")
86+
assert expr.column == "name"
87+
assert expr.direction == "ASC"
88+
89+
def test_infers_descending_from_suffix(self):
90+
expr = OrderByExpression("created_at desc")
91+
assert expr.column == "created_at"
92+
assert expr.direction == "DESC"
93+
94+
def test_infers_ascending_from_suffix(self):
95+
expr = OrderByExpression("name asc")
96+
assert expr.column == "name"
97+
assert expr.direction == "ASC"
98+
99+
def test_raw_disables_suffix_parsing(self):
100+
expr = OrderByExpression("name desc", raw=True)
101+
assert expr.column == "name desc"
102+
assert expr.direction == "ASC"
103+
104+
105+
class TestGroupByExpression:
106+
def test_strips_column(self):
107+
expr = GroupByExpression(" category ")
108+
assert expr.column == "category"
109+
assert expr.raw is False
110+
111+
112+
class TestAggregateExpression:
113+
def test_plain_column(self):
114+
expr = AggregateExpression(aggregate="SUM", column="amount")
115+
assert expr.aggregate == "SUM"
116+
assert expr.column == "amount"
117+
assert expr.alias is False
118+
119+
def test_splits_alias(self):
120+
expr = AggregateExpression(aggregate="SUM", column="amount as total")
121+
assert expr.column == "amount"
122+
assert expr.alias == "total"
123+
124+
125+
class TestRaw:
126+
def test_stores_expression(self):
127+
assert Raw("NOW()").expression == "NOW()"
128+
129+
130+
class TestUpdateQueryExpression:
131+
def test_defaults(self):
132+
expr = UpdateQueryExpression("name", "bob")
133+
assert expr.column == "name"
134+
assert expr.value == "bob"
135+
assert expr.update_type == "keyvalue"
136+
137+
138+
class TestSubExpressions:
139+
def test_sub_select_holds_builder(self):
140+
sentinel = object()
141+
assert SubSelectExpression(sentinel).builder is sentinel
142+
143+
def test_sub_group_default_alias(self):
144+
sentinel = object()
145+
expr = SubGroupExpression(sentinel)
146+
assert expr.builder is sentinel
147+
assert expr.alias == "group"
148+
149+
150+
class TestJoinClause:
151+
def test_parses_table_alias(self):
152+
clause = JoinClause("users as u")
153+
assert clause.table == "users"
154+
assert clause.alias == "u"
155+
assert clause.clause == "join"
156+
157+
def test_no_alias(self):
158+
clause = JoinClause("users", clause="left")
159+
assert clause.table == "users"
160+
assert clause.alias is None
161+
assert clause.clause == "left"
162+
163+
def test_on_builds_and_clause(self):
164+
clause = JoinClause("users").on("users.id", "=", "posts.user_id")
165+
[on] = clause.get_on_clauses()
166+
assert isinstance(on, OnClause)
167+
assert on.column1 == "users.id"
168+
assert on.column2 == "posts.user_id"
169+
assert on.operator == "and"
170+
171+
def test_or_on_builds_or_clause(self):
172+
clause = JoinClause("users").or_on("a", "=", "b")
173+
assert clause.get_on_clauses()[0].operator == "or"
174+
175+
def test_chaining_returns_self(self):
176+
clause = JoinClause("users")
177+
assert clause.on("a", "=", "b") is clause
178+
179+
def test_on_value_with_operator_and_value(self):
180+
clause = JoinClause("users").on_value("age", ">", 18)
181+
on = clause.get_on_clauses()[0]
182+
assert isinstance(on, OnValueClause)
183+
assert on.equality == ">"
184+
assert on.value == 18
185+
assert on.operator == "and"
186+
187+
def test_on_value_with_single_value_defaults_operator(self):
188+
clause = JoinClause("users").on_value("active", 1)
189+
on = clause.get_on_clauses()[0]
190+
assert on.equality == "="
191+
assert on.value == 1
192+
193+
def test_or_on_value_sets_or_operator(self):
194+
clause = JoinClause("users").or_on_value("age", ">", 18)
195+
assert clause.get_on_clauses()[0].operator == "or"
196+
197+
def test_on_null(self):
198+
clause = JoinClause("users").on_null("deleted_at")
199+
on = clause.get_on_clauses()[0]
200+
assert on.value_type == "NULL"
201+
assert on.value is None
202+
203+
def test_on_not_null(self):
204+
clause = JoinClause("users").on_not_null("verified_at")
205+
on = clause.get_on_clauses()[0]
206+
assert on.value_type == "NOT NULL"
207+
assert on.value is True
208+
209+
def test_or_on_null(self):
210+
clause = JoinClause("users").or_on_null("deleted_at")
211+
assert clause.get_on_clauses()[0].operator == "or"
212+
213+
def test_or_on_not_null(self):
214+
clause = JoinClause("users").or_on_not_null("verified_at")
215+
on = clause.get_on_clauses()[0]
216+
assert on.operator == "or"
217+
assert on.value_type == "NOT NULL"
218+
219+
def test_invalid_operator_raises(self):
220+
with pytest.raises(ValueError):
221+
JoinClause("users").on_value("age", "bogus", 18)
222+
223+
def test_where_is_deprecated_alias_of_on_value(self):
224+
clause = JoinClause("users")
225+
with warnings.catch_warnings():
226+
warnings.simplefilter("ignore", DeprecationWarning)
227+
result = clause.where("age", ">", 18)
228+
assert result is clause
229+
assert clause.get_on_clauses()[0].equality == ">"
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"""Unit tests for the schema Column builder (task #1214)."""
2+
3+
from fastapi_startkit.masoniteorm.schema.Column import Column
4+
5+
6+
def make_column(**kwargs):
7+
defaults = {"name": "email", "column_type": "string"}
8+
defaults.update(kwargs)
9+
return Column(**defaults)
10+
11+
12+
class TestColumnDefaults:
13+
def test_initial_state(self):
14+
col = make_column(length=255)
15+
assert col.name == "email"
16+
assert col.column_type == "string"
17+
assert col.length == 255
18+
assert col.values == []
19+
assert col.is_null is False
20+
assert col.primary is False
21+
assert col.comment is None
22+
23+
def test_values_defaults_to_empty_list(self):
24+
assert make_column(values=None).values == []
25+
assert make_column(values=["a", "b"]).values == ["a", "b"]
26+
27+
28+
class TestNullability:
29+
def test_nullable_sets_flag_and_returns_self(self):
30+
col = make_column()
31+
assert col.nullable() is col
32+
assert col.is_null is True
33+
34+
def test_not_nullable_clears_flag(self):
35+
col = make_column(nullable=True)
36+
assert col.not_nullable() is col
37+
assert col.is_null is False
38+
39+
40+
class TestSignedness:
41+
def test_signed(self):
42+
col = make_column()
43+
assert col.signed() is col
44+
assert col._signed == "signed"
45+
46+
def test_unsigned(self):
47+
col = make_column()
48+
assert col.unsigned() is col
49+
assert col._signed == "unsigned"
50+
51+
52+
class TestPrimaryAndComment:
53+
def test_set_as_primary(self):
54+
col = make_column()
55+
col.set_as_primary()
56+
assert col.primary is True
57+
58+
def test_add_comment_returns_self(self):
59+
col = make_column()
60+
assert col.add_comment("the user email") is col
61+
assert col.comment == "the user email"
62+
63+
64+
class TestRenameAndPositioning:
65+
def test_rename_records_old_column(self):
66+
col = make_column()
67+
assert col.rename("old_email") is col
68+
assert col.old_column == "old_email"
69+
70+
def test_after_sets_and_get_after_column_reads(self):
71+
col = make_column()
72+
assert col.after("created_at") is col
73+
assert col.get_after_column() == "created_at"
74+
75+
def test_get_after_column_defaults_to_none(self):
76+
assert make_column().get_after_column() is None
77+
78+
79+
class TestChangeAndCurrent:
80+
def test_change_marks_modify_action(self):
81+
col = make_column()
82+
assert col.change() is col
83+
assert col._action == "modify"
84+
85+
def test_use_current_sets_default_current(self):
86+
col = make_column()
87+
assert col.use_current() is col
88+
assert col.default == "current"
89+
90+
91+
class TestDefaultValue:
92+
def test_default_value_is_stored_as_attribute_from_constructor(self):
93+
col = make_column(default="anon", default_is_raw=True)
94+
assert col.default == "anon"
95+
assert col.default_is_raw is True
96+
97+
def test_default_method_is_reachable_on_the_class(self):
98+
# The constructor assigns ``self.default`` as an attribute, shadowing the
99+
# method on instances; the method is still invocable via the class.
100+
col = make_column()
101+
assert Column.default(col, "seed", raw=True) is col
102+
assert col.default == "seed"
103+
assert col.default_is_raw is True

0 commit comments

Comments
 (0)