arthexis 0.1.15__py3-none-any.whl → 0.1.17__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.

core/tests.py CHANGED
@@ -10,6 +10,7 @@ from django.urls import reverse
10
10
  from django.http import HttpRequest
11
11
  import csv
12
12
  import json
13
+ import importlib.util
13
14
  from decimal import Decimal
14
15
  from unittest import mock
15
16
  from unittest.mock import patch
@@ -19,9 +20,11 @@ import types
19
20
  from glob import glob
20
21
  from datetime import datetime, timedelta, timezone as datetime_timezone
21
22
  import tempfile
23
+ from io import StringIO
22
24
  from urllib.parse import quote
23
25
 
24
26
  from django.utils import timezone
27
+ from django.conf import settings
25
28
  from django.contrib.auth.models import Permission
26
29
  from django.contrib.messages import get_messages
27
30
  from tablib import Dataset
@@ -51,9 +54,11 @@ from core.admin import (
51
54
  USER_PROFILE_INLINES,
52
55
  )
53
56
  from ocpp.models import Transaction, Charger
57
+ from nodes.models import ContentSample
54
58
 
55
59
  from django.core.exceptions import ValidationError
56
60
  from django.core.management import call_command
61
+ from django.core.management.base import CommandError
57
62
  from django.db import IntegrityError
58
63
  from .backends import LocalhostAdminBackend
59
64
  from core.views import (
@@ -365,6 +370,45 @@ class RFIDLoginTests(TestCase):
365
370
  self.assertEqual(response.status_code, 401)
366
371
  mock_run.assert_called_once()
367
372
 
373
+ @patch("core.backends.subprocess.Popen")
374
+ def test_rfid_login_post_command_runs_after_success(self, mock_popen):
375
+ tag = self.account.rfids.first()
376
+ tag.post_auth_command = "echo welcome"
377
+ tag.save(update_fields=["post_auth_command"])
378
+
379
+ response = self.client.post(
380
+ reverse("rfid-login"),
381
+ data={"rfid": "CARD123"},
382
+ content_type="application/json",
383
+ )
384
+
385
+ self.assertEqual(response.status_code, 200)
386
+ mock_popen.assert_called_once()
387
+ args, kwargs = mock_popen.call_args
388
+ self.assertEqual(args[0], "echo welcome")
389
+ self.assertTrue(kwargs.get("shell"))
390
+ env = kwargs.get("env", {})
391
+ self.assertEqual(env.get("RFID_VALUE"), "CARD123")
392
+ self.assertEqual(env.get("RFID_LABEL_ID"), str(tag.pk))
393
+ self.assertIs(kwargs.get("stdout"), subprocess.DEVNULL)
394
+ self.assertIs(kwargs.get("stderr"), subprocess.DEVNULL)
395
+
396
+ @patch("core.backends.subprocess.Popen")
397
+ def test_rfid_login_post_command_skipped_on_failure(self, mock_popen):
398
+ tag = self.account.rfids.first()
399
+ tag.post_auth_command = "echo welcome"
400
+ tag.allowed = False
401
+ tag.save(update_fields=["post_auth_command", "allowed"])
402
+
403
+ response = self.client.post(
404
+ reverse("rfid-login"),
405
+ data={"rfid": "CARD123"},
406
+ content_type="application/json",
407
+ )
408
+
409
+ self.assertEqual(response.status_code, 401)
410
+ mock_popen.assert_not_called()
411
+
368
412
 
369
413
  class RFIDBatchApiTests(TestCase):
370
414
  def setUp(self):
@@ -387,6 +431,8 @@ class RFIDBatchApiTests(TestCase):
387
431
  "rfid": "CARD999",
388
432
  "custom_label": "Main Tag",
389
433
  "energy_accounts": [self.account.id],
434
+ "external_command": "",
435
+ "post_auth_command": "",
390
436
  "allowed": True,
391
437
  "color": "B",
392
438
  "released": False,
@@ -406,6 +452,8 @@ class RFIDBatchApiTests(TestCase):
406
452
  "rfid": "CARD111",
407
453
  "custom_label": "",
408
454
  "energy_accounts": [],
455
+ "external_command": "",
456
+ "post_auth_command": "",
409
457
  "allowed": True,
410
458
  "color": "W",
411
459
  "released": False,
@@ -426,6 +474,8 @@ class RFIDBatchApiTests(TestCase):
426
474
  "rfid": "CARD112",
427
475
  "custom_label": "",
428
476
  "energy_accounts": [],
477
+ "external_command": "",
478
+ "post_auth_command": "",
429
479
  "allowed": True,
430
480
  "color": "B",
431
481
  "released": True,
@@ -441,6 +491,8 @@ class RFIDBatchApiTests(TestCase):
441
491
  "rfid": "A1B2C3D4",
442
492
  "custom_label": "Imported Tag",
443
493
  "energy_accounts": [self.account.id],
494
+ "external_command": "echo pre",
495
+ "post_auth_command": "echo post",
444
496
  "allowed": True,
445
497
  "color": "W",
446
498
  "released": True,
@@ -459,6 +511,8 @@ class RFIDBatchApiTests(TestCase):
459
511
  rfid="A1B2C3D4",
460
512
  custom_label="Imported Tag",
461
513
  energy_accounts=self.account,
514
+ external_command="echo pre",
515
+ post_auth_command="echo post",
462
516
  color=RFID.WHITE,
463
517
  released=True,
464
518
  ).exists()
