lvi-api-client 0.2.6__tar.gz → 0.2.9__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lvi-api-client
3
- Version: 0.2.6
3
+ Version: 0.2.9
4
4
  Summary: LogicVein API Client Library
5
5
  Requires-Python: >=3.8
6
6
  Description-Content-Type: text/markdown
@@ -0,0 +1 @@
1
+ 0.2.9
@@ -3,7 +3,7 @@ import json
3
3
  import logging
4
4
  import os
5
5
  import re
6
- from typing import Any, Dict, List, MutableMapping, Optional, Sequence, Tuple, Union
6
+ from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
7
7
  from xml.etree import ElementTree
8
8
  from xml.sax.saxutils import escape
9
9
 
@@ -211,7 +211,10 @@ class LogicVeinAPIClient:
211
211
  f"LogicVeinAPIClient initialized: host={self.host}, username={self.username}, using_token={bool(self.api_token)}, ignore_env={ignore_env}"
212
212
  )
213
213
  self.base_url = f"https://{self.host}/rest"
214
- self.headers: MutableMapping[str, Union[str, bytes]] = {"Content-Type": "application/json"}
214
+ # The session holds this same mapping, so its headers are exactly these (requests' defaults are not sent).
215
+ self.headers: requests.structures.CaseInsensitiveDict[str] = requests.structures.CaseInsensitiveDict(
216
+ {"Content-Type": "application/json"}
217
+ )
215
218
  if self.api_token:
216
219
  self.headers["Authorization"] = f"Bearer {self.api_token}"
217
220
  self.lvi_client = requests.Session()
@@ -1840,7 +1843,8 @@ class LogicVeinAPIClient:
1840
1843
  "summary": str - Incident summary,
1841
1844
  "severity": str - "INFO", "WARNING", "ERROR", or "CRITICAL",
1842
1845
  "priority": str - "LOW", "MEDIUM", "HIGH", or "CRITICAL",
1843
- "status": str - "OPEN", "WORKING", or "CLOSED",
1846
+ "status": str - "OPEN", "RESOLVED" or "DEFERRED" seen on a server; whether
1847
+ "WORKING" or "CLOSED" exist has not been measured,
1844
1848
  "resolution": str - "PENDING" or "RESOLVED",
1845
1849
  "assignee": str - Assigned username (empty string if unassigned),
1846
1850
  "labels": list[str] - Label tags,
@@ -1875,14 +1879,15 @@ class LogicVeinAPIClient:
1875
1879
  # Incidents
1876
1880
  # =====================================================
1877
1881
 
1878
- def get_incident_by_id(self, incident_id: int) -> Dict[str, Any]:
1882
+ def get_incident_by_id(self, incident_id: int) -> Optional[Dict[str, Any]]:
1879
1883
  """Get an incident by its ID from the LogicVein server.
1880
1884
 
1881
1885
  Args:
1882
1886
  incident_id (int): The ID of the incident.
1883
1887
 
1884
1888
  Returns:
1885
- Dict[str, Any]: The incident if it exists, otherwise an empty dictionary.
1889
+ Dict[str, Any]: The incident, or None for an unknown ID, which the server answers with
1890
+ a null result. An empty dictionary only when the server gives no result at all.
1886
1891
  """
1887
1892
  response = self.call("Incidents.getIncidentById", incidentId=incident_id)
1888
1893
 
@@ -2549,6 +2554,8 @@ class LogicVeinAPIClient:
2549
2554
  def delete_managed_network(self, network_name: str) -> bool:
2550
2555
  """Delete a managed network from the LogicVein server.
2551
2556
 
2557
+ An alert policy whose only network this is goes with it.
2558
+
2552
2559
  Args:
2553
2560
  network_name (str): The name of the managed network to delete.
2554
2561
 
@@ -3648,6 +3655,8 @@ class LogicVeinAPIClient:
3648
3655
  """Get actions configured for an alert policy.
3649
3656
 
3650
3657
  Calls Collection.getActionsForPolicy. Returns the result list directly (not paginated).
3658
+ Settings come back as stored, secrets included: an exec action's ``password`` and a
3659
+ trap action's ``community`` are not masked, and the web UI sends them back unchanged.
3651
3660
  Each action has:
3652
3661
  {
3653
3662
  "actionId": int - Action ID,
@@ -3677,6 +3686,10 @@ class LogicVeinAPIClient:
3677
3686
  Returns the saved actions with assigned actionIds (new actions use -1 as input id,
3678
3687
  which is replaced with the assigned ID in the response).
3679
3688
 
3689
+ The saved actions **replace** all of the policy's actions; they are not added to them.
3690
+ An empty list does nothing, so it cannot clear a policy's actions. ``username`` is
3691
+ filled in with the caller's when left out.
3692
+
3680
3693
  Each action object should have:
3681
3694
  {
3682
3695
  "actionId": int - Use -1 for new actions,
@@ -4288,6 +4301,33 @@ class LogicVeinAPIClient:
4288
4301
  Returns:
4289
4302
  List[Dict[str, Any]]: All matching alert policies.
4290
4303
  """
4304
+ return self._search_alert_policies(managed_networks, queries, sort_column, descending, page_size, strict=False)
4305
+
4306
+ def _search_alert_policies(
4307
+ self,
4308
+ managed_networks: List[str],
4309
+ queries: List[str],
4310
+ sort_column: str,
4311
+ descending: bool,
4312
+ page_size: int,
4313
+ strict: bool,
4314
+ ) -> List[Dict[str, Any]]:
4315
+ """Read every page of alert policies.
4316
+
4317
+ Args:
4318
+ managed_networks (List[str]): Networks to search. Empty for all.
4319
+ queries (List[str]): Query filters.
4320
+ sort_column (str): Column to sort by.
4321
+ descending (bool): Sort descending if True.
4322
+ page_size (int): Records per API call.
4323
+ strict (bool): Raise on an answer with no policies list, rather than stopping there.
4324
+
4325
+ Returns:
4326
+ List[Dict[str, Any]]: All matching alert policies.
4327
+
4328
+ Raises:
4329
+ ValueError: If ``strict`` and an answer has no policies list.
4330
+ """
4291
4331
  all_policies: List[Dict[str, Any]] = []
4292
4332
  offset = 0
4293
4333
 
@@ -4306,8 +4346,11 @@ class LogicVeinAPIClient:
4306
4346
  }
4307
4347
  response = self.call("Collection.searchAlertPolicies", **params)
4308
4348
 
4309
- if response and isinstance(response, dict) and "result" in response and "policies" in response["result"]:
4310
- batch = response["result"]["policies"]
4349
+ result = response.get("result") if isinstance(response, dict) else None
4350
+ if isinstance(result, dict) and isinstance(result.get("policies"), list):
4351
+ batch = result["policies"]
4352
+ elif strict:
4353
+ raise ValueError("Collection.searchAlertPolicies did not answer with a list of policies")
4311
4354
  else:
4312
4355
  break
4313
4356
 
@@ -4325,6 +4368,12 @@ class LogicVeinAPIClient:
4325
4368
 
4326
4369
  Calls Collection.saveAlertPolicy. The response mirrors the input policy object.
4327
4370
  Use policyId=-1 (or omit) to create a new policy; provide an existing policyId to update.
4371
+ The result carries the new policy's ID.
4372
+
4373
+ Saving an existing policy **removes all its actions**, although ``actionTypes`` still
4374
+ lists them: save them again with :meth:`save_alert_actions`, as the web UI does. Saving
4375
+ a name that is already in use makes a second policy of that name; check with
4376
+ :meth:`is_alert_policy_name_unique` first.
4328
4377
 
4329
4378
  The policy object has:
4330
4379
  {
@@ -4347,6 +4396,117 @@ class LogicVeinAPIClient:
4347
4396
  return response["result"]
4348
4397
  return None
4349
4398
 
4399
+ def delete_alert_policy(self, policy_id: int, reassign_policy_id: int = -1) -> bool:
4400
+ """Delete an alert policy, with its actions, refusing one still in use.
4401
+
4402
+ A policy that alert triggers still use (:meth:`is_alert_policy_referenced`) may not be
4403
+ deleted on its own: as in the web UI, which asks for an alternate policy to take those
4404
+ triggers, ``reassign_policy_id`` must name another existing policy. Otherwise this
4405
+ raises before deleting anything.
4406
+
4407
+ Calls Collection.deleteAlertPolicy with ``policyId`` and ``reassignPolicyId``, as the UI
4408
+ does; the server refuses ``policyId`` alone. It answers true when it deletes the policy
4409
+ and false for an unknown ID. The policies are then read back, and the result is whether
4410
+ this one is gone: an answer that cannot show it raises rather than reading as deleted.
4411
+ Errors from the calls themselves (``LogicVeinAPIError``, ``requests`` errors) propagate.
4412
+
4413
+ Args:
4414
+ policy_id (int): The ID of the policy to delete.
4415
+ reassign_policy_id (int): The policy to move its triggers to, required when the
4416
+ policy is in use; -1 for none.
4417
+
4418
+ Returns:
4419
+ bool: True if the policy is gone afterwards.
4420
+
4421
+ Raises:
4422
+ ValueError: If the policy is in use and no other existing policy is given to take
4423
+ its triggers, if the policies cannot be read (an answer with no list of policies,
4424
+ or a policy without an ID), or if the reference check does not answer true or false.
4425
+ """
4426
+ if reassign_policy_id == policy_id:
4427
+ raise ValueError(f"Alert policy {policy_id} cannot take its own triggers; name another policy")
4428
+ in_use = self.is_alert_policy_referenced(policy_id)
4429
+ if reassign_policy_id != -1:
4430
+ if reassign_policy_id not in self._alert_policy_ids():
4431
+ raise ValueError(f"Alert policy {reassign_policy_id}, to take the triggers, does not exist")
4432
+ elif in_use:
4433
+ raise ValueError(
4434
+ f"Alert policy {policy_id} is used by alert triggers, so it may not be deleted until another "
4435
+ "policy takes them: pass reassign_policy_id"
4436
+ )
4437
+ self.call("Collection.deleteAlertPolicy", policyId=policy_id, reassignPolicyId=reassign_policy_id)
4438
+ return policy_id not in self._alert_policy_ids()
4439
+
4440
+ def _alert_policy_ids(self) -> List[int]:
4441
+ """Read the IDs of every alert policy, failing on an answer that cannot list them.
4442
+
4443
+ Returns:
4444
+ List[int]: The IDs.
4445
+
4446
+ Raises:
4447
+ ValueError: If the answer has no list of policies, or a policy without an ID.
4448
+ """
4449
+ policies = self._search_alert_policies([], [], "policyName", False, 500, strict=True)
4450
+ ids = [policy.get("policyId") if isinstance(policy, dict) else None for policy in policies]
4451
+ known = [found for found in ids if isinstance(found, int)]
4452
+ if len(known) != len(ids):
4453
+ raise ValueError("Collection.searchAlertPolicies answered with a policy that has no ID")
4454
+ return known
4455
+
4456
+ def is_alert_policy_referenced(self, policy_id: int) -> bool:
4457
+ """Check whether anything refers to an alert policy.
4458
+
4459
+ Calls Collection.isAlertPolicyReferenced, which the web UI calls before deleting one: a
4460
+ policy in use may only be deleted with another policy to take its alert triggers (see
4461
+ :meth:`delete_alert_policy`). On the lab, a policy no monitor trigger names also
4462
+ answered true, so more than monitor triggers count. An unknown ID answers false.
4463
+
4464
+ Args:
4465
+ policy_id (int): The ID of the policy.
4466
+
4467
+ Returns:
4468
+ bool: True if the policy is referenced.
4469
+
4470
+ Raises:
4471
+ ValueError: If the server's answer is not true or false.
4472
+ """
4473
+ response = self.call("Collection.isAlertPolicyReferenced", policyId=policy_id)
4474
+ referenced = response.get("result") if isinstance(response, dict) else None
4475
+ if not isinstance(referenced, bool):
4476
+ raise ValueError(f"Collection.isAlertPolicyReferenced did not answer true or false for {policy_id}")
4477
+ return referenced
4478
+
4479
+ def is_alert_policy_name_unique(
4480
+ self, policy_name: str, managed_networks: List[str], ignore_policy_id: int = -1
4481
+ ) -> bool:
4482
+ """Check whether an alert policy name is free in some networks.
4483
+
4484
+ Calls Collection.isAlertPolicyNameUnique, with the parameters the web UI sends as a
4485
+ name is typed. :meth:`save_alert_policy` does not check: it saves a second policy of
4486
+ a name in use.
4487
+
4488
+ Args:
4489
+ policy_name (str): The name to check.
4490
+ managed_networks (List[str]): The networks to check it in.
4491
+ ignore_policy_id (int): A policy to leave out, such as the one being renamed. -1 for none.
4492
+
4493
+ Returns:
4494
+ bool: True if the name is free.
4495
+
4496
+ Raises:
4497
+ ValueError: If the server's answer is not true or false.
4498
+ """
4499
+ response = self.call(
4500
+ "Collection.isAlertPolicyNameUnique",
4501
+ policyName=policy_name,
4502
+ managedNetworks=managed_networks,
4503
+ ignorePolicyId=ignore_policy_id,
4504
+ )
4505
+ unique = response.get("result") if isinstance(response, dict) else None
4506
+ if not isinstance(unique, bool):
4507
+ raise ValueError(f"Collection.isAlertPolicyNameUnique did not answer true or false for {policy_name}")
4508
+ return unique
4509
+
4350
4510
  # =====================================================
4351
4511
  # Maps / Topology
4352
4512
  # =====================================================
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lvi-api-client
3
- Version: 0.2.6
3
+ Version: 0.2.9
4
4
  Summary: LogicVein API Client Library
5
5
  Requires-Python: >=3.8
6
6
  Description-Content-Type: text/markdown
@@ -9,5 +9,6 @@ src/lvi_api_client.egg-info/SOURCES.txt
9
9
  src/lvi_api_client.egg-info/dependency_links.txt
10
10
  src/lvi_api_client.egg-info/requires.txt
11
11
  src/lvi_api_client.egg-info/top_level.txt
12
+ tests/test_check_version_bump.py
12
13
  tests/test_lvi_api_client.py
13
14
  tests/test_server_request_shapes.py
@@ -0,0 +1,62 @@
1
+ """Tests for scripts/ci/check_version_bump.py."""
2
+
3
+ import importlib.util
4
+ from pathlib import Path
5
+
6
+ import pytest
7
+
8
+ _PATH = Path(__file__).resolve().parent.parent / "scripts" / "ci" / "check_version_bump.py"
9
+ _SPEC = importlib.util.spec_from_file_location("check_version_bump", _PATH)
10
+ assert _SPEC is not None and _SPEC.loader is not None, _PATH
11
+ check = importlib.util.module_from_spec(_SPEC)
12
+ _SPEC.loader.exec_module(check)
13
+
14
+ LOG = "# Changelog\n\n## [0.2.7] - 2026-11-08\n\n### Fixed\n- Something.\n\n## [0.2.6] - 2026-11-07\n"
15
+
16
+
17
+ def test_a_heading_for_the_version_passes():
18
+ assert check.changelog_problem(LOG, "0.2.7") == ""
19
+
20
+
21
+ @pytest.mark.parametrize(
22
+ "changelog",
23
+ [
24
+ "## [Unreleased]\n\n### Fixed\n- Something.\n\n## [0.2.6] - 2026-11-07\n",
25
+ "Fixed in [0.2.7], see below.\n## [0.2.6]\n",
26
+ "## [0.2.70] - 2026-11-08\n",
27
+ ],
28
+ ids=["only unreleased", "not a heading", "another version"],
29
+ )
30
+ def test_no_heading_for_the_version_fails(changelog):
31
+ assert "no '## [0.2.7]' heading" in check.changelog_problem(changelog, "0.2.7")
32
+
33
+
34
+ def _run(monkeypatch, tmp_path, changed, version, changelog, base_version="0.2.6"):
35
+ (tmp_path / "VERSION").write_text(version + "\n")
36
+ (tmp_path / "CHANGELOG.md").write_text(changelog)
37
+ monkeypatch.setattr(check, "REPO_ROOT", tmp_path)
38
+
39
+ def git(*args):
40
+ return "\n".join(changed) if args[0] == "diff" else base_version + "\n"
41
+
42
+ monkeypatch.setattr(check, "_git", git)
43
+ monkeypatch.setattr("sys.argv", ["check_version_bump.py", "--base", "abc123"])
44
+ return check.main()
45
+
46
+
47
+ def test_a_bump_with_entries_left_under_unreleased_fails(monkeypatch, tmp_path, capsys):
48
+ changelog = "## [Unreleased]\n\n### Fixed\n- Something.\n"
49
+ assert _run(monkeypatch, tmp_path, ["src/x.py", "VERSION", "CHANGELOG.md"], "0.2.7", changelog) == 1
50
+ assert "no '## [0.2.7]' heading" in capsys.readouterr().out
51
+
52
+
53
+ def test_a_bump_without_a_src_change_still_needs_the_heading(monkeypatch, tmp_path):
54
+ assert _run(monkeypatch, tmp_path, ["VERSION"], "0.2.7", "## [0.2.6]\n") == 1
55
+
56
+
57
+ def test_a_bump_with_its_heading_passes(monkeypatch, tmp_path):
58
+ assert _run(monkeypatch, tmp_path, ["src/x.py", "VERSION", "CHANGELOG.md"], "0.2.7", LOG) == 0
59
+
60
+
61
+ def test_a_docs_change_needs_nothing(monkeypatch, tmp_path):
62
+ assert _run(monkeypatch, tmp_path, ["CHANGELOG.md", "README.md"], "0.2.6", "## [Unreleased]\n") == 0
@@ -48,6 +48,16 @@ class TestLogicVeinAPIClientUnit:
48
48
  assert "Authorization" in client.headers
49
49
  assert client.headers["Authorization"] == "Bearer test-token-123"
50
50
 
51
+ def test_the_session_sends_only_the_clients_headers(self, mock_env):
52
+ """The session holds the client's own header mapping, with no requests defaults added."""
53
+ client = LogicVeinAPIClient()
54
+ assert client.lvi_client.headers is client.headers
55
+ assert dict(client.lvi_client.headers) == {
56
+ "Content-Type": "application/json",
57
+ "Authorization": "Bearer test-token-123",
58
+ }
59
+ assert client.lvi_client.headers["content-type"] == "application/json"
60
+
51
61
  def test_initialization_with_kwargs(self, monkeypatch):
52
62
  """Test client initialization with kwargs instead of env vars."""
53
63
  # Clear env vars so kwargs take precedence
@@ -620,3 +620,116 @@ class TestDeployingASet:
620
620
  with pytest.raises(ValueError):
621
621
  client.associate_monitor_set(2, queries)
622
622
  assert server.requests == []
623
+
624
+
625
+ class TestAlertPolicies:
626
+ """Deleting alert policies and the checks the web UI makes around saving and deleting them."""
627
+
628
+ @staticmethod
629
+ def listing(*ids):
630
+ return {"policies": [{"policyId": i, "policyName": f"p{i}"} for i in ids]}
631
+
632
+ def test_a_delete_of_a_policy_not_in_use_sends_what_the_ui_sends_and_reads_the_policies_back(self, connect):
633
+ client, server = connect(
634
+ {
635
+ "Collection.isAlertPolicyReferenced": False,
636
+ "Collection.deleteAlertPolicy": True,
637
+ "Collection.searchAlertPolicies": self.listing(1),
638
+ }
639
+ )
640
+ assert client.delete_alert_policy(65) is True
641
+ assert server.sent("Collection.isAlertPolicyReferenced") == {"policyId": 65}
642
+ assert server.sent("Collection.deleteAlertPolicy") == {"policyId": 65, "reassignPolicyId": -1}
643
+ assert server.sent("Collection.searchAlertPolicies")["managedNetworks"] == []
644
+
645
+ def test_a_policy_still_listed_after_a_delete_is_not_deleted(self, connect):
646
+ client, _ = connect(
647
+ {
648
+ "Collection.isAlertPolicyReferenced": False,
649
+ "Collection.deleteAlertPolicy": False,
650
+ "Collection.searchAlertPolicies": self.listing(65),
651
+ }
652
+ )
653
+ assert client.delete_alert_policy(65) is False
654
+
655
+ def test_a_policy_in_use_is_not_deleted_without_a_policy_to_take_its_triggers(self, connect):
656
+ client, server = connect({"Collection.isAlertPolicyReferenced": True})
657
+ with pytest.raises(ValueError, match="used by alert triggers"):
658
+ client.delete_alert_policy(65)
659
+ assert [method for method, _ in server.requests] == ["Collection.isAlertPolicyReferenced"]
660
+
661
+ def test_a_policy_in_use_is_deleted_with_another_to_take_its_triggers(self, connect):
662
+ client, server = connect(
663
+ {
664
+ "Collection.isAlertPolicyReferenced": True,
665
+ "Collection.deleteAlertPolicy": True,
666
+ "Collection.searchAlertPolicies": self.listing(1, 65),
667
+ }
668
+ )
669
+ # The read-back still lists 65 because this fake does not delete; what matters is the request.
670
+ client.delete_alert_policy(65, reassign_policy_id=1)
671
+ assert server.sent("Collection.deleteAlertPolicy") == {"policyId": 65, "reassignPolicyId": 1}
672
+
673
+ @pytest.mark.parametrize("reassign", [65, 2])
674
+ def test_a_policy_to_take_the_triggers_must_be_another_that_exists(self, connect, reassign):
675
+ client, server = connect(
676
+ {"Collection.isAlertPolicyReferenced": True, "Collection.searchAlertPolicies": self.listing(1, 65)}
677
+ )
678
+ with pytest.raises(ValueError):
679
+ client.delete_alert_policy(65, reassign_policy_id=reassign)
680
+ assert "Collection.deleteAlertPolicy" not in [method for method, _ in server.requests]
681
+
682
+ def test_a_reference_check_with_no_answer_deletes_nothing(self, connect):
683
+ client, server = connect({"Collection.isAlertPolicyReferenced": None})
684
+ with pytest.raises(ValueError):
685
+ client.delete_alert_policy(65)
686
+ assert "Collection.deleteAlertPolicy" not in [method for method, _ in server.requests]
687
+
688
+ @pytest.mark.parametrize(
689
+ "answer", [None, {}, {"policies": None}, {"policies": [{}]}, {"policies": [{"policyId": None}, "x"]}]
690
+ )
691
+ def test_a_failed_read_back_raises_rather_than_reading_as_deleted(self, connect, answer):
692
+ client, _ = connect(
693
+ {
694
+ "Collection.isAlertPolicyReferenced": False,
695
+ "Collection.deleteAlertPolicy": True,
696
+ "Collection.searchAlertPolicies": answer,
697
+ }
698
+ )
699
+ with pytest.raises(ValueError):
700
+ client.delete_alert_policy(65)
701
+
702
+ def test_a_failed_search_still_reads_as_no_policies(self, connect):
703
+ client, _ = connect({"Collection.searchAlertPolicies": None})
704
+ assert client.search_alert_policies() == []
705
+
706
+ @pytest.mark.parametrize("answer", [True, False])
707
+ def test_a_reference_check_sends_the_id_and_returns_the_answer(self, connect, answer):
708
+ client, server = connect({"Collection.isAlertPolicyReferenced": answer})
709
+ assert client.is_alert_policy_referenced(27) is answer
710
+ assert server.sent("Collection.isAlertPolicyReferenced") == {"policyId": 27}
711
+
712
+ def test_a_reference_check_with_no_answer_raises_rather_than_reading_as_unreferenced(self, connect):
713
+ client, _ = connect({"Collection.isAlertPolicyReferenced": None})
714
+ with pytest.raises(ValueError):
715
+ client.is_alert_policy_referenced(27)
716
+
717
+ @pytest.mark.parametrize("answer", [True, False])
718
+ def test_a_name_check_sends_what_the_ui_sends_and_returns_the_answer(self, connect, answer):
719
+ client, server = connect({"Collection.isAlertPolicyNameUnique": answer})
720
+ assert client.is_alert_policy_name_unique("p1", ["Default"], ignore_policy_id=1) is answer
721
+ assert server.sent("Collection.isAlertPolicyNameUnique") == {
722
+ "policyName": "p1",
723
+ "managedNetworks": ["Default"],
724
+ "ignorePolicyId": 1,
725
+ }
726
+
727
+ def test_a_name_check_ignores_no_policy_by_default(self, connect):
728
+ client, server = connect({"Collection.isAlertPolicyNameUnique": True})
729
+ client.is_alert_policy_name_unique("p1", ["Default"])
730
+ assert server.sent("Collection.isAlertPolicyNameUnique")["ignorePolicyId"] == -1
731
+
732
+ def test_a_name_check_with_no_answer_raises_rather_than_reading_as_taken(self, connect):
733
+ client, _ = connect({"Collection.isAlertPolicyNameUnique": None})
734
+ with pytest.raises(ValueError):
735
+ client.is_alert_policy_name_unique("p1", ["Default"])
@@ -1 +0,0 @@
1
- 0.2.6
File without changes
File without changes
File without changes