arthexis 0.1.9__py3-none-any.whl → 0.1.26__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of arthexis might be problematic. Click here for more details.
- arthexis-0.1.26.dist-info/METADATA +272 -0
- arthexis-0.1.26.dist-info/RECORD +111 -0
- {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/licenses/LICENSE +674 -674
- config/__init__.py +5 -5
- config/active_app.py +15 -15
- config/asgi.py +29 -29
- config/auth_app.py +7 -7
- config/celery.py +32 -25
- config/context_processors.py +67 -68
- config/horologia_app.py +7 -7
- config/loadenv.py +11 -11
- config/logging.py +59 -48
- config/middleware.py +71 -25
- config/offline.py +49 -49
- config/settings.py +676 -492
- config/settings_helpers.py +109 -0
- config/urls.py +228 -159
- config/wsgi.py +17 -17
- core/admin.py +4052 -2066
- core/admin_history.py +50 -50
- core/admindocs.py +192 -151
- core/apps.py +350 -223
- core/auto_upgrade.py +72 -0
- core/backends.py +311 -124
- core/changelog.py +403 -0
- core/entity.py +149 -133
- core/environment.py +60 -43
- core/fields.py +168 -75
- core/form_fields.py +75 -0
- core/github_helper.py +188 -25
- core/github_issues.py +183 -172
- core/github_repos.py +72 -0
- core/lcd_screen.py +78 -78
- core/liveupdate.py +25 -25
- core/log_paths.py +114 -100
- core/mailer.py +89 -83
- core/middleware.py +91 -91
- core/models.py +5041 -2195
- core/notifications.py +105 -105
- core/public_wifi.py +267 -227
- core/reference_utils.py +107 -0
- core/release.py +940 -346
- core/rfid_import_export.py +113 -0
- core/sigil_builder.py +149 -131
- core/sigil_context.py +20 -20
- core/sigil_resolver.py +250 -284
- core/system.py +1425 -230
- core/tasks.py +538 -199
- core/temp_passwords.py +181 -0
- core/test_system_info.py +202 -43
- core/tests.py +2673 -1069
- core/tests_liveupdate.py +17 -17
- core/urls.py +11 -11
- core/user_data.py +681 -495
- core/views.py +2484 -789
- core/widgets.py +213 -51
- nodes/admin.py +2236 -445
- nodes/apps.py +98 -70
- nodes/backends.py +160 -53
- nodes/dns.py +203 -0
- nodes/feature_checks.py +133 -0
- nodes/lcd.py +165 -165
- nodes/models.py +2375 -870
- nodes/reports.py +411 -0
- nodes/rfid_sync.py +210 -0
- nodes/signals.py +18 -0
- nodes/tasks.py +141 -46
- nodes/tests.py +5045 -1489
- nodes/urls.py +29 -13
- nodes/utils.py +172 -73
- nodes/views.py +1768 -304
- ocpp/admin.py +1775 -481
- ocpp/apps.py +25 -25
- ocpp/consumers.py +1843 -630
- ocpp/evcs.py +844 -928
- ocpp/evcs_discovery.py +158 -0
- ocpp/models.py +1417 -640
- ocpp/network.py +398 -0
- ocpp/reference_utils.py +42 -0
- ocpp/routing.py +11 -9
- ocpp/simulator.py +745 -368
- ocpp/status_display.py +26 -0
- ocpp/store.py +603 -403
- ocpp/tasks.py +479 -31
- ocpp/test_export_import.py +131 -130
- ocpp/test_rfid.py +1072 -540
- ocpp/tests.py +5494 -2296
- ocpp/transactions_io.py +197 -165
- ocpp/urls.py +50 -50
- ocpp/views.py +2024 -912
- pages/admin.py +1123 -396
- pages/apps.py +45 -10
- pages/checks.py +40 -40
- pages/context_processors.py +151 -85
- pages/defaults.py +13 -0
- pages/forms.py +221 -0
- pages/middleware.py +213 -153
- pages/models.py +720 -252
- pages/module_defaults.py +156 -0
- pages/site_config.py +137 -0
- pages/tasks.py +74 -0
- pages/tests.py +4009 -1389
- pages/urls.py +38 -20
- pages/utils.py +93 -12
- pages/views.py +1736 -762
- arthexis-0.1.9.dist-info/METADATA +0 -168
- arthexis-0.1.9.dist-info/RECORD +0 -92
- core/workgroup_urls.py +0 -17
- core/workgroup_views.py +0 -94
- nodes/actions.py +0 -70
- {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/WHEEL +0 -0
- {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/top_level.txt +0 -0
core/temp_passwords.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Utilities for temporary password lock files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
import secrets
|
|
9
|
+
import string
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from datetime import datetime, timedelta
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
from django.conf import settings
|
|
16
|
+
from django.contrib.auth.hashers import check_password, make_password
|
|
17
|
+
from django.utils import timezone
|
|
18
|
+
from django.utils.dateparse import parse_datetime
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
DEFAULT_PASSWORD_LENGTH = 16
|
|
22
|
+
DEFAULT_EXPIRATION = timedelta(hours=1)
|
|
23
|
+
_SAFE_COMPONENT_RE = re.compile(r"[^A-Za-z0-9_.-]+")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _base_lock_dir() -> Path:
|
|
27
|
+
"""Return the root directory used for temporary password lock files."""
|
|
28
|
+
|
|
29
|
+
configured = getattr(settings, "TEMP_PASSWORD_LOCK_DIR", None)
|
|
30
|
+
if configured:
|
|
31
|
+
path = Path(configured)
|
|
32
|
+
else:
|
|
33
|
+
path = Path(settings.BASE_DIR) / "locks" / "temp-passwords"
|
|
34
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
return path
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _safe_component(value: str) -> str:
|
|
39
|
+
"""Return a filesystem safe component derived from ``value``."""
|
|
40
|
+
|
|
41
|
+
if not value:
|
|
42
|
+
return ""
|
|
43
|
+
safe = _SAFE_COMPONENT_RE.sub("_", value)
|
|
44
|
+
safe = safe.strip("._")
|
|
45
|
+
return safe[:64]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _lockfile_name(username: str) -> str:
|
|
49
|
+
"""Return the filename used for the provided ``username``."""
|
|
50
|
+
|
|
51
|
+
digest = hashlib.sha256(username.encode("utf-8")).hexdigest()[:12]
|
|
52
|
+
safe = _safe_component(username)
|
|
53
|
+
if safe:
|
|
54
|
+
return f"{safe}-{digest}.json"
|
|
55
|
+
return f"user-{digest}.json"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _lockfile_path(username: str) -> Path:
|
|
59
|
+
"""Return the lockfile path for ``username``."""
|
|
60
|
+
|
|
61
|
+
return _base_lock_dir() / _lockfile_name(username)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _parse_timestamp(value: str | None) -> Optional[datetime]:
|
|
65
|
+
"""Return a timezone aware datetime parsed from ``value``."""
|
|
66
|
+
|
|
67
|
+
if not value:
|
|
68
|
+
return None
|
|
69
|
+
parsed = parse_datetime(value)
|
|
70
|
+
if parsed is None:
|
|
71
|
+
return None
|
|
72
|
+
if timezone.is_naive(parsed):
|
|
73
|
+
parsed = timezone.make_aware(parsed)
|
|
74
|
+
return parsed
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True)
|
|
78
|
+
class TempPasswordEntry:
|
|
79
|
+
"""Details for a temporary password stored on disk."""
|
|
80
|
+
|
|
81
|
+
username: str
|
|
82
|
+
password_hash: str
|
|
83
|
+
expires_at: datetime
|
|
84
|
+
created_at: datetime
|
|
85
|
+
path: Path
|
|
86
|
+
allow_change: bool = False
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def is_expired(self) -> bool:
|
|
90
|
+
return timezone.now() >= self.expires_at
|
|
91
|
+
|
|
92
|
+
def check_password(self, raw_password: str) -> bool:
|
|
93
|
+
"""Return ``True`` if ``raw_password`` matches this entry."""
|
|
94
|
+
|
|
95
|
+
return check_password(raw_password, self.password_hash)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def generate_password(length: int = DEFAULT_PASSWORD_LENGTH) -> str:
|
|
99
|
+
"""Return a random password composed of letters and digits."""
|
|
100
|
+
|
|
101
|
+
if length <= 0:
|
|
102
|
+
raise ValueError("length must be a positive integer")
|
|
103
|
+
alphabet = string.ascii_letters + string.digits
|
|
104
|
+
return "".join(secrets.choice(alphabet) for _ in range(length))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def store_temp_password(
|
|
108
|
+
username: str,
|
|
109
|
+
raw_password: str,
|
|
110
|
+
expires_at: Optional[datetime] = None,
|
|
111
|
+
*,
|
|
112
|
+
allow_change: bool = False,
|
|
113
|
+
) -> TempPasswordEntry:
|
|
114
|
+
"""Persist a temporary password for ``username`` and return the entry."""
|
|
115
|
+
|
|
116
|
+
if expires_at is None:
|
|
117
|
+
expires_at = timezone.now() + DEFAULT_EXPIRATION
|
|
118
|
+
if timezone.is_naive(expires_at):
|
|
119
|
+
expires_at = timezone.make_aware(expires_at)
|
|
120
|
+
created_at = timezone.now()
|
|
121
|
+
path = _lockfile_path(username)
|
|
122
|
+
data = {
|
|
123
|
+
"username": username,
|
|
124
|
+
"password_hash": make_password(raw_password),
|
|
125
|
+
"expires_at": expires_at.isoformat(),
|
|
126
|
+
"created_at": created_at.isoformat(),
|
|
127
|
+
"allow_change": allow_change,
|
|
128
|
+
}
|
|
129
|
+
path.write_text(json.dumps(data, indent=2, sort_keys=True))
|
|
130
|
+
return TempPasswordEntry(
|
|
131
|
+
username=username,
|
|
132
|
+
password_hash=data["password_hash"],
|
|
133
|
+
expires_at=expires_at,
|
|
134
|
+
created_at=created_at,
|
|
135
|
+
path=path,
|
|
136
|
+
allow_change=allow_change,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def load_temp_password(username: str) -> Optional[TempPasswordEntry]:
|
|
141
|
+
"""Return the stored temporary password for ``username``, if any."""
|
|
142
|
+
|
|
143
|
+
path = _lockfile_path(username)
|
|
144
|
+
if not path.exists():
|
|
145
|
+
return None
|
|
146
|
+
try:
|
|
147
|
+
data = json.loads(path.read_text())
|
|
148
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
149
|
+
path.unlink(missing_ok=True)
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
expires_at = _parse_timestamp(data.get("expires_at"))
|
|
153
|
+
created_at = _parse_timestamp(data.get("created_at")) or timezone.now()
|
|
154
|
+
password_hash = data.get("password_hash")
|
|
155
|
+
if not expires_at or not password_hash:
|
|
156
|
+
path.unlink(missing_ok=True)
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
username = data.get("username") or username
|
|
160
|
+
allow_change_value = data.get("allow_change", False)
|
|
161
|
+
if isinstance(allow_change_value, str):
|
|
162
|
+
allow_change = allow_change_value.lower() in {"1", "true", "yes", "on"}
|
|
163
|
+
else:
|
|
164
|
+
allow_change = bool(allow_change_value)
|
|
165
|
+
|
|
166
|
+
return TempPasswordEntry(
|
|
167
|
+
username=username,
|
|
168
|
+
password_hash=password_hash,
|
|
169
|
+
expires_at=expires_at,
|
|
170
|
+
created_at=created_at,
|
|
171
|
+
path=path,
|
|
172
|
+
allow_change=allow_change,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def discard_temp_password(username: str) -> None:
|
|
177
|
+
"""Remove any stored temporary password for ``username``."""
|
|
178
|
+
|
|
179
|
+
path = _lockfile_path(username)
|
|
180
|
+
path.unlink(missing_ok=True)
|
|
181
|
+
|
core/test_system_info.py
CHANGED
|
@@ -1,43 +1,202 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from datetime import timedelta
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from subprocess import CompletedProcess
|
|
6
|
+
from tempfile import TemporaryDirectory
|
|
7
|
+
from unittest.mock import Mock, patch
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
|
11
|
+
|
|
12
|
+
import django
|
|
13
|
+
|
|
14
|
+
django.setup()
|
|
15
|
+
|
|
16
|
+
from django.conf import settings
|
|
17
|
+
from django.test import SimpleTestCase, override_settings
|
|
18
|
+
from nodes.models import Node, NodeFeature, NodeRole
|
|
19
|
+
from core.system import (
|
|
20
|
+
_gather_info,
|
|
21
|
+
_load_auto_upgrade_log_entries,
|
|
22
|
+
_read_auto_upgrade_mode,
|
|
23
|
+
get_system_sigil_values,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SystemInfoRoleTests(SimpleTestCase):
|
|
28
|
+
@override_settings(NODE_ROLE="Terminal")
|
|
29
|
+
def test_defaults_to_terminal(self):
|
|
30
|
+
info = _gather_info()
|
|
31
|
+
self.assertEqual(info["role"], "Terminal")
|
|
32
|
+
|
|
33
|
+
@override_settings(NODE_ROLE="Satellite")
|
|
34
|
+
def test_uses_settings_role(self):
|
|
35
|
+
info = _gather_info()
|
|
36
|
+
self.assertEqual(info["role"], "Satellite")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class SystemInfoScreenModeTests(SimpleTestCase):
|
|
40
|
+
def test_without_lockfile(self):
|
|
41
|
+
info = _gather_info()
|
|
42
|
+
self.assertEqual(info["screen_mode"], "")
|
|
43
|
+
|
|
44
|
+
def test_with_lockfile(self):
|
|
45
|
+
lock_dir = Path(settings.BASE_DIR) / "locks"
|
|
46
|
+
lock_dir.mkdir(exist_ok=True)
|
|
47
|
+
lock_file = lock_dir / "screen_mode.lck"
|
|
48
|
+
lock_file.write_text("tft")
|
|
49
|
+
try:
|
|
50
|
+
info = _gather_info()
|
|
51
|
+
self.assertEqual(info["screen_mode"], "tft")
|
|
52
|
+
finally:
|
|
53
|
+
lock_file.unlink()
|
|
54
|
+
if not any(lock_dir.iterdir()):
|
|
55
|
+
lock_dir.rmdir()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class SystemInfoModeTests(SimpleTestCase):
|
|
59
|
+
def test_public_mode_case_insensitive(self):
|
|
60
|
+
lock_dir = Path(settings.BASE_DIR) / "locks"
|
|
61
|
+
lock_dir.mkdir(exist_ok=True)
|
|
62
|
+
lock_file = lock_dir / "nginx_mode.lck"
|
|
63
|
+
lock_file.write_text("PUBLIC", encoding="utf-8")
|
|
64
|
+
try:
|
|
65
|
+
info = _gather_info()
|
|
66
|
+
self.assertEqual(info["mode"], "public")
|
|
67
|
+
self.assertEqual(info["port"], 8000)
|
|
68
|
+
finally:
|
|
69
|
+
lock_file.unlink()
|
|
70
|
+
if not any(lock_dir.iterdir()):
|
|
71
|
+
lock_dir.rmdir()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class SystemInfoRevisionTests(SimpleTestCase):
|
|
75
|
+
@patch("core.system.revision.get_revision", return_value="abcdef1234567890")
|
|
76
|
+
def test_includes_full_revision(self, mock_revision):
|
|
77
|
+
info = _gather_info()
|
|
78
|
+
self.assertEqual(info["revision"], "abcdef1234567890")
|
|
79
|
+
mock_revision.assert_called_once()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class SystemInfoDatabaseTests(SimpleTestCase):
|
|
83
|
+
def test_collects_database_definitions(self):
|
|
84
|
+
info = _gather_info()
|
|
85
|
+
self.assertIn("databases", info)
|
|
86
|
+
aliases = {entry["alias"] for entry in info["databases"]}
|
|
87
|
+
self.assertIn("default", aliases)
|
|
88
|
+
|
|
89
|
+
@override_settings(
|
|
90
|
+
DATABASES={
|
|
91
|
+
"default": {
|
|
92
|
+
"ENGINE": "django.db.backends.sqlite3",
|
|
93
|
+
"NAME": Path("/tmp/db.sqlite3"),
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
def test_serializes_path_database_names(self):
|
|
98
|
+
info = _gather_info()
|
|
99
|
+
databases = info["databases"]
|
|
100
|
+
self.assertEqual(databases[0]["name"], "/tmp/db.sqlite3")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class AutoUpgradeModeTests(SimpleTestCase):
|
|
104
|
+
def test_lock_file_read_error_marks_enabled(self):
|
|
105
|
+
mock_path = Mock()
|
|
106
|
+
mock_path.exists.return_value = True
|
|
107
|
+
mock_path.read_text.side_effect = OSError
|
|
108
|
+
|
|
109
|
+
with patch("core.system._auto_upgrade_mode_file", return_value=mock_path):
|
|
110
|
+
info = _read_auto_upgrade_mode(Path("/tmp"))
|
|
111
|
+
|
|
112
|
+
self.assertTrue(info["lock_exists"])
|
|
113
|
+
self.assertTrue(info["enabled"])
|
|
114
|
+
self.assertTrue(info["read_error"])
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class AutoUpgradeLogParsingTests(SimpleTestCase):
|
|
118
|
+
def test_parses_zulu_timestamp_entries(self):
|
|
119
|
+
with TemporaryDirectory() as tmpdir:
|
|
120
|
+
base_dir = Path(tmpdir)
|
|
121
|
+
log_dir = base_dir / "logs"
|
|
122
|
+
log_dir.mkdir()
|
|
123
|
+
log_path = log_dir / "auto-upgrade.log"
|
|
124
|
+
log_path.write_text("2024-01-01T12:34:56Z Started\n", encoding="utf-8")
|
|
125
|
+
|
|
126
|
+
with patch("core.system._format_timestamp", return_value="formatted") as mock_format:
|
|
127
|
+
result = _load_auto_upgrade_log_entries(base_dir)
|
|
128
|
+
|
|
129
|
+
entries = result["entries"]
|
|
130
|
+
self.assertEqual(len(entries), 1)
|
|
131
|
+
entry = entries[0]
|
|
132
|
+
self.assertEqual(entry["message"], "Started")
|
|
133
|
+
self.assertEqual(entry["timestamp"], "formatted")
|
|
134
|
+
|
|
135
|
+
mock_format.assert_called_once()
|
|
136
|
+
parsed_dt = mock_format.call_args[0][0]
|
|
137
|
+
self.assertEqual(parsed_dt.year, 2024)
|
|
138
|
+
self.assertEqual(parsed_dt.month, 1)
|
|
139
|
+
self.assertEqual(parsed_dt.day, 1)
|
|
140
|
+
self.assertEqual(parsed_dt.utcoffset(), timedelta(0))
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class SystemInfoRunserverDetectionTests(SimpleTestCase):
|
|
144
|
+
@patch("core.system.subprocess.run")
|
|
145
|
+
def test_detects_runserver_process_port(self, mock_run):
|
|
146
|
+
mock_run.return_value = CompletedProcess(
|
|
147
|
+
args=["pgrep"],
|
|
148
|
+
returncode=0,
|
|
149
|
+
stdout="123 python manage.py runserver 0.0.0.0:8000 --noreload\n",
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
info = _gather_info()
|
|
153
|
+
|
|
154
|
+
self.assertTrue(info["running"])
|
|
155
|
+
self.assertEqual(info["port"], 8000)
|
|
156
|
+
|
|
157
|
+
@patch("core.system._probe_ports", return_value=(True, 8000))
|
|
158
|
+
@patch("core.system.subprocess.run", side_effect=FileNotFoundError)
|
|
159
|
+
def test_falls_back_to_port_probe_when_pgrep_missing(self, mock_run, mock_probe):
|
|
160
|
+
info = _gather_info()
|
|
161
|
+
|
|
162
|
+
self.assertTrue(info["running"])
|
|
163
|
+
self.assertEqual(info["port"], 8000)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class SystemSigilValueTests(SimpleTestCase):
|
|
167
|
+
def test_exports_values_for_sigil_resolution(self):
|
|
168
|
+
sample_info = {
|
|
169
|
+
"installed": True,
|
|
170
|
+
"revision": "abcdef",
|
|
171
|
+
"service": "gunicorn",
|
|
172
|
+
"mode": "internal",
|
|
173
|
+
"port": 8888,
|
|
174
|
+
"role": "Terminal",
|
|
175
|
+
"screen_mode": "",
|
|
176
|
+
"features": [
|
|
177
|
+
{"display": "Feature", "expected": True, "actual": False, "slug": "feature"}
|
|
178
|
+
],
|
|
179
|
+
"running": True,
|
|
180
|
+
"service_status": "active",
|
|
181
|
+
"hostname": "example.local",
|
|
182
|
+
"ip_addresses": ["127.0.0.1"],
|
|
183
|
+
"databases": [
|
|
184
|
+
{
|
|
185
|
+
"alias": "default",
|
|
186
|
+
"engine": "django.db.backends.sqlite3",
|
|
187
|
+
"name": "db.sqlite3",
|
|
188
|
+
}
|
|
189
|
+
],
|
|
190
|
+
}
|
|
191
|
+
with patch("core.system._gather_info", return_value=sample_info):
|
|
192
|
+
values = get_system_sigil_values()
|
|
193
|
+
|
|
194
|
+
self.assertEqual(values["REVISION"], "abcdef")
|
|
195
|
+
self.assertEqual(values["RUNNING"], "True")
|
|
196
|
+
self.assertEqual(values["NGINX_MODE"], "internal (8888)")
|
|
197
|
+
self.assertEqual(values["IP_ADDRESSES"], "127.0.0.1")
|
|
198
|
+
features = json.loads(values["FEATURES"])
|
|
199
|
+
self.assertEqual(features[0]["display"], "Feature")
|
|
200
|
+
databases = json.loads(values["DATABASES"])
|
|
201
|
+
self.assertEqual(databases[0]["alias"], "default")
|
|
202
|
+
|