@@ -931,6 +985,40 @@ class RFIDImportExportCommandTests(TestCase):
931
985
  self.assertTrue(tag.energy_accounts.filter(pk=account.pk).exists())
932
986
 
933
987
 
988
+ class CheckRFIDCommandTests(TestCase):
989
+ def test_successful_validation_outputs_json(self):
990
+ out = StringIO()
991
+
992
+ call_command("check_rfid", "abcd1234", stdout=out)
993
+
994
+ payload = json.loads(out.getvalue())
995
+ self.assertEqual(payload["rfid"], "ABCD1234")
996
+ self.assertTrue(payload["created"])
997
+ self.assertTrue(RFID.objects.filter(rfid="ABCD1234").exists())
998
+
999
+ def test_invalid_value_raises_error(self):
1000
+ with self.assertRaises(CommandError):
1001
+ call_command("check_rfid", "invalid!")
1002
+
1003
+ def test_kind_option_updates_existing_tag(self):
1004
+ tag = RFID.objects.create(rfid="EXISTING", allowed=False, kind=RFID.CLASSIC)
1005
+ out = StringIO()
1006
+
1007
+ call_command(
1008
+ "check_rfid",
1009
+ "existing",
1010
+ "--kind",
1011
+ RFID.NTAG215,
1012
+ stdout=out,
1013
+ )
1014
+
1015
+ payload = json.loads(out.getvalue())
1016
+ tag.refresh_from_db()
1017
+ self.assertFalse(payload["created"])
1018
+ self.assertEqual(payload["kind"], RFID.NTAG215)
1019
+ self.assertEqual(tag.kind, RFID.NTAG215)
1020
+
1021
+
934
1022
  class RFIDKeyVerificationFlagTests(TestCase):
935
1023
  def test_flags_reset_on_key_change(self):
936
1024
  tag = RFID.objects.create(
@@ -985,6 +1073,44 @@ class ReleaseProcessTests(TestCase):
985
1073
  )
986
1074
  sync_main.assert_called_once_with(Path("rel.log"))
987
1075
 
1076
+ def test_step_check_todos_logs_instruction_when_pending(self):
1077
+ log_path = Path("rel.log")
1078
+ log_path.unlink(missing_ok=True)
1079
+ Todo.objects.create(request="Review checklist")
1080
+ ctx: dict[str, object] = {}
1081
+
1082
+ try:
1083
+ with self.assertRaises(core_views.PendingTodos):
1084
+ core_views._step_check_todos(self.release, ctx, log_path)
1085
+
1086
+ contents = log_path.read_text(encoding="utf-8")
1087
+ message = "Release checklist requires acknowledgment before continuing."
1088
+ self.assertIn(message, contents)
1089
+ self.assertIn("Review outstanding TODO items", contents)
1090
+
1091
+ with self.assertRaises(core_views.PendingTodos):
1092
+ core_views._step_check_todos(self.release, ctx, log_path)
1093
+
1094
+ contents = log_path.read_text(encoding="utf-8")
1095
+ self.assertEqual(contents.count(message), 1)
1096
+ finally:
1097
+ log_path.unlink(missing_ok=True)
1098
+
1099
+ def test_step_check_todos_auto_ack_when_no_pending(self):
1100
+ log_path = Path("rel.log")
1101
+ log_path.unlink(missing_ok=True)
1102
+ ctx: dict[str, object] = {}
1103
+
1104
+ try:
1105
+ with mock.patch("core.views._refresh_changelog_once"):
1106
+ core_views._step_check_todos(self.release, ctx, log_path)
1107
+ finally:
1108
+ log_path.unlink(missing_ok=True)
1109
+
1110
+ self.assertTrue(ctx.get("todos_ack"))
1111
+ self.assertNotIn("todos_required", ctx)
1112
+ self.assertIsNone(ctx.get("todos"))
1113
+
988
1114
  @mock.patch("core.views._sync_with_origin_main")
