Skip to content

Commit 38a23d1

Browse files
authored
Merge pull request #18 from unicef/feature/improvement
Updated flags and debug toolbar
2 parents 7ab34c6 + 2ab6f8b commit 38a23d1

6 files changed

Lines changed: 182 additions & 16 deletions

File tree

src/hope_live/config/fragments/debug_toolbar.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@
1414
def show_ddt(request: "HttpRequest") -> bool: # pragma: no-cover
1515
from flags.state import flag_enabled
1616

17-
return True
18-
if request.path in RegexList(("/api/.*", "/dal/.*", "/healthcheck/", "/autocomplete/")):
17+
if request.path in RegexList(("/api/.*", "/dal/.*", "/healthcheck/", "/autocomplete/.*")):
1918
return False
2019
return bool(flag_enabled("DEVELOP_DEBUG_TOOLBAR", request=request))
2120

src/hope_live/config/fragments/flags.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,18 @@
44

55
FLAGS_STATE_LOGGING = DEBUG
66
FLAGS: dict[str, list[Any]] = {
7-
"LOCAL_LOGIN": [],
8-
"DEVELOP_DEBUG_TOOLBAR": [],
7+
"LOCAL_LOGIN": [
8+
{"condition": "hostname", "value": "localhost,127.0.0.1"},
9+
]
10+
if DEBUG
11+
else [],
12+
"DEVELOP_DEBUG_TOOLBAR": [
13+
{"condition": "hostname", "value": "localhost,127.0.0.1"},
14+
]
15+
if DEBUG
16+
else [],
917
"DJANGO_ADMIN": [],
1018
}
19+
20+
# Register flag conditions
21+
from hope_live.utils.flags import * # noqa: E402 F403

src/hope_live/theme/static_src/package-lock.json

Lines changed: 12 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/hope_live/utils/flags.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import contextlib
2+
from typing import TYPE_CHECKING, Any
3+
4+
from adminfilters.utils import parse_bool
5+
from django.conf import settings
6+
from django.core.exceptions import ValidationError
7+
from flags import state as flag_state
8+
from flags.conditions import conditions
9+
10+
if TYPE_CHECKING:
11+
from django.http import HttpRequest
12+
13+
14+
@contextlib.contextmanager
15+
def enable_flag(name: str) -> Any:
16+
flag_state.enable_flag(name)
17+
yield
18+
flag_state.disable_flag(name)
19+
20+
21+
def validate_bool(value: str) -> None:
22+
if value.lower() not in ["true", "1", "yes", "t", "y", "false", "0", "no", "f", "n"]:
23+
raise ValidationError("Enter a valid bool")
24+
25+
26+
@conditions.register("superuser", validator=validate_bool)
27+
def superuser(value: str, request: "HttpRequest | None" = None, **kwargs: Any) -> bool:
28+
if request is None:
29+
return False
30+
return request.user.is_superuser == parse_bool(value)
31+
32+
33+
@conditions.register("debug", validator=validate_bool)
34+
def debug(value: str, **kwargs: Any) -> bool:
35+
return parse_bool(value) == settings.DEBUG
36+
37+
38+
@conditions.register("hostname")
39+
def hostname(value: str, request: "HttpRequest | None" = None, **kwargs: Any) -> bool:
40+
if request is None:
41+
return False
42+
host = request.get_host().split(":")[0]
43+
return host in value.split(",")

tests/conftest.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import contextlib
12
import os
23
import sys
34
from pathlib import Path
@@ -66,3 +67,23 @@ def user_role_factory():
6667
from factories import UserRoleFactory
6768

6869
return UserRoleFactory
70+
71+
72+
@pytest.fixture
73+
def admin_user(db):
74+
from hope_live.models import User
75+
76+
return User.objects.create_superuser(username="admin", email="[email protected]", password="password")
77+
78+
79+
@pytest.fixture(autouse=True)
80+
def cleanup_flags(db):
81+
from django.db import DatabaseError, transaction
82+
from flags.models import FlagState
83+
84+
yield
85+
86+
# Try to cleanup, but if it fails (e.g., due to broken transaction), just skip it
87+
# The test database will be destroyed after the test run anyway
88+
with contextlib.suppress(DatabaseError, transaction.TransactionManagementError):
89+
FlagState.objects.all().delete()

