xsoar-cli 2.4.0__tar.gz → 2.4.2__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.
Files changed (176) hide show
  1. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/CHANGELOG.md +8 -0
  2. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/PKG-INFO +2 -2
  3. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/_version.py +2 -2
  4. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/case/README.md +47 -0
  5. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/case/commands.py +106 -0
  6. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/manifest/commands.py +21 -0
  7. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/xsoar_client/artifact_providers/azure.py +7 -2
  8. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/xsoar_client/artifact_providers/base.py +8 -2
  9. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/xsoar_client/artifact_providers/s3.py +7 -2
  10. xsoar_cli-2.4.2/src/xsoar_cli/xsoar_client/cases.py +78 -0
  11. xsoar_cli-2.4.2/src/xsoar_cli/xsoar_client/constants.py +8 -0
  12. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/xsoar_client/execution.py +2 -5
  13. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/cli/conftest.py +13 -3
  14. xsoar_cli-2.4.2/tests/cli/test_case.py +123 -0
  15. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/cli/test_manifest.py +64 -0
  16. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/test_artifact_azure.py +2 -3
  17. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/test_artifact_s3.py +12 -3
  18. xsoar_cli-2.4.2/tests/unit/test_case_commands.py +39 -0
  19. xsoar_cli-2.4.2/tests/unit/test_cases.py +353 -0
  20. xsoar_cli-2.4.0/src/xsoar_cli/xsoar_client/cases.py +0 -25
  21. xsoar_cli-2.4.0/src/xsoar_cli/xsoar_client/constants.py +0 -4
  22. xsoar_cli-2.4.0/tests/cli/test_case.py +0 -54
  23. xsoar_cli-2.4.0/tests/unit/test_cases.py +0 -175
  24. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/.github/workflows/main.yml +0 -0
  25. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/.github/workflows/pull-request-open.yml +0 -0
  26. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/.github/workflows/release.yml +0 -0
  27. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/.github/workflows/update-changelog.yml +0 -0
  28. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/.gitignore +0 -0
  29. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/CLAUDE.md +0 -0
  30. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/CONTRIBUTING.md +0 -0
  31. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/LICENSE.txt +0 -0
  32. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/README.md +0 -0
  33. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/pyproject.toml +0 -0
  34. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/__init__.py +0 -0
  35. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/cli.py +0 -0
  36. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/__init__.py +0 -0
  37. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/case/__init__.py +0 -0
  38. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/completions/README.md +0 -0
  39. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/completions/__init__.py +0 -0
  40. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/completions/commands.py +0 -0
  41. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/config/README.md +0 -0
  42. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/config/__init__.py +0 -0
  43. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/config/commands.py +0 -0
  44. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/content/README.md +0 -0
  45. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/content/__init__.py +0 -0
  46. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/content/commands.py +0 -0
  47. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/execute/README.md +0 -0
  48. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/execute/__init__.py +0 -0
  49. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/execute/commands.py +0 -0
  50. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/graph/README.md +0 -0
  51. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/graph/__init__.py +0 -0
  52. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/graph/commands.py +0 -0
  53. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/integration/README.md +0 -0
  54. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/integration/__init__.py +0 -0
  55. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/integration/commands.py +0 -0
  56. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/manifest/README.md +0 -0
  57. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/manifest/__init__.py +0 -0
  58. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/pack/README.md +0 -0
  59. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/pack/__init__.py +0 -0
  60. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/pack/commands.py +0 -0
  61. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/plugins/README.md +0 -0
  62. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/plugins/__init__.py +0 -0
  63. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/plugins/commands.py +0 -0
  64. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/rbac/README.md +0 -0
  65. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/rbac/__init__.py +0 -0
  66. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/rbac/commands.py +0 -0
  67. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/configuration.py +0 -0
  68. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/error_handling/__init__.py +0 -0
  69. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/error_handling/connection.py +0 -0
  70. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/error_handling/http.py +0 -0
  71. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/log.py +0 -0
  72. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/plugins/README.md +0 -0
  73. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/plugins/__init__.py +0 -0
  74. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/plugins/manager.py +0 -0
  75. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/utilities/__init__.py +0 -0
  76. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/utilities/config_file.py +0 -0
  77. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/utilities/content.py +0 -0
  78. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/utilities/download_content_handlers.py +0 -0
  79. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/utilities/manifest.py +0 -0
  80. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/utilities/output.py +0 -0
  81. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/utilities/validators.py +0 -0
  82. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/utilities/version_check.py +0 -0
  83. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/xsoar_client/__init__.py +0 -0
  84. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/xsoar_client/artifact_providers/__init__.py +0 -0
  85. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/xsoar_client/client.py +0 -0
  86. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/xsoar_client/content.py +0 -0
  87. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/xsoar_client/integrations.py +0 -0
  88. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/xsoar_client/packs.py +0 -0
  89. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/xsoar_client/rbac.py +0 -0
  90. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/__init__.py +0 -0
  91. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/cli/__init__.py +0 -0
  92. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/cli/test_base.py +0 -0
  93. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/cli/test_completions.py +0 -0
  94. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/cli/test_config.py +0 -0
  95. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/cli/test_content.py +0 -0
  96. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/cli/test_execute.py +0 -0
  97. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/cli/test_graph.py +0 -0
  98. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/cli/test_pack.py +0 -0
  99. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/cli/test_plugins.py +0 -0
  100. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/conftest.py +0 -0
  101. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/fixtures/cases/create.json +0 -0
  102. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/fixtures/cases/get.json +0 -0
  103. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/fixtures/content/automation_search.json +0 -0
  104. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/fixtures/content/playbook_search.json +0 -0
  105. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/fixtures/content/user_commands.json +0 -0
  106. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/fixtures/integrations/instances.json +0 -0
  107. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/fixtures/manifest/manifest_base.json +0 -0
  108. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/fixtures/manifest/manifest_invalid.json +0 -0
  109. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/fixtures/manifest/manifest_with_pack_not_on_server.json +0 -0
  110. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/fixtures/manifest/server_base_response.json +0 -0
  111. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/fixtures/manifest/server_base_response_missing_one_pack.json +0 -0
  112. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/fixtures/manifest/server_base_response_with_updates.json +0 -0
  113. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/fixtures/manifest/server_base_response_with_updates_and_one_extra.json +0 -0
  114. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/fixtures/packs/installed.json +0 -0
  115. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/fixtures/packs/installed_expired.json +0 -0
  116. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/fixtures/rbac/roles.json +0 -0
  117. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/fixtures/rbac/user_groups.json +0 -0
  118. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/fixtures/rbac/users.json +0 -0
  119. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Download/playbook-empty.yml +0 -0
  120. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_CommonPlaybooks/.pack-ignore +0 -0
  121. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_CommonPlaybooks/.secrets-ignore +0 -0
  122. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_CommonPlaybooks/Author_image.png +0 -0
  123. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_CommonPlaybooks/Playbooks/GenericPlaybook.yml +0 -0
  124. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_CommonPlaybooks/README.md +0 -0
  125. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_CommonPlaybooks/pack_metadata.json +0 -0
  126. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_CommonScripts/.pack-ignore +0 -0
  127. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_CommonScripts/.secrets-ignore +0 -0
  128. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_CommonScripts/Author_image.png +0 -0
  129. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_CommonScripts/README.md +0 -0
  130. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_CommonScripts/Scripts/GenericScript/GenericScript.py +0 -0
  131. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_CommonScripts/Scripts/GenericScript/GenericScript.yml +0 -0
  132. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_CommonScripts/Scripts/GenericScript/README.md +0 -0
  133. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_CommonScripts/pack_metadata.json +0 -0
  134. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_EDR/.pack-ignore +0 -0
  135. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_EDR/.secrets-ignore +0 -0
  136. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_EDR/Author_image.png +0 -0
  137. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_EDR/Playbooks/EDR_InitialTriage.yml +0 -0
  138. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_EDR/README.md +0 -0
  139. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_EDR/Scripts/EDR_FetchFile/EDR_FetchFile.py +0 -0
  140. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_EDR/Scripts/EDR_FetchFile/EDR_FetchFile.yml +0 -0
  141. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_EDR/Scripts/EDR_FetchFile/README.md +0 -0
  142. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_EDR/Scripts/EDR_Triage/EDR_Triage.py +0 -0
  143. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_EDR/Scripts/EDR_Triage/EDR_Triage.yml +0 -0
  144. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_EDR/Scripts/EDR_Triage/README.md +0 -0
  145. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_EDR/Scripts/LegacyItem/LegacyItem.py +0 -0
  146. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_EDR/Scripts/LegacyItem/LegacyItem.yml +0 -0
  147. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_EDR/Scripts/LegacyItem/README.md +0 -0
  148. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_EDR/Scripts/LegacyItem/test_data/basescript-dummy.json +0 -0
  149. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_EDR/pack_metadata.json +0 -0
  150. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_Layouts/.pack-ignore +0 -0
  151. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_Layouts/.secrets-ignore +0 -0
  152. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_Layouts/Author_image.png +0 -0
  153. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_Layouts/Layouts/layoutscontainer-GenericLayout.json +0 -0
  154. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_Layouts/README.md +0 -0
  155. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_Layouts/pack_metadata.json +0 -0
  156. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/Not_applicable/Playbooks/Empty.yml +0 -0
  157. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/__init__.py +0 -0
  158. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/conftest.py +0 -0
  159. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/test_artifact_base.py +0 -0
  160. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/test_client.py +0 -0
  161. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/test_config_file.py +0 -0
  162. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/test_content_domain.py +0 -0
  163. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/test_content_filters.py +0 -0
  164. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/test_content_handlers.py +0 -0
  165. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/test_error_handling.py +0 -0
  166. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/test_execution.py +0 -0
  167. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/test_integrations.py +0 -0
  168. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/test_main.py +0 -0
  169. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/test_manifest_utils.py +0 -0
  170. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/test_output.py +0 -0
  171. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/test_packs.py +0 -0
  172. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/test_plugin_manager.py +0 -0
  173. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/test_rbac.py +0 -0
  174. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/test_validators.py +0 -0
  175. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/test_version_check.py +0 -0
  176. {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/uv.lock +0 -0
@@ -6,12 +6,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [2.4.1] - 2026-06-19
10
+
11
+ ## [2.4.0] - 2026-06-17
12
+
9
13
  ### Added
10
14
 
11
15
  - `content get-detached --type all` queries both scripts and playbooks and displays results for each type.
12
16
  - `content list --output-format` option (choices: `table`, `json`, `plain`, default: `table`). `table` is human-readable aligned columns, `json` is machine-readable, and `plain` is tab-separated values suitable for piping to tools like `grep`.
13
17
  - `content list --search TERM` option. Filters the listed items with a case-insensitive substring match against each item's id, name, and description. Content types and integration brands with no matching items are omitted, and a `No content matching '<term>' found.` message is shown when nothing matches.
14
18
  - `content describe --type [script|playbook|command] NAME` command. Shows a single content item in detail: description, arguments, and inputs/outputs. Commands also show the integration brand and its configured instances (name and state). Supports `--output-format [table|json]` (default: `table`). See the [content README](src/xsoar_cli/commands/content/README.md) for details.
19
+ - `case get-context CASENUMBER` command. Retrieves the investigation context tree for a case as JSON, matching `demisto.context()` at runtime. The incident record is not merged in. See the [case README](src/xsoar_cli/commands/case/README.md) for details.
20
+ - `case get-entry ENTRY_ID` command. Retrieves a single War Room entry by its full ID (e.g. `112@153483`) as JSON. The case ID is derived from the entry ID. Exits non-zero with a clear message when the entry ID is malformed or the entry is not found. See the [case README](src/xsoar_cli/commands/case/README.md) for details.
21
+ - `case get-entries CASENUMBER` command. Retrieves all War Room entries for a case as a JSON array, or an empty array when the case has no entries. See the [case README](src/xsoar_cli/commands/case/README.md) for details.
15
22
 
16
23
  ### Changed
17
24
 
@@ -26,6 +33,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
26
33
 
27
34
  ### Fixed
28
35
 
36
+ - `manifest update` no longer crashes with `ValueError: max() iterable argument is empty` when a custom pack is installed on the XSOAR server but has no artifacts in the configured artifact repository. The command now skips such packs (they are already reported in the pre-existing "installed but not found in artifacts repo" warning) and continues updating the remaining packs. As part of the fix, `S3ArtifactProvider.get_latest_version()` and `AzureArtifactProvider.get_latest_version()` now return `None` instead of raising when the pack has no versions in the repo.
29
37
  - `content get-detached` now returns only the content items that are actually detached. The previous query returned all system items, so the command reported every system script (or playbook) as detached instead of just the detached ones. Results are now filtered on each item's `detached` field.
30
38
 
31
39
  ## [2.3.0] - 2026-06-04
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: xsoar-cli
3
- Version: 2.4.0
3
+ Version: 2.4.2
4
4
  Summary: A command-line interface for managing Palo Alto Networks XSOAR (Cortex XSOAR)
5
5
  Project-URL: Documentation, https://github.com/tlium/xsoar-cli#readme
6
6
  Project-URL: Issues, https://github.com/tlium/xsoar-cli/issues
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '2.4.0'
22
- __version_tuple__ = version_tuple = (2, 4, 0)
21
+ __version__ = version = '2.4.2'
22
+ __version_tuple__ = version_tuple = (2, 4, 2)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -17,6 +17,53 @@ xsoar-cli case get 312412
17
17
  xsoar-cli case get --environment prod 312412
18
18
  ```
19
19
 
20
+ ## Get Context
21
+
22
+ Retrieve the investigation context tree for a single case. Output is the raw context tree as JSON, matching `demisto.context()` at runtime. The incident record is not merged in.
23
+
24
+ **Syntax:** `xsoar-cli case get-context [OPTIONS] CASENUMBER`
25
+
26
+ **Options:**
27
+ - `--environment TEXT` - Target environment (default: uses default environment from config)
28
+
29
+ **Examples:**
30
+ ```
31
+ xsoar-cli case get-context 153483
32
+ xsoar-cli case get-context --environment prod 153483
33
+ ```
34
+
35
+ ## Get Entry
36
+
37
+ Retrieve a single War Room entry by its full ID. Output is the raw entry as JSON. The command exits non-zero with a clear message when no entry with that ID exists in the case.
38
+
39
+ `ENTRY_ID` is the full entry ID as shown in the GUI, in the form `<n>@<case-id>` (e.g. `112@153483`). The case ID is taken from the entry ID, so it does not need to be supplied separately.
40
+
41
+ **Syntax:** `xsoar-cli case get-entry [OPTIONS] ENTRY_ID`
42
+
43
+ **Options:**
44
+ - `--environment TEXT` - Target environment (default: uses default environment from config)
45
+
46
+ **Examples:**
47
+ ```
48
+ xsoar-cli case get-entry 112@153483
49
+ xsoar-cli case get-entry --environment prod 112@153483
50
+ ```
51
+
52
+ ## Get Entries
53
+
54
+ Retrieve all War Room entries for a single case. Output is the full list of entries as a JSON array, or an empty array when the case has no entries.
55
+
56
+ **Syntax:** `xsoar-cli case get-entries [OPTIONS] CASENUMBER`
57
+
58
+ **Options:**
59
+ - `--environment TEXT` - Target environment (default: uses default environment from config)
60
+
61
+ **Examples:**
62
+ ```
63
+ xsoar-cli case get-entries 153483
64
+ xsoar-cli case get-entries --environment prod 153483
65
+ ```
66
+
20
67
  ## Clone
21
68
 
22
69
  Clone a case from one environment to another. Useful for copying production cases to development environment for testing.
@@ -25,6 +25,19 @@ def parse_string_to_dict(input_string: str | None, delimiter: str) -> dict:
25
25
  return {key.strip(): value.strip() for key, value in valid_pairs}
26
26
 
27
27
 
28
+ def parse_entry_id(entry_id: str) -> int:
29
+ """Extract the numeric case ID from a full entry ID of the form '<n>@<case-id>'.
30
+
31
+ Raises ValueError if the entry ID is not in that form.
32
+ """
33
+ parts = entry_id.split("@")
34
+ expected_parts = 2
35
+ if len(parts) != expected_parts or not parts[0].isdigit() or not parts[1].isdigit():
36
+ msg = f"invalid entry id '{entry_id}', expected the form <n>@<case-id> (e.g. 112@153483)"
37
+ raise ValueError(msg)
38
+ return int(parts[1])
39
+
40
+
28
41
  @click.group()
29
42
  def case() -> None:
30
43
  """Create, retrieve, and clone cases"""
@@ -210,6 +223,99 @@ def create( # noqa: PLR0913
210
223
  click.echo(f"Created XSOAR case {case_id}")
211
224
 
212
225
 
226
+ @click.command(name="get-context")
227
+ @click.argument("casenumber", type=int)
228
+ @click.option("--environment", default=None, help="Default environment set in config file.")
229
+ @click.pass_context
230
+ @load_config
231
+ @validate_xsoar_connectivity
232
+ def get_context(ctx: click.Context, casenumber: int, environment: str | None) -> None:
233
+ """Retrieve the investigation context tree for a single case.
234
+
235
+ CASENUMBER is the numeric case ID to look up. Output is the raw context
236
+ tree as JSON, matching demisto.context() at runtime.
237
+
238
+ Usage examples:
239
+
240
+ xsoar-cli case get-context 153483
241
+ """
242
+ config = get_xsoar_config(ctx)
243
+ xsoar_client: Client = config.get_client(environment)
244
+ try:
245
+ response = xsoar_client.cases.get_context(casenumber)
246
+ except HTTPError as ex:
247
+ handler = HTTPErrorHandler()
248
+ click.echo(f"Error: {handler.get_message(ex, context='case')}")
249
+ ctx.exit(1)
250
+ click.echo(json.dumps(response, indent=4))
251
+
252
+
253
+ @click.command(name="get-entry")
254
+ @click.argument("entry_id", type=str)
255
+ @click.option("--environment", default=None, help="Default environment set in config file.")
256
+ @click.pass_context
257
+ @load_config
258
+ @validate_xsoar_connectivity
259
+ def get_entry(ctx: click.Context, entry_id: str, environment: str | None) -> None:
260
+ """Retrieve a single War Room entry by its full ID.
261
+
262
+ ENTRY_ID is the full entry ID as shown in the GUI, in the form
263
+ <n>@<case-id> (e.g. 112@153483). The case ID is taken from the entry ID, so
264
+ it does not need to be supplied separately. Output is the raw entry as JSON.
265
+
266
+ Usage examples:
267
+
268
+ xsoar-cli case get-entry 112@153483
269
+ """
270
+ try:
271
+ casenumber = parse_entry_id(entry_id)
272
+ except ValueError as ex:
273
+ click.echo(f"Error: {ex}")
274
+ ctx.exit(1)
275
+ config = get_xsoar_config(ctx)
276
+ xsoar_client: Client = config.get_client(environment)
277
+ try:
278
+ response = xsoar_client.cases.get_entry(casenumber, entry_id)
279
+ except HTTPError as ex:
280
+ handler = HTTPErrorHandler()
281
+ click.echo(f"Error: {handler.get_message(ex, context='case')}")
282
+ ctx.exit(1)
283
+ except ValueError as ex:
284
+ click.echo(f"Error: {ex}")
285
+ ctx.exit(1)
286
+ click.echo(json.dumps(response, indent=4))
287
+
288
+
289
+ @click.command(name="get-entries")
290
+ @click.argument("casenumber", type=int)
291
+ @click.option("--environment", default=None, help="Default environment set in config file.")
292
+ @click.pass_context
293
+ @load_config
294
+ @validate_xsoar_connectivity
295
+ def get_entries(ctx: click.Context, casenumber: int, environment: str | None) -> None:
296
+ """Retrieve all War Room entries for a single case.
297
+
298
+ CASENUMBER is the numeric case ID. Output is the full list of entries as a
299
+ JSON array, or an empty array when the case has no entries.
300
+
301
+ Usage examples:
302
+
303
+ xsoar-cli case get-entries 153483
304
+ """
305
+ config = get_xsoar_config(ctx)
306
+ xsoar_client: Client = config.get_client(environment)
307
+ try:
308
+ response = xsoar_client.cases.get_entries(casenumber)
309
+ except HTTPError as ex:
310
+ handler = HTTPErrorHandler()
311
+ click.echo(f"Error: {handler.get_message(ex, context='case')}")
312
+ ctx.exit(1)
313
+ click.echo(json.dumps(response, indent=4))
314
+
315
+
213
316
  case.add_command(get)
214
317
  case.add_command(clone)
215
318
  case.add_command(create)
319
+ case.add_command(get_context)
320
+ case.add_command(get_entry)
321
+ case.add_command(get_entries)
@@ -190,12 +190,33 @@ def update(ctx: click.Context, environment: str | None, manifest: str) -> None:
190
190
  click.echo(f" - {pack_id}", err=True)
191
191
 
192
192
  changes_made = False
193
+ skipped_custom_packs = set(result.skipped)
193
194
  for key in MANIFEST_KEYS:
194
195
  custom = key == "custom_packs"
195
196
  pack_type = "Custom" if custom else "Marketplace"
196
197
  for index, manifest_pack in enumerate(manifest_data[key]):
197
198
  if custom:
199
+ if manifest_pack["id"] in skipped_custom_packs:
200
+ # Pack is installed on the server but has no artifacts in the repo.
201
+ # Already surfaced to the user via the warning above.
202
+ logger.debug(
203
+ "%s pack '%s' skipped: not found in artifacts repo",
204
+ pack_type,
205
+ manifest_pack["id"],
206
+ )
207
+ continue
198
208
  latest = xsoar_client.artifact_provider.get_latest_version(manifest_pack["id"]) # ty: ignore[unresolved-attribute]
209
+ if latest is None:
210
+ # Defensive guard: the artifact provider reports no versions for this
211
+ # pack. This overlaps with the skipped_custom_packs check above, but
212
+ # keeps update() safe if a pack is missing from the repo without
213
+ # having been flagged by get_outdated().
214
+ logger.debug(
215
+ "%s pack '%s' skipped: no versions returned by artifact provider",
216
+ pack_type,
217
+ manifest_pack["id"],
218
+ )
219
+ continue
199
220
  else:
200
221
  pack = next((item for item in outdated_installed_packs if item["id"] == manifest_pack["id"]), None)
201
222
  if not pack:
@@ -58,9 +58,14 @@ class AzureArtifactProvider(BaseArtifactProvider):
58
58
  download_stream = self.container_client.download_blob(blob=key_name)
59
59
  return download_stream.readall()
60
60
 
61
- def get_latest_version(self, pack_id: str) -> str:
62
- """Fetch the latest version of a Pack"""
61
+ def get_latest_version(self, pack_id: str) -> str | None:
62
+ """Fetch the latest version of a Pack.
63
+
64
+ Returns None if the pack has no artifacts in the container.
65
+ """
63
66
  prefix = f"content/packs/{pack_id}/"
64
67
  iter_names = self.container_client.list_blob_names(name_starts_with=prefix)
65
68
  version_list = [x.split("/")[3] for x in list(iter_names)]
69
+ if not version_list:
70
+ return None
66
71
  return str(max(version_list, key=version.parse))
@@ -22,8 +22,14 @@ class BaseArtifactProvider(ABC):
22
22
  ...
23
23
 
24
24
  @abstractmethod
25
- def get_latest_version(self, pack_id: str) -> str:
26
- """Get the latest version of a pack."""
25
+ def get_latest_version(self, pack_id: str) -> str | None:
26
+ """Get the latest version of a pack.
27
+
28
+ Returns the highest available version string, or None if the pack has
29
+ no artifacts in the storage backend. Callers must handle the None case
30
+ (typically a pack installed on the server but missing from the artifact
31
+ repository).
32
+ """
27
33
  ...
28
34
 
29
35
  def get_pack_path(self, pack_id: str, pack_version: str) -> str:
@@ -49,8 +49,11 @@ class S3ArtifactProvider(BaseArtifactProvider):
49
49
  response = obj.get()
50
50
  return response["Body"].read()
51
51
 
52
- def get_latest_version(self, pack_id: str) -> str:
53
- """Fetch the latest version of a Pack."""
52
+ def get_latest_version(self, pack_id: str) -> str | None:
53
+ """Fetch the latest version of a Pack.
54
+
55
+ Returns None if the pack has no artifacts in the S3 bucket.
56
+ """
54
57
  client = self.session.client("s3", verify=self.verify_ssl)
55
58
  result = client.list_objects_v2(
56
59
  Bucket=self.bucket_name,
@@ -58,4 +61,6 @@ class S3ArtifactProvider(BaseArtifactProvider):
58
61
  Delimiter="/",
59
62
  )
60
63
  version_list = [x["Prefix"].split("/")[3] for x in result.get("CommonPrefixes", [])]
64
+ if not version_list:
65
+ return None
61
66
  return str(max(version_list, key=version.parse))
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from .constants import INVESTIGATION_PAGE_SIZE
6
+
7
+ if TYPE_CHECKING:
8
+ from .client import Client
9
+
10
+
11
+ class Cases:
12
+ def __init__(self, client: Client) -> None:
13
+ self.client = client
14
+
15
+ def get(self, case_id: int) -> dict:
16
+ """Fetches a case by ID."""
17
+ endpoint = f"/incident/load/{case_id}"
18
+ response = self.client.make_request(endpoint=endpoint, method="GET")
19
+ response.raise_for_status()
20
+ return response.json()
21
+
22
+ def create(self, data: dict) -> dict:
23
+ """Creates a new case."""
24
+ endpoint = self.client.resolve_endpoint(v6="/incident", v8="/xsoar/public/v1/incident")
25
+ response = self.client.make_request(endpoint=endpoint, json=data, method="POST")
26
+ response.raise_for_status()
27
+ return response.json()
28
+
29
+ def get_context(self, case_id: int) -> dict:
30
+ """Fetches the investigation context tree for a case, verbatim.
31
+
32
+ Returns the raw POST /investigation/<id>/context response, which matches
33
+ demisto.context() at runtime. The incident record is not merged in.
34
+
35
+ The body query '${.}' is a DT expression meaning "the entire context
36
+ root", so the full context tree is returned.
37
+ """
38
+ endpoint = f"/investigation/{case_id}/context"
39
+ response = self.client.make_request(endpoint=endpoint, method="POST", json={"query": "${.}"})
40
+ response.raise_for_status()
41
+ return response.json()
42
+
43
+ def get_entry(self, case_id: int, entry_id: str) -> dict:
44
+ """Fetches a single War Room entry by its full ID (e.g. '112@153483').
45
+
46
+ XSOAR has no per-entry endpoint, so the full War Room history is fetched
47
+ via POST /investigation/<id> and the entry with the matching id is
48
+ returned verbatim. Raises ValueError if no entry with that id exists.
49
+ """
50
+ for entry in self._fetch_entries(case_id):
51
+ if entry.get("id") == entry_id:
52
+ return entry
53
+ msg = f"Entry '{entry_id}' not found in case {case_id}"
54
+ raise ValueError(msg)
55
+
56
+ def get_entries(self, case_id: int) -> list[dict]:
57
+ """Fetches all War Room entries for a case, verbatim.
58
+
59
+ Returns the full entries list from POST /investigation/<id>. An empty
60
+ list is returned when the case has no entries.
61
+ """
62
+ return self._fetch_entries(case_id)
63
+
64
+ def _fetch_entries(self, case_id: int) -> list[dict]:
65
+ """Fetches the full War Room entry history for a case.
66
+
67
+ XSOAR has no per-entry endpoint, so the whole history is retrieved with
68
+ a single POST /investigation/<id> call and the entries list is returned
69
+ verbatim.
70
+ """
71
+ data, _, _ = self.client.demisto_py_instance.generic_request(
72
+ path=f"/investigation/{case_id}",
73
+ method="POST",
74
+ body={"pageSize": INVESTIGATION_PAGE_SIZE},
75
+ content_type="application/json",
76
+ response_type=object,
77
+ )
78
+ return data.get("entries") or []
@@ -0,0 +1,8 @@
1
+ """Shared constants for the xsoar_client package."""
2
+
3
+ XSOAR_OLD_VERSION = 6
4
+ HTTP_CALL_TIMEOUT = 30
5
+
6
+ # Page size requested when fetching investigation entries via POST /investigation/<id>.
7
+ # Set high so that all War Room entries are returned in a single response.
8
+ INVESTIGATION_PAGE_SIZE = 1000
@@ -4,6 +4,8 @@ import logging
4
4
  import time
5
5
  from typing import TYPE_CHECKING
6
6
 
7
+ from .constants import INVESTIGATION_PAGE_SIZE
8
+
7
9
  if TYPE_CHECKING:
8
10
  from .client import Client
9
11
 
@@ -23,11 +25,6 @@ DEFAULT_SYNC_TIMEOUT = 30
23
25
  # result entries.
24
26
  SYNC_POLL_INTERVAL = 2
25
27
 
26
- # Page size requested when fetching investigation entries during polling. The
27
- # playground accumulates history over time, so this is set high to ensure a
28
- # freshly created result entry is included in the response.
29
- INVESTIGATION_PAGE_SIZE = 1000
30
-
31
28
 
32
29
  def _format_arg_value(value: str) -> str:
33
30
  """Format a single argument value for a War Room command string.
@@ -272,9 +272,10 @@ def mock_execute_env(mock_config_file) -> Iterator[types.SimpleNamespace]: # no
272
272
  def mock_case_env(mock_config_file, make_case_response, make_case_create_response) -> Iterator[types.SimpleNamespace]: # noqa: ANN001
273
273
  """Mock environment for ``case`` commands.
274
274
 
275
- Patches ``Cases.get`` and ``Cases.create`` with default success responses.
276
- The mocks are exposed on the yielded namespace so tests can override
277
- return values or side effects as needed.
275
+ Patches ``Cases.get``, ``Cases.create``, ``Cases.get_context``,
276
+ ``Cases.get_entry``, and ``Cases.get_entries`` with default success
277
+ responses. The mocks are exposed on the yielded namespace so tests can
278
+ override return values or side effects as needed.
278
279
  """
279
280
  import types as _types
280
281
 
@@ -282,13 +283,22 @@ def mock_case_env(mock_config_file, make_case_response, make_case_create_respons
282
283
  patch("xsoar_cli.xsoar_client.client.Client.test_connectivity", return_value=True) as mock_conn,
283
284
  patch("xsoar_cli.xsoar_client.cases.Cases.get") as mock_get,
284
285
  patch("xsoar_cli.xsoar_client.cases.Cases.create") as mock_create,
286
+ patch("xsoar_cli.xsoar_client.cases.Cases.get_context") as mock_get_context,
287
+ patch("xsoar_cli.xsoar_client.cases.Cases.get_entry") as mock_get_entry,
288
+ patch("xsoar_cli.xsoar_client.cases.Cases.get_entries") as mock_get_entries,
285
289
  ):
286
290
  mock_get.return_value = make_case_response()
287
291
  mock_create.return_value = make_case_create_response()
292
+ mock_get_context.return_value = {"playbook_name": "My Playbook", "File": []}
293
+ mock_get_entry.return_value = {"id": "112@153483", "contents": "entry data"}
294
+ mock_get_entries.return_value = [{"id": "112@153483", "contents": "entry data"}]
288
295
  ns = _types.SimpleNamespace(
289
296
  config=mock_config_file,
290
297
  connectivity=mock_conn,
291
298
  get=mock_get,
292
299
  create=mock_create,
300
+ get_context=mock_get_context,
301
+ get_entry=mock_get_entry,
302
+ get_entries=mock_get_entries,
293
303
  )
294
304
  yield ns
@@ -0,0 +1,123 @@
1
+ """CLI integration tests for the ``case`` command group."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from tests.cli.conftest import InvokeHelper
9
+
10
+
11
+ class TestCaseGroup:
12
+ """The bare ``case`` command and its help output."""
13
+
14
+ def test_shows_help(self, invoke: InvokeHelper) -> None:
15
+ result = invoke(["case"])
16
+ assert result.exit_code == 0
17
+
18
+ def test_help_flag(self, invoke: InvokeHelper) -> None:
19
+ result = invoke(["case", "--help"])
20
+ assert result.exit_code == 0
21
+
22
+
23
+ class TestCaseGet:
24
+ """Tests for ``case get``."""
25
+
26
+ def test_get_case_success(self, invoke: InvokeHelper, mock_case_env) -> None:
27
+ result = invoke(["case", "get", "152230"])
28
+ assert result.exit_code == 0
29
+
30
+ def test_get_case_http_error(self, invoke: InvokeHelper, mock_case_env, make_http_error) -> None:
31
+ mock_case_env.get.side_effect = make_http_error(400, text="Bad Request")
32
+ result = invoke(["case", "get", "152230"])
33
+ assert result.exit_code == 1
34
+
35
+
36
+ class TestCaseCreate:
37
+ """Tests for ``case create``."""
38
+
39
+ def test_create_case_success(self, invoke: InvokeHelper, mock_case_env) -> None:
40
+ result = invoke(["case", "create"])
41
+ assert result.exit_code == 0
42
+
43
+
44
+ class TestCaseClone:
45
+ """Tests for ``case clone``."""
46
+
47
+ def test_clone_missing_options(self, invoke: InvokeHelper, mock_case_env) -> None:
48
+ result = invoke(["case", "clone", "152230"])
49
+ assert result.exit_code == 2
50
+
51
+ def test_clone_with_bogus_environments(self, invoke: InvokeHelper, mock_case_env) -> None:
52
+ result = invoke(["case", "clone", "--dest", "bogus", "--source", "bogus", "152230"])
53
+ assert result.exit_code == 1
54
+
55
+
56
+ class TestCaseGetContext:
57
+ """Tests for ``case get-context``."""
58
+
59
+ def test_get_context_success(self, invoke: InvokeHelper, mock_case_env) -> None:
60
+ result = invoke(["case", "get-context", "153483"])
61
+ assert result.exit_code == 0
62
+ mock_case_env.get_context.assert_called_once_with(153483)
63
+
64
+ def test_get_context_http_error(self, invoke: InvokeHelper, mock_case_env, make_http_error) -> None:
65
+ mock_case_env.get_context.side_effect = make_http_error(404, text="Not Found")
66
+ result = invoke(["case", "get-context", "153483"])
67
+ assert result.exit_code == 1
68
+
69
+
70
+ class TestCaseGetEntry:
71
+ """Tests for ``case get-entry``."""
72
+
73
+ def test_get_entry_success(self, invoke: InvokeHelper, mock_case_env) -> None:
74
+ result = invoke(["case", "get-entry", "112@153483"])
75
+ assert result.exit_code == 0
76
+ mock_case_env.get_entry.assert_called_once_with(153483, "112@153483")
77
+
78
+ def test_get_entry_not_found(self, invoke: InvokeHelper, mock_case_env) -> None:
79
+ mock_case_env.get_entry.side_effect = ValueError("Entry '999@153483' not found in case 153483")
80
+ result = invoke(["case", "get-entry", "999@153483"])
81
+ assert result.exit_code == 1
82
+ assert "not found" in result.output
83
+
84
+ def test_get_entry_http_error(self, invoke: InvokeHelper, mock_case_env, make_http_error) -> None:
85
+ mock_case_env.get_entry.side_effect = make_http_error(400, text="Bad Request")
86
+ result = invoke(["case", "get-entry", "112@153483"])
87
+ assert result.exit_code == 1
88
+
89
+ def test_get_entry_missing_entry_id_argument(self, invoke: InvokeHelper, mock_case_env) -> None:
90
+ result = invoke(["case", "get-entry"])
91
+ assert result.exit_code == 2
92
+
93
+ def test_get_entry_malformed_entry_id(self, invoke: InvokeHelper, mock_case_env) -> None:
94
+ result = invoke(["case", "get-entry", "112"])
95
+ assert result.exit_code == 1
96
+ assert "invalid entry id" in result.output
97
+ mock_case_env.get_entry.assert_not_called()
98
+
99
+ def test_get_entry_non_numeric_entry_number(self, invoke: InvokeHelper, mock_case_env) -> None:
100
+ result = invoke(["case", "get-entry", "12INVALID21@1234"])
101
+ assert result.exit_code == 1
102
+ assert "invalid entry id" in result.output
103
+ mock_case_env.get_entry.assert_not_called()
104
+
105
+
106
+ class TestCaseGetEntries:
107
+ """Tests for ``case get-entries``."""
108
+
109
+ def test_get_entries_success(self, invoke: InvokeHelper, mock_case_env) -> None:
110
+ result = invoke(["case", "get-entries", "153483"])
111
+ assert result.exit_code == 0
112
+ mock_case_env.get_entries.assert_called_once_with(153483)
113
+
114
+ def test_get_entries_empty(self, invoke: InvokeHelper, mock_case_env) -> None:
115
+ mock_case_env.get_entries.return_value = []
116
+ result = invoke(["case", "get-entries", "153483"])
117
+ assert result.exit_code == 0
118
+ assert result.output.strip() == "[]"
119
+
120
+ def test_get_entries_http_error(self, invoke: InvokeHelper, mock_case_env, make_http_error) -> None:
121
+ mock_case_env.get_entries.side_effect = make_http_error(404, text="Not Found")
122
+ result = invoke(["case", "get-entries", "153483"])
123
+ assert result.exit_code == 1
@@ -115,6 +115,70 @@ class TestManifestValidate:
115
115
  assert mock_xsoar_env.client.packs.is_available.call_count == 2
116
116
 
117
117
 
118
+ class TestManifestUpdate:
119
+ """Tests for ``manifest update``."""
120
+
121
+ def test_skips_custom_pack_missing_from_artifacts(self, invoke: InvokeHelper, mock_xsoar_env, tmp_path: Path) -> None:
122
+ """Regression test: a custom pack installed on the server but missing from
123
+ the artifacts repo is surfaced in the warning and then skipped, without
124
+ crashing on an empty version list."""
125
+ from xsoar_cli.xsoar_client.packs import OutdatedResult
126
+
127
+ manifest = tmp_path / "xsoar_config.json"
128
+ manifest.write_text(
129
+ json.dumps(
130
+ {
131
+ "custom_packs": [{"id": "OrphanPack", "version": "1.0.0"}],
132
+ "marketplace_packs": [],
133
+ }
134
+ )
135
+ )
136
+ mock_xsoar_env.client.packs.get_outdated.return_value = OutdatedResult(
137
+ outdated=[],
138
+ skipped=["OrphanPack"],
139
+ )
140
+ # If get_latest_version is reached for the orphan pack, this would
141
+ # historically raise ValueError. The command must not call it.
142
+ mock_xsoar_env.client.artifact_provider.get_latest_version.return_value = None
143
+
144
+ result = invoke(["manifest", "update", str(manifest)])
145
+
146
+ assert result.exit_code == 0, result.output
147
+ assert "OrphanPack" in result.output
148
+ assert "installed but not found in artifacts repo" in result.output
149
+ mock_xsoar_env.client.artifact_provider.get_latest_version.assert_not_called()
150
+ # Manifest must remain unchanged.
151
+ assert json.loads(manifest.read_text()) == {
152
+ "custom_packs": [{"id": "OrphanPack", "version": "1.0.0"}],
153
+ "marketplace_packs": [],
154
+ }
155
+
156
+ def test_skips_when_provider_returns_none(self, invoke: InvokeHelper, mock_xsoar_env, tmp_path: Path) -> None:
157
+ """Defensive guard: even if a pack is not in the skipped list, a None from
158
+ the artifact provider is handled cleanly instead of raising."""
159
+ from xsoar_cli.xsoar_client.packs import OutdatedResult
160
+
161
+ manifest = tmp_path / "xsoar_config.json"
162
+ manifest.write_text(
163
+ json.dumps(
164
+ {
165
+ "custom_packs": [{"id": "MyOrg_EDR", "version": "1.0.0"}],
166
+ "marketplace_packs": [],
167
+ }
168
+ )
169
+ )
170
+ mock_xsoar_env.client.packs.get_outdated.return_value = OutdatedResult(
171
+ outdated=[],
172
+ skipped=[],
173
+ )
174
+ mock_xsoar_env.client.artifact_provider.get_latest_version.return_value = None
175
+
176
+ result = invoke(["manifest", "update", str(manifest)])
177
+
178
+ assert result.exit_code == 0, result.output
179
+ mock_xsoar_env.client.artifact_provider.get_latest_version.assert_called_once_with("MyOrg_EDR")
180
+
181
+
118
182
  class TestManifestValidateOnlyChanged:
119
183
  """Tests for ``manifest validate --only-changed``."""
120
184