989
1115
  @mock.patch("core.views.release_utils._git_clean", return_value=True)
990
1116
  @mock.patch("core.views.release_utils.network_available", return_value=False)
@@ -1475,6 +1601,54 @@ class ReleaseProcessTests(TestCase):
1475
1601
  ctx = session.get(f"release_publish_{self.release.pk}")
1476
1602
  self.assertTrue(ctx.get("dry_run"))
1477
1603
 
1604
+ def test_resume_button_shown_when_credentials_missing(self):
1605
+ user = User.objects.create_superuser("admin", "admin@example.com", "pw")
1606
+ url = reverse("release-progress", args=[self.release.pk, "publish"])
1607
+ self.client.force_login(user)
1608
+
1609
+ self.client.get(f"{url}?start=1")
1610
+
1611
+ session = self.client.session
1612
+ ctx = session.get(f"release_publish_{self.release.pk}") or {}
1613
+ ctx.update({"step": 7, "started": True, "paused": False})
1614
+ session[f"release_publish_{self.release.pk}"] = ctx
1615
+ session.save()
1616
+
1617
+ response = self.client.get(f"{url}?step=7")
1618
+ self.assertEqual(response.status_code, 200)
1619
+ context = response.context
1620
+ if isinstance(context, list):
1621
+ context = context[-1]
1622
+ self.assertTrue(context["resume_available"])
1623
+ self.assertIn(b"Resume Publish", response.content)
1624
+
1625
+ def test_resume_without_step_parameter_defaults_to_current_progress(self):
1626
+ run: list[str] = []
1627
+
1628
+ def step_fn(release, ctx, log_path):
1629
+ run.append("step")
1630
+
1631
+ steps = [("Only step", step_fn)]
1632
+ user = User.objects.create_superuser("admin", "admin@example.com", "pw")
1633
+ url = reverse("release-progress", args=[self.release.pk, "publish"])
1634
+ with mock.patch("core.views.PUBLISH_STEPS", steps):
1635
+ self.client.force_login(user)
1636
+ session = self.client.session
1637
+ session[f"release_publish_{self.release.pk}"] = {
1638
+ "step": 0,
1639
+ "started": True,
1640
+ "paused": False,
1641
+ }
1642
+ session.save()
1643
+
1644
+ response = self.client.get(f"{url}?resume=1")
1645
+ self.assertEqual(response.status_code, 200)
1646
+ self.assertEqual(run, ["step"])
1647
+
1648
+ session = self.client.session
1649
+ ctx = session.get(f"release_publish_{self.release.pk}")
1650
+ self.assertEqual(ctx.get("step"), 1)
1651
+
1478
1652
  def test_new_todo_does_not_reset_pending_flow(self):
1479
1653
  user = User.objects.create_superuser("admin", "admin@example.com", "pw")
1480
1654
  url = reverse("release-progress", args=[self.release.pk, "publish"])
@@ -1552,6 +1726,71 @@ class ReleaseProcessTests(TestCase):
1552
1726
  self.assertEqual(count_file.read_text(), "1")
1553
1727
 
1554
1728
 