tests/test_utils/test_flags.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
from typing import TYPE_CHECKING
2+
from unittest import mock
3+
4+
import pytest
5+
from django.core.exceptions import ValidationError
6+
7+
from hope_live.utils.flags import debug, hostname, superuser, validate_bool
8+
9+
if TYPE_CHECKING:
10+
from django.http import HttpRequest
11+
12+
13+
@pytest.mark.parametrize("value", ["true", "1", "yes", "t", "y", "false", "0", "no", "f", "n"])
14+
def test_validate_bool(value):
15+
validate_bool(value) # No exception should be raised
16+
17+
18+
@pytest.mark.parametrize("value", ["a", "9"])
19+
def test_validate_bool_fail(value):
20+
with pytest.raises(ValidationError):
21+
assert validate_bool(value)
22+
23+
24+
@pytest.mark.parametrize("value", ["t", "tRue", "yes", "1"])
25+
def test_superuser(rf, value, admin_user):
26+
request = rf.get("/")
27+
request.user = admin_user
28+
assert superuser(value, request)
29+
30+
31+
@pytest.mark.parametrize("value", ["t", "tRue", "yes", "1"])
32+
def test_debug(settings, value):
33+
settings.DEBUG = True
34+
assert debug(value)
35+
36+
37+
@pytest.mark.parametrize("value", ["myserver.com", "myserver.com:888", "myserver.com:443"])
38+
def test_hostname(value, rf):
39+
request: HttpRequest = rf.get("/")
40+
with mock.patch.object(request, "get_host", return_value=value):
41+
assert hostname("myserver.com", request)
42+
43+
44+
def test_hostname_mismatch(rf):
45+
request: HttpRequest = rf.get("/")
46+
with mock.patch.object(request, "get_host", return_value="production.com"):
47+
assert not hostname("localhost", request)
48+
49+
50+
@pytest.mark.parametrize("path", ["/api/data/", "/dal/autocomplete/", "/healthcheck/", "/autocomplete/user/"])
51+
def test_show_ddt_excluded_paths(rf, settings, path):
52+
from hope_live.config.fragments.debug_toolbar import show_ddt
53+
54+
settings.DEBUG = True
55+
request = rf.get(path)
56+
with mock.patch.object(request, "get_host", return_value="localhost:8000"):
57+
assert show_ddt(request) is False
58+
59+
60+
def test_show_ddt_allowed_path(rf, settings):
61+
from flags.models import FlagState
62+
63+
from hope_live.config.fragments.debug_toolbar import show_ddt
64+
65+
settings.DEBUG = True
66+
FlagState.objects.create(
67+
name="DEVELOP_DEBUG_TOOLBAR",
68+
condition="hostname",
69+
value="localhost,127.0.0.1",
70+
)
71+
72+
request = rf.get("/dashboard/")
73+
with mock.patch.object(request, "get_host", return_value="localhost:8000"):
74+
assert request.path == "/dashboard/"
75+
assert show_ddt(request) is True
76+
77+
78+
def test_show_ddt_production_hostname(rf, settings):
79+
from flags.models import FlagState
80+
81+
from hope_live.config.fragments.debug_toolbar import show_ddt
82+
83+
settings.DEBUG = True
84+
FlagState.objects.create(
85+
name="DEVELOP_DEBUG_TOOLBAR",
86+
condition="hostname",
87+
value="localhost,127.0.0.1",
88+
)
89+
90+
request = rf.get("/dashboard/")
91+
with mock.patch.object(request, "get_host", return_value="dashboard-hope-dev.unitst.org:443"):
92+
assert show_ddt(request) is False

0 commit comments

Comments
 (0)