1729
+ class PackageReleaseFixtureTests(TestCase):
1730
+ def setUp(self):
1731
+ self.base = Path("core/fixtures")
1732
+ self.existing_fixtures = {
1733
+ path: path.read_text(encoding="utf-8")
1734
+ for path in self.base.glob("releases__*.json")
1735
+ }
1736
+ self.addCleanup(self._restore_fixtures)
1737
+
1738
+ self.package = Package.objects.create(name="fixture-pkg")
1739
+ self.release_one = PackageRelease.objects.create(
1740
+ package=self.package,
1741
+ version="9.9.9",
1742
+ )
1743
+ self.release_two = PackageRelease.objects.create(
1744
+ package=self.package,
1745
+ version="9.9.10",
1746
+ )
1747
+
1748
+ def _restore_fixtures(self):
1749
+ current_paths = set(self.base.glob("releases__*.json"))
1750
+ for path in current_paths:
1751
+ if path not in self.existing_fixtures:
1752
+ path.unlink()
1753
+ for path, content in self.existing_fixtures.items():
1754
+ path.write_text(content, encoding="utf-8")
1755
+
1756
+ def test_dump_fixture_only_writes_changed_releases(self):
1757
+ PackageRelease.dump_fixture()
1758
+ target_one = self.base / "releases__packagerelease_9_9_9.json"
1759
+ target_two = self.base / "releases__packagerelease_9_9_10.json"
1760
+
1761
+ self.assertTrue(target_one.exists())
1762
+ self.assertTrue(target_two.exists())
1763
+
1764
+ self.release_two.changelog = "updated notes"
1765
+ self.release_two.save(update_fields=["changelog"])
1766
+
1767
+ original_write = Path.write_text
1768
+ written_paths: list[Path] = []
1769
+
1770
+ def tracking_write_text(path_obj, data, *args, **kwargs):
1771
+ written_paths.append(path_obj)
1772
+ return original_write(path_obj, data, *args, **kwargs)
1773
+
1774
+ with mock.patch("pathlib.Path.write_text", tracking_write_text):
1775
+ PackageRelease.dump_fixture()
1776
+
1777
+ written_set = set(written_paths)
1778
+ self.assertNotIn(target_one, written_set)
1779
+ self.assertIn(target_two, written_set)
1780
+ self.assertIn("updated notes", target_two.read_text(encoding="utf-8"))
1781
+
1782
+ def test_dump_fixture_removes_missing_release_files(self):
1783
+ PackageRelease.dump_fixture()
1784
+ target_two = self.base / "releases__packagerelease_9_9_10.json"
1785
+ self.assertTrue(target_two.exists())
1786
+
1787
+ self.release_two.delete()
1788
+
1789
+ PackageRelease.dump_fixture()
1790
+
1791
+ self.assertFalse(target_two.exists())
1792
+
1793
+
1555
1794
  class ReleaseProgressSyncTests(TestCase):
1556
1795
  def setUp(self):
1557
1796
  self.client = Client()
@@ -2044,6 +2283,42 @@ class TodoDoneTests(TestCase):
2044
2283
  todo.refresh_from_db()
2045
2284
  self.assertIsNotNone(todo.done_on)
2046
2285
 
2286
+ def test_env_refresh_preserves_completed_fixture_todo(self):
2287
+ base_dir = Path(settings.BASE_DIR)
2288
+ fixture_path = base_dir / "core" / "fixtures" / "todo__validate_screen_system_reports.json"
2289
+ spec = importlib.util.spec_from_file_location(
2290
+ "env_refresh_todo", base_dir / "env-refresh.py"
2291
+ )
2292
+ env_refresh = importlib.util.module_from_spec(spec)
2293
+ spec.loader.exec_module(env_refresh)
2294
+ fixture_rel_path = str(fixture_path.relative_to(base_dir))
2295
+ env_refresh._fixture_files = lambda: [fixture_rel_path]
2296
+
2297
+ from django.core.management import call_command as django_call
2298
+
2299
+ def fake_call_command(name, *args, **kwargs):
2300
+ if name == "loaddata":
2301
+ return django_call(name, *args, **kwargs)
2302
+ return None
2303
+
2304
+ env_refresh.call_command = fake_call_command
2305
+ env_refresh.load_shared_user_fixtures = lambda *args, **kwargs: None
2306
+ env_refresh.load_user_fixtures = lambda *args, **kwargs: None
2307
+ env_refresh.generate_model_sigils = lambda: None
2308
+
2309
+ env_refresh.run_database_tasks()
2310
+
2311
+ todo = Todo.objects.get(request="Validate screen System Reports")
2312
+ self.assertTrue(todo.is_seed_data)
2313
+ todo.done_on = timezone.now()
2314
+ todo.save(update_fields=["done_on"])
2315
+
2316
+ env_refresh.run_database_tasks()
2317
+
2318
+ todo.refresh_from_db()
2319
+ self.assertIsNotNone(todo.done_on)
2320
+ self.assertTrue(todo.is_seed_data)
2321
+
2047
2322
 
2048
2323
  class TodoFocusViewTests(TestCase):
2049
2324
  def setUp(self):
@@ -2063,6 +2338,9 @@ class TodoFocusViewTests(TestCase):
2063
2338
  self.assertContains(resp, f'src="{todo.url}"')
2064
2339
  self.assertContains(resp, "Done")
2065
2340
  self.assertContains(resp, "Back")
2341
+ self.assertContains(resp, "Take Snapshot")
2342
+ snapshot_url = reverse("todo-snapshot", args=[todo.pk])
2343
+ self.assertContains(resp, snapshot_url)
2066
2344
 
2067
2345
  def test_focus_view_uses_admin_change_when_no_url(self):
2068
2346
  todo = Todo.objects.create(request="Task")
@@ -2134,6 +2412,57 @@ class TodoFocusViewTests(TestCase):
2134
2412
  self.assertRedirects(resp, next_url, target_status_code=200)
2135
2413
 
2136
2414
 
2415
+ class TodoSnapshotViewTests(TestCase):
2416
+ PNG_PIXEL = (
2417
+ "data:image/png;base64," "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR42mP8/5+hHgAFgwJ/lSdX6QAAAABJRU5ErkJggg=="
2418
+ )
2419
+
2420
+ def setUp(self):
2421
+ self.user = User.objects.create_user(
2422
+ username="manager", password="secret", is_staff=True
2423
+ )
2424
+ self.client.force_login(self.user)
2425
+ self.todo = Todo.objects.create(
2426
+ request="QA release notes", request_details="Verify layout"
2427
+ )
2428
+
2429
+ def test_snapshot_creates_content_sample(self):
2430
+ tmpdir = tempfile.TemporaryDirectory()
2431
+ self.addCleanup(tmpdir.cleanup)
2432
+ with override_settings(LOG_DIR=Path(tmpdir.name)):
2433
+ response = self.client.post(
2434
+ reverse("todo-snapshot", args=[self.todo.pk]),
2435
+ data=json.dumps({"image": self.PNG_PIXEL}),
2436
+ content_type="application/json",
2437
+ )
2438
+ self.assertEqual(response.status_code, 200)
2439
+ payload = response.json()
2440
+ self.assertIn("sample", payload)
2441
+ self.assertEqual(ContentSample.objects.filter(kind=ContentSample.IMAGE).count(), 1)
2442
+ sample = ContentSample.objects.get(pk=payload["sample"])
2443
+ self.assertEqual(sample.method, "TODO_QA")
2444
+ self.assertEqual(sample.user, self.user)
2445
+ self.assertEqual(sample.content, "QA release notes — Verify layout")
2446
+ rel_path = Path(sample.path)
2447
+ self.assertTrue(rel_path.parts)
2448
+ self.assertEqual(rel_path.parts[0], "screenshots")
2449
+ self.assertTrue((Path(tmpdir.name) / rel_path).exists())
2450
+
2451
+ def test_snapshot_rejects_completed_todo(self):
2452
+ self.todo.done_on = timezone.now()
2453
+ self.todo.save(update_fields=["done_on"])
2454
+ tmpdir = tempfile.TemporaryDirectory()
2455
+ self.addCleanup(tmpdir.cleanup)
2456
+ with override_settings(LOG_DIR=Path(tmpdir.name)):
2457
+ response = self.client.post(
2458
+ reverse("todo-snapshot", args=[self.todo.pk]),
2459
+ data=json.dumps({"image": self.PNG_PIXEL}),
2460
+ content_type="application/json",
2461
+ )
2462
+ self.assertEqual(response.status_code, 400)
2463
+ self.assertEqual(ContentSample.objects.count(), 0)
2464
+
2465
+
2137
2466
  class TodoUrlValidationTests(TestCase):
2138
2467
  def test_relative_url_valid(self):
2139
2468
  todo = Todo(request="Task", url="/path")