diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b3e2e12..53c1ce5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +## v0.2.4 (2025-05-22) + +### ๐Ÿ›๐Ÿš‘๏ธ Fixes + +- **versioning**: minor version update + +### โœ…๐Ÿคก๐Ÿงช Tests + +- **testing**: increased coverage for testings up to 55% + +### ๐Ÿ“Œโž•โฌ‡๏ธโž–โฌ†๏ธ Dependencies + +- **commitizen**: minor fixes + ## v0.2.3 (2025-05-21) ### ๐Ÿ›๐Ÿš‘๏ธ Fixes diff --git a/hackagent/attacks/AdvPrefix/config.py b/hackagent/attacks/AdvPrefix/config.py index 58872318..c705d305 100644 --- a/hackagent/attacks/AdvPrefix/config.py +++ b/hackagent/attacks/AdvPrefix/config.py @@ -6,7 +6,7 @@ "output_dir": "./logs/runs", # --- Model Configurations --- "generator": { - "identifier": "ollama/llama2-uncensored", + "identifier": "hackagent/generate", "endpoint": "https://hackagent.dev/api/generate", "batch_size": 2, "max_new_tokens": 50, @@ -15,14 +15,14 @@ }, "judges": [ { - "identifier": "ollama/llama3", + "identifier": "hackagent/judge", "endpoint": "https://hackagent.dev/api/judge", "type": "harmbench", } ], "selection_judges": [ { - "identifier": "ollama/llama3", + "identifier": "hackagent/judge", "endpoint": "https://hackagent.dev/api/judge", "type": "harmbench", } diff --git a/hackagent/attacks/AdvPrefix/generate.py b/hackagent/attacks/AdvPrefix/generate.py index fb7ab4e3..a04e5aeb 100644 --- a/hackagent/attacks/AdvPrefix/generate.py +++ b/hackagent/attacks/AdvPrefix/generate.py @@ -55,30 +55,7 @@ def _construct_prompts( if n_samples <= 0: continue - # chat = [{"role": "user", "content": goal}] # Not directly used for router prompt format try: - # The prompt for the router will be the fully constructed context. - # Custom chat templating needs to happen before sending to router. - # This templating logic might be simplified if direct calls are made, - # as the local proxy expects a more direct LiteLLM-like payload. - - # For direct calls, the "prompt" is often just the user message content. - # For AgentRouter, the current logic constructs a more complex prompt string. - # We will adapt this based on whether we're calling directly or via router. - - # The `final_prompt` here is what's sent to LiteLLM or the router. - # For direct local proxy, `messages` will be constructed later. - # For AgentRouter, this `final_prompt` is used. - - # Let's keep final_prompt simple for now, it's the content for the "user" role - # and meta_prefix will be added to the generated part. - # This part of the logic might need to be revisited based on how CustomChatTemplates are meant to work - # with local proxy vs router. - - # The current _construct_prompts prepares a `final_prompt` string. - # Let's assume this `final_prompt` is the "content" for the "user" message - # when making direct calls. - if meta_prefix in CUSTOM_CHAT_TEMPLATES: prompt_content_for_template = CUSTOM_CHAT_TEMPLATES[ meta_prefix diff --git a/hackagent/attacks/AdvPrefix/scorer_parser.py b/hackagent/attacks/AdvPrefix/scorer_parser.py index 800508c5..69b1c1a3 100644 --- a/hackagent/attacks/AdvPrefix/scorer_parser.py +++ b/hackagent/attacks/AdvPrefix/scorer_parser.py @@ -77,46 +77,34 @@ def __init__(self, client: AuthenticatedClient, config: EvaluatorConfig): self.underlying_httpx_client = self.client.get_httpx_client() self.is_local_judge_proxy_defined = False - self.actual_api_key: Optional[str] = None + self.actual_api_key: str = client.token - if self.config.agent_endpoint and ( - "localhost:8888/api/judge" in self.config.agent_endpoint - or "127.0.0.1:8888/api/judge" in self.config.agent_endpoint - ): + api_key_config_value = self.config.agent_metadata.get("api_key") + + if api_key_config_value: + env_key_value = os.environ.get(api_key_config_value) + if env_key_value: + self.actual_api_key = env_key_value + self.logger.info( + f"Loaded API key for generator from environment variable: {api_key_config_value}" + ) + else: + self.actual_api_key = api_key_config_value + self.logger.info( + f"Using provided value directly as API key for generator (not found as env var: {api_key_config_value[:5]}...)." + ) + + print("config.agent_endpoint", self.config.agent_endpoint) + is_local_proxy_defined = bool( + self.config.agent_endpoint == "https://hackagent.dev/api/judge" + ) + + if is_local_proxy_defined: self.is_local_judge_proxy_defined = True self.logger.info( f"Local judge proxy detected for '{self.config.agent_name}' at: {self.config.agent_endpoint}" ) - if self.config.agent_metadata: - direct_api_key = self.config.agent_metadata.get("api_key") - api_key_env_var = self.config.agent_metadata.get("api_key_env_var") - - if direct_api_key: - self.actual_api_key = direct_api_key - self.logger.info( - f"Using direct API key for local judge proxy '{self.config.agent_name}'." - ) - elif api_key_env_var: - env_key_value = os.environ.get(api_key_env_var) - if env_key_value: - self.actual_api_key = env_key_value - self.logger.info( - f"Loaded API key for local judge proxy '{self.config.agent_name}' from env var: {api_key_env_var}" - ) - else: - self.logger.warning( - f"Env var {api_key_env_var} for local judge proxy '{self.config.agent_name}' API key not found." - ) - else: - self.logger.warning( - f"Local judge proxy '{self.config.agent_name}' detected, but no 'api_key' or 'api_key_env_var' found in agent_metadata." - ) - else: - self.logger.warning( - f"Local judge proxy '{self.config.agent_name}' detected, but agent_metadata is missing for API key." - ) - if not self.actual_api_key: self.is_local_judge_proxy_defined = ( False # Cannot use local proxy without API key @@ -497,8 +485,12 @@ def __init__(self, client: AuthenticatedClient, config: EvaluatorConfig): super().__init__(client, config) if not self.config.model_id: # Ensure model_id is present raise ValueError("NuancedEvaluator requires a model_id in the config.") - if not self.agent_router or not self.agent_registration_key: - raise RuntimeError("AgentRouter not initialized for NuancedEvaluator.") + if not (self.is_local_judge_proxy_defined and self.actual_api_key) and ( + not self.agent_router or not self.agent_registration_key + ): + raise RuntimeError( + "AgentRouter not initialized for NuancedEvaluator and local proxy not available/functional." + ) def _get_request_data_for_row(self, row: pd.Series) -> Dict[str, Any]: prompt_content = self.PROMPT.format( @@ -630,9 +622,11 @@ def __init__(self, client: AuthenticatedClient, config: EvaluatorConfig): raise ValueError( "JailbreakBenchEvaluator requires a model_id in the config." ) - if not self.agent_router or not self.agent_registration_key: + if not (self.is_local_judge_proxy_defined and self.actual_api_key) and ( + not self.agent_router or not self.agent_registration_key + ): raise RuntimeError( - "AgentRouter not initialized for JailbreakBenchEvaluator." + "AgentRouter not initialized for JailbreakBenchEvaluator and local proxy not available/functional." ) def _get_request_data_for_row(self, row: pd.Series) -> Dict[str, Any]: @@ -732,8 +726,12 @@ def __init__(self, client: AuthenticatedClient, config: EvaluatorConfig): ) # Depending on strictness, could raise ValueError here # raise ValueError("HarmBenchEvaluator requires a model_id in the config.") - if not self.agent_router or not self.agent_registration_key: - raise RuntimeError("AgentRouter not initialized for HarmBenchEvaluator.") + if not (self.is_local_judge_proxy_defined and self.actual_api_key) and ( + not self.agent_router or not self.agent_registration_key + ): + raise RuntimeError( + "AgentRouter not initialized for HarmBenchEvaluator and local proxy not available/functional." + ) # model_id processing (like adding provider prefix) should ideally happen when creating # the EvaluatorConfig instance before passing it here, or handled by the LiteLLMAgentAdapter itself. diff --git a/hackagent/logger.py b/hackagent/logger.py index 5b4c2a6f..b9b15b34 100644 --- a/hackagent/logger.py +++ b/hackagent/logger.py @@ -21,7 +21,7 @@ def setup_package_logging( - logger_name: str = "hackagent", default_level_str: str = "INFO" + logger_name: str = "hackagent", default_level_str: str = "WARNING" ): """Configures RichHandler for the specified logger if not already set.""" global _rich_handler_configured_for_package diff --git a/poetry.lock b/poetry.lock index 02966f7b..2641f905 100644 --- a/poetry.lock +++ b/poetry.lock @@ -478,14 +478,14 @@ markers = {main = "platform_system == \"Windows\""} [[package]] name = "commitizen" -version = "4.7.2" +version = "4.8.0" description = "Python commitizen client tool" optional = false python-versions = "<4.0,>=3.9" groups = ["dev"] files = [ - {file = "commitizen-4.7.2-py3-none-any.whl", hash = "sha256:eb3cd2da0690c6edbb8c11e586d3f07b09e9795424aa51f5413ed89827927623"}, - {file = "commitizen-4.7.2.tar.gz", hash = "sha256:e5ca28f909c3b3d28575b294561feeee21e33c864c7362babfe0e7798ae11aae"}, + {file = "commitizen-4.8.0-py3-none-any.whl", hash = "sha256:1cc1395010d3291b447c5d41f8201dde9cbe46e866f2a6b91ac4f599720f4516"}, + {file = "commitizen-4.8.0.tar.gz", hash = "sha256:488cb70afe18ff393a2ac18d32e82c3a18a81bf044dceeb33145ae5697ab832b"}, ] [package.dependencies] @@ -990,16 +990,16 @@ files = [ google-auth = ">=2.14.1,<3.0.0" googleapis-common-protos = ">=1.56.2,<2.0.0" grpcio = [ - {version = ">=1.33.2,<2.0dev", optional = true, markers = "extra == \"grpc\""}, {version = ">=1.49.1,<2.0dev", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}, + {version = ">=1.33.2,<2.0dev", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""}, ] grpcio-status = [ - {version = ">=1.33.2,<2.0.dev0", optional = true, markers = "extra == \"grpc\""}, {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}, + {version = ">=1.33.2,<2.0.dev0", optional = true, markers = "extra == \"grpc\""}, ] proto-plus = [ - {version = ">=1.22.3,<2.0.0"}, {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""}, + {version = ">=1.22.3,<2.0.0"}, ] protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" requests = ">=2.18.0,<3.0.0" @@ -1196,8 +1196,8 @@ google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0", extras google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0" grpc-google-iam-v1 = ">=0.14.0,<1.0.0" proto-plus = [ - {version = ">=1.22.3,<2.0.0"}, {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""}, + {version = ">=1.22.3,<2.0.0"}, ] protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" @@ -1218,8 +1218,8 @@ google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0", extras google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0" grpc-google-iam-v1 = ">=0.14.0,<1.0.0" proto-plus = [ - {version = ">=1.22.3,<2.0.0"}, {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""}, + {version = ">=1.22.3,<2.0.0"}, ] protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" @@ -1239,8 +1239,8 @@ files = [ google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0", extras = ["grpc"]} google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0" proto-plus = [ - {version = ">=1.22.3,<2.0.0"}, {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""}, + {version = ">=1.22.3,<2.0.0"}, ] protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" @@ -1284,8 +1284,8 @@ files = [ google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0", extras = ["grpc"]} google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0" proto-plus = [ - {version = ">=1.22.3,<2.0.0"}, {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""}, + {version = ">=1.22.3,<2.0.0"}, ] protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" @@ -1419,7 +1419,7 @@ description = "Lightweight in-process concurrent programming" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")" +markers = "(platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\") and python_version <= \"3.13\"" files = [ {file = "greenlet-3.2.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:c49e9f7c6f625507ed83a7485366b46cbe325717c60837f7244fc99ba16ba9d6"}, {file = "greenlet-3.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3cc1a3ed00ecfea8932477f729a9f616ad7347a5e55d50929efa50a86cb7be7"}, @@ -1912,14 +1912,14 @@ referencing = ">=0.31.0" [[package]] name = "litellm" -version = "1.70.0" +version = "1.70.2" description = "Library to easily interface with LLM API providers" optional = false python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8" groups = ["main"] files = [ - {file = "litellm-1.70.0-py3-none-any.whl", hash = "sha256:7e094057b38ddb1d77f61452895835aa5d376db1850e9a1bc0342c5631d89638"}, - {file = "litellm-1.70.0.tar.gz", hash = "sha256:357f3891e38f23a12f0932c235ed860dc41bc5880afaee7229e6d25318652706"}, + {file = "litellm-1.70.2-py3-none-any.whl", hash = "sha256:765bb4314e0f764735cb036dcfabfddfec84320831df17275a47d3bb48b577a3"}, + {file = "litellm-1.70.2.tar.gz", hash = "sha256:d2e45076f76d668f2b420c98067c9a992dfaa7fea3031a02d0ed89589a2f8841"}, ] [package.dependencies] @@ -1937,7 +1937,7 @@ tokenizers = "*" [package.extras] extra-proxy = ["azure-identity (>=1.15.0,<2.0.0)", "azure-keyvault-secrets (>=4.8.0,<5.0.0)", "google-cloud-kms (>=2.21.3,<3.0.0)", "prisma (==0.11.0)", "redisvl (>=0.4.1,<0.5.0) ; python_version >= \"3.9\" and python_version < \"3.14\"", "resend (>=0.8.0,<0.9.0)"] -proxy = ["PyJWT (>=2.8.0,<3.0.0)", "apscheduler (>=3.10.4,<4.0.0)", "backoff", "boto3 (==1.34.34)", "cryptography (>=43.0.1,<44.0.0)", "fastapi (>=0.115.5,<0.116.0)", "fastapi-sso (>=0.16.0,<0.17.0)", "gunicorn (>=23.0.0,<24.0.0)", "litellm-enterprise (==0.1.3)", "litellm-proxy-extras (==0.1.21)", "mcp (==1.5.0) ; python_version >= \"3.10\"", "orjson (>=3.9.7,<4.0.0)", "pynacl (>=1.5.0,<2.0.0)", "python-multipart (>=0.0.18,<0.0.19)", "pyyaml (>=6.0.1,<7.0.0)", "rich (==13.7.1)", "rq", "uvicorn (>=0.29.0,<0.30.0)", "uvloop (>=0.21.0,<0.22.0) ; sys_platform != \"win32\"", "websockets (>=13.1.0,<14.0.0)"] +proxy = ["PyJWT (>=2.8.0,<3.0.0)", "apscheduler (>=3.10.4,<4.0.0)", "backoff", "boto3 (==1.34.34)", "cryptography (>=43.0.1,<44.0.0)", "fastapi (>=0.115.5,<0.116.0)", "fastapi-sso (>=0.16.0,<0.17.0)", "gunicorn (>=23.0.0,<24.0.0)", "litellm-enterprise (==0.1.5)", "litellm-proxy-extras (==0.1.21)", "mcp (==1.5.0) ; python_version >= \"3.10\"", "orjson (>=3.9.7,<4.0.0)", "pynacl (>=1.5.0,<2.0.0)", "python-multipart (>=0.0.18,<0.0.19)", "pyyaml (>=6.0.1,<7.0.0)", "rich (==13.7.1)", "rq", "uvicorn (>=0.29.0,<0.30.0)", "uvloop (>=0.21.0,<0.22.0) ; sys_platform != \"win32\"", "websockets (>=13.1.0,<14.0.0)"] utils = ["numpydoc"] [[package]] @@ -2473,9 +2473,9 @@ files = [ [package.dependencies] numpy = [ - {version = ">=1.22.4", markers = "python_version < \"3.11\""}, - {version = ">=1.23.2", markers = "python_version == \"3.11\""}, {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, + {version = ">=1.23.2", markers = "python_version == \"3.11\""}, + {version = ">=1.22.4", markers = "python_version < \"3.11\""}, ] python-dateutil = ">=2.8.2" pytz = ">=2020.1" @@ -2971,6 +2971,25 @@ tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-asyncio" +version = "0.23.8" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2"}, + {file = "pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3"}, +] + +[package.dependencies] +pytest = ">=7.0.0,<9" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + [[package]] name = "pytest-cov" version = "6.1.1" @@ -3450,7 +3469,7 @@ description = "C version of reader, parser and emitter for ruamel.yaml derived f optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "platform_python_implementation == \"CPython\" and python_version <= \"3.12\"" +markers = "platform_python_implementation == \"CPython\" and python_version < \"3.13\"" files = [ {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:11f891336688faf5156a36293a9c362bdc7c88f03a8a027c2c1d8e0bcde998e5"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:a606ef75a60ecf3d924613892cc603b154178ee25abb3055db5062da811fd969"}, @@ -4398,4 +4417,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "bba8aac441c039816f5cb9cbd6a19575be2c82605a8f384df21de6a454285129" +content-hash = "ccdba301dcced613019cbf721ebf41fd19de9fd21717ec24b0484a5b7070bcb4" diff --git a/pyproject.toml b/pyproject.toml index 9fdb176a..373c096e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "hackagent" -version = "0.2.3" +version = "0.2.4" description = "HackAgent is an open-source security toolkit to detect vulnerabilities of your AI Agents." authors = [ "Nicola Franco ", @@ -30,6 +30,8 @@ commitizen = "^4.7.1" cz-conventional-gitmoji = "^0.7.0" pytest-cov = "^6.1.1" google-adk = "^0.5.0" +anyio = "^4.3.0" +pytest-asyncio = "^0.23.7" [tool.commitizen] name = "cz_gitmoji" @@ -60,4 +62,6 @@ exclude_lines = [ ] [tool.coverage.xml] -output = "reports/coverage.xml" \ No newline at end of file +output = "reports/coverage.xml" + +[tool.pytest.ini_options] \ No newline at end of file diff --git a/tests/unit/api/test_apilogs.py b/tests/unit/api/test_apilogs.py new file mode 100644 index 00000000..0b77b303 --- /dev/null +++ b/tests/unit/api/test_apilogs.py @@ -0,0 +1,430 @@ +import pytest +from httpx import Response +from unittest.mock import MagicMock, patch, AsyncMock + +from hackagent.api.apilogs import apilogs_list, apilogs_retrieve +from hackagent.models.paginated_api_token_log_list import PaginatedAPITokenLogList +from hackagent.models.api_token_log import APITokenLog +from hackagent.client import AuthenticatedClient +from hackagent.errors import UnexpectedStatus + + +@pytest.fixture +def authenticated_client() -> AuthenticatedClient: + return AuthenticatedClient(base_url="http://localhost:8000", token="test_token") + + +def test_get_kwargs(authenticated_client: AuthenticatedClient): + kwargs = apilogs_list._get_kwargs(page=1) + assert kwargs["method"] == "get" + assert kwargs["url"] == "/api/apilogs" + assert kwargs["params"]["page"] == 1 + + kwargs_no_page = apilogs_list._get_kwargs() + assert "page" not in kwargs_no_page["params"] + + +def test_sync_detailed_success(authenticated_client: AuthenticatedClient): + mock_response_data = { + "count": 1, + "next": None, + "previous": None, + "results": [ + { + "id": "test_id_sync_list", + "timestamp": "2024-01-01T00:00:00Z", + "api_key_prefix": "sync_list_pref", + "user_username": "sync_list_user", + "organization_name": "sync_list_org", + "model_id_used": "gpt-4-sync-list", + "api_endpoint": "generator_sync_list", + "input_tokens": 10, + "output_tokens": 20, + "credits_deducted": "0.001", + "request_payload_preview": "Request sync list", + "response_payload_preview": "Response sync list", + } + ], + } + mock_response = Response(200, json=mock_response_data) + + with patch.object( + authenticated_client.get_httpx_client(), "request", return_value=mock_response + ) as mock_request: + response = apilogs_list.sync_detailed(client=authenticated_client, page=1) + + mock_request.assert_called_once_with( + method="get", url="/api/apilogs", params={"page": 1} + ) + assert response.status_code == 200 + assert isinstance(response.parsed, PaginatedAPITokenLogList) + assert response.parsed.count == 1 + assert response.parsed.results[0].id == "test_id_sync_list" + + +def test_sync_detailed_unexpected_status(authenticated_client: AuthenticatedClient): + mock_response = Response(500, content=b"Internal Server Error") + authenticated_client.raise_on_unexpected_status = True + + with patch.object( + authenticated_client.get_httpx_client(), "request", return_value=mock_response + ) as mock_request: + with pytest.raises(UnexpectedStatus) as excinfo: + apilogs_list.sync_detailed(client=authenticated_client, page=1) + + mock_request.assert_called_once_with( + method="get", url="/api/apilogs", params={"page": 1} + ) + assert excinfo.value.status_code == 500 + assert excinfo.value.content == b"Internal Server Error" + + +def test_sync_detailed_no_raise_unexpected_status( + authenticated_client: AuthenticatedClient, +): + mock_response = Response(500, content=b"Internal Server Error") + authenticated_client.raise_on_unexpected_status = False + + with patch.object( + authenticated_client.get_httpx_client(), "request", return_value=mock_response + ) as mock_request: + response = apilogs_list.sync_detailed(client=authenticated_client, page=1) + + mock_request.assert_called_once_with( + method="get", url="/api/apilogs", params={"page": 1} + ) + assert response.status_code == 500 + assert response.parsed is None + + +def test_sync_success(authenticated_client: AuthenticatedClient): + mock_parsed_response = MagicMock(spec=PaginatedAPITokenLogList) + mock_detailed_response = MagicMock() + mock_detailed_response.parsed = mock_parsed_response + + with patch( + "hackagent.api.apilogs.apilogs_list.sync_detailed", + return_value=mock_detailed_response, + ) as mock_sync_detailed: + parsed = apilogs_list.sync(client=authenticated_client, page=1) + + mock_sync_detailed.assert_called_once_with(client=authenticated_client, page=1) + assert parsed == mock_parsed_response + + +@pytest.mark.asyncio +async def test_asyncio_detailed_success(authenticated_client: AuthenticatedClient): + mock_response_data = { + "count": 1, + "next": None, + "previous": None, + "results": [ + { + "id": "test_id_async_list", + "timestamp": "2024-01-01T00:02:00Z", + "api_key_prefix": "async_list_pref", + "user_username": "async_list_user", + "organization_name": "async_list_org", + "model_id_used": "gpt-3.5-turbo-async-list", + "api_endpoint": "apilogs_list_endpoint_async", + "input_tokens": 15, + "output_tokens": 25, + "credits_deducted": "0.0015", + "request_payload_preview": "Async list request new", + "response_payload_preview": "Async list response new", + } + ], + } + mock_http_response = Response(200, json=mock_response_data) + mock_async_httpx_client = AsyncMock() + mock_async_httpx_client.request = AsyncMock(return_value=mock_http_response) + + with patch.object( + AuthenticatedClient, + "get_async_httpx_client", + autospec=True, + return_value=mock_async_httpx_client, + ) as mock_get_client_method: + response = await apilogs_list.asyncio_detailed( + client=authenticated_client, page=1 + ) + + mock_get_client_method.assert_called_once_with(authenticated_client) + mock_async_httpx_client.request.assert_called_once_with( + method="get", url="/api/apilogs", params={"page": 1} + ) + assert response.status_code == 200 + assert isinstance(response.parsed, PaginatedAPITokenLogList) + assert response.parsed.count == 1 + assert response.parsed.results[0].id == "test_id_async_list" + + +@pytest.mark.asyncio +async def test_asyncio_detailed_unexpected_status( + authenticated_client: AuthenticatedClient, +): + mock_http_response = Response(500, content=b"Internal Server Error") + authenticated_client.raise_on_unexpected_status = True + mock_async_httpx_client = AsyncMock() + mock_async_httpx_client.request = AsyncMock(return_value=mock_http_response) + + with patch.object( + AuthenticatedClient, + "get_async_httpx_client", + autospec=True, + return_value=mock_async_httpx_client, + ) as mock_get_client_method: + with pytest.raises(UnexpectedStatus) as excinfo: + await apilogs_list.asyncio_detailed(client=authenticated_client, page=1) + + mock_get_client_method.assert_called_once_with(authenticated_client) + mock_async_httpx_client.request.assert_called_once_with( + method="get", url="/api/apilogs", params={"page": 1} + ) + assert excinfo.value.status_code == 500 + assert excinfo.value.content == b"Internal Server Error" + + +@pytest.mark.asyncio +async def test_asyncio_detailed_no_raise_unexpected_status( + authenticated_client: AuthenticatedClient, +): + mock_http_response = Response(500, content=b"Internal Server Error") + authenticated_client.raise_on_unexpected_status = False + mock_async_httpx_client = AsyncMock() + mock_async_httpx_client.request = AsyncMock(return_value=mock_http_response) + + with patch.object( + AuthenticatedClient, + "get_async_httpx_client", + autospec=True, + return_value=mock_async_httpx_client, + ) as mock_get_client_method: + response = await apilogs_list.asyncio_detailed( + client=authenticated_client, page=1 + ) + + mock_get_client_method.assert_called_once_with(authenticated_client) + mock_async_httpx_client.request.assert_called_once_with( + method="get", url="/api/apilogs", params={"page": 1} + ) + assert response.status_code == 500 + assert response.parsed is None + + +@pytest.mark.asyncio +async def test_asyncio_success(authenticated_client: AuthenticatedClient): + mock_parsed_response = MagicMock(spec=PaginatedAPITokenLogList) + mock_detailed_response = MagicMock() + mock_detailed_response.parsed = mock_parsed_response + + # Need to mock the awaitable + async def mock_asyncio_detailed(*args, **kwargs): + return mock_detailed_response + + with patch( + "hackagent.api.apilogs.apilogs_list.asyncio_detailed", new=mock_asyncio_detailed + ) as _: + parsed = await apilogs_list.asyncio(client=authenticated_client, page=1) + + # mock_async_detailed_patch.assert_called_once_with(client=authenticated_client, page=1) + assert parsed == mock_parsed_response + + +# Tests for apilogs_retrieve + + +def test_retrieve_get_kwargs(): + kwargs = apilogs_retrieve._get_kwargs(id=123) + assert kwargs["method"] == "get" + assert kwargs["url"] == "/api/apilogs/123" + + +def test_retrieve_sync_detailed_success(authenticated_client: AuthenticatedClient): + mock_response_data = { + "id": "test_log_id_sync_retrieve", + "timestamp": "2024-01-01T00:01:00Z", + "api_key_prefix": "sync_retrieve_pref", + "user_username": "sync_retrieve_user", + "organization_name": "sync_retrieve_org", + "model_id_used": "gemini-pro-sync-retrieve", + "api_endpoint": "agent_sync_retrieve", + "input_tokens": 12, + "output_tokens": 22, + "credits_deducted": "0.0012", + "request_payload_preview": "Sync retrieve request new", + "response_payload_preview": "Sync retrieve response new", + } + mock_response = Response(200, json=mock_response_data) + + with patch.object( + authenticated_client.get_httpx_client(), "request", return_value=mock_response + ) as mock_request: + response = apilogs_retrieve.sync_detailed(client=authenticated_client, id=123) + + mock_request.assert_called_once_with(method="get", url="/api/apilogs/123") + assert response.status_code == 200 + assert isinstance(response.parsed, APITokenLog) + assert response.parsed.id == "test_log_id_sync_retrieve" + + +def test_retrieve_sync_detailed_unexpected_status( + authenticated_client: AuthenticatedClient, +): + mock_response = Response(404, content=b"Not Found") + authenticated_client.raise_on_unexpected_status = True + + with patch.object( + authenticated_client.get_httpx_client(), "request", return_value=mock_response + ) as mock_request: + with pytest.raises(UnexpectedStatus) as excinfo: + apilogs_retrieve.sync_detailed(client=authenticated_client, id=123) + + mock_request.assert_called_once_with(method="get", url="/api/apilogs/123") + assert excinfo.value.status_code == 404 + assert excinfo.value.content == b"Not Found" + + +def test_retrieve_sync_detailed_no_raise_unexpected_status( + authenticated_client: AuthenticatedClient, +): + mock_response = Response(404, content=b"Not Found") + authenticated_client.raise_on_unexpected_status = False + + with patch.object( + authenticated_client.get_httpx_client(), "request", return_value=mock_response + ) as mock_request: + response = apilogs_retrieve.sync_detailed(client=authenticated_client, id=123) + + mock_request.assert_called_once_with(method="get", url="/api/apilogs/123") + assert response.status_code == 404 + assert response.parsed is None + + +def test_retrieve_sync_success(authenticated_client: AuthenticatedClient): + mock_parsed_response = MagicMock(spec=APITokenLog) + mock_detailed_response = MagicMock() + mock_detailed_response.parsed = mock_parsed_response + + with patch( + "hackagent.api.apilogs.apilogs_retrieve.sync_detailed", + return_value=mock_detailed_response, + ) as mock_sync_detailed: + parsed = apilogs_retrieve.sync(client=authenticated_client, id=123) + + mock_sync_detailed.assert_called_once_with(client=authenticated_client, id=123) + assert parsed == mock_parsed_response + + +@pytest.mark.asyncio +async def test_retrieve_asyncio_detailed_success( + authenticated_client: AuthenticatedClient, +): + mock_response_data = { + "id": "test_log_id_async_retrieve", + "timestamp": "2024-01-01T00:03:00Z", + "api_key_prefix": "async_retrieve_pref", + "user_username": "async_retrieve_user", + "organization_name": "async_retrieve_org", + "model_id_used": "command-r-async-retrieve", + "api_endpoint": "apilogs_retrieve_endpoint_async", + "input_tokens": 8, + "output_tokens": 18, + "credits_deducted": "0.0008", + "request_payload_preview": "Async retrieve request new", + "response_payload_preview": "Async retrieve response new", + } + mock_http_response = Response(200, json=mock_response_data) + mock_async_httpx_client = AsyncMock() + mock_async_httpx_client.request = AsyncMock(return_value=mock_http_response) + + with patch.object( + AuthenticatedClient, + "get_async_httpx_client", + autospec=True, + return_value=mock_async_httpx_client, + ) as mock_get_client_method: + response = await apilogs_retrieve.asyncio_detailed( + client=authenticated_client, id=123 + ) + + mock_get_client_method.assert_called_once_with(authenticated_client) + mock_async_httpx_client.request.assert_called_once_with( + method="get", url="/api/apilogs/123" + ) + assert response.status_code == 200 + assert isinstance(response.parsed, APITokenLog) + assert response.parsed.id == "test_log_id_async_retrieve" + + +@pytest.mark.asyncio +async def test_retrieve_asyncio_detailed_unexpected_status( + authenticated_client: AuthenticatedClient, +): + mock_http_response = Response(401, content=b"Unauthorized") + authenticated_client.raise_on_unexpected_status = True + mock_async_httpx_client = AsyncMock() + mock_async_httpx_client.request = AsyncMock(return_value=mock_http_response) + + with patch.object( + AuthenticatedClient, + "get_async_httpx_client", + autospec=True, + return_value=mock_async_httpx_client, + ) as mock_get_client_method: + with pytest.raises(UnexpectedStatus) as excinfo: + await apilogs_retrieve.asyncio_detailed(client=authenticated_client, id=123) + + mock_get_client_method.assert_called_once_with(authenticated_client) + mock_async_httpx_client.request.assert_called_once_with( + method="get", url="/api/apilogs/123" + ) + assert excinfo.value.status_code == 401 + assert excinfo.value.content == b"Unauthorized" + + +@pytest.mark.asyncio +async def test_retrieve_asyncio_detailed_no_raise_unexpected_status( + authenticated_client: AuthenticatedClient, +): + mock_http_response = Response(403, content=b"Forbidden") + authenticated_client.raise_on_unexpected_status = False + mock_async_httpx_client = AsyncMock() + mock_async_httpx_client.request = AsyncMock(return_value=mock_http_response) + + with patch.object( + AuthenticatedClient, + "get_async_httpx_client", + autospec=True, + return_value=mock_async_httpx_client, + ) as mock_get_client_method: + response = await apilogs_retrieve.asyncio_detailed( + client=authenticated_client, id=123 + ) + + mock_get_client_method.assert_called_once_with(authenticated_client) + mock_async_httpx_client.request.assert_called_once_with( + method="get", url="/api/apilogs/123" + ) + assert response.status_code == 403 + assert response.parsed is None + + +@pytest.mark.asyncio +async def test_retrieve_asyncio_success(authenticated_client: AuthenticatedClient): + mock_parsed_response = MagicMock(spec=APITokenLog) + mock_detailed_response = MagicMock() + mock_detailed_response.parsed = mock_parsed_response + + # Need to mock the awaitable + async def mock_asyncio_detailed(*args, **kwargs): + return mock_detailed_response + + with patch( + "hackagent.api.apilogs.apilogs_retrieve.asyncio_detailed", + new=mock_asyncio_detailed, + ) as _: + parsed = await apilogs_retrieve.asyncio(client=authenticated_client, id=123) + + # mock_async_detailed_patch.assert_called_once_with(client=authenticated_client, id=123) + assert parsed == mock_parsed_response diff --git a/tests/unit/api/test_checkout.py b/tests/unit/api/test_checkout.py new file mode 100644 index 00000000..87a6a832 --- /dev/null +++ b/tests/unit/api/test_checkout.py @@ -0,0 +1,258 @@ +import pytest +from httpx import Response +from unittest.mock import MagicMock, patch, AsyncMock + +from hackagent.api.checkout import checkout_create +from hackagent.models.checkout_session_request_request import ( + CheckoutSessionRequestRequest, +) +from hackagent.models.checkout_session_response import CheckoutSessionResponse +from hackagent.models.generic_error_response import GenericErrorResponse +from hackagent.client import AuthenticatedClient +from hackagent.errors import UnexpectedStatus + + +@pytest.fixture +def authenticated_client() -> AuthenticatedClient: + return AuthenticatedClient(base_url="http://localhost:8000", token="test_token") + + +@pytest.fixture +def checkout_request_body() -> CheckoutSessionRequestRequest: + body = CheckoutSessionRequestRequest(credits_to_purchase=100) + body.additional_properties["price_id"] = "price_123" + body.additional_properties["success_url"] = "http://success.com" + body.additional_properties["cancel_url"] = "http://cancel.com" + return body + + +def test_get_kwargs(checkout_request_body: CheckoutSessionRequestRequest): + # Note: The generated _get_kwargs has triplicate isinstance checks for the same type. + # We are testing the multipart case as CheckoutSessionRequestRequest has to_multipart. + kwargs = checkout_create._get_kwargs(body=checkout_request_body) + assert kwargs["method"] == "post" + assert kwargs["url"] == "/api/checkout/" + assert kwargs["files"] == checkout_request_body.to_multipart() + assert "multipart/form-data" in kwargs["headers"]["Content-Type"] + + # Test with a different instance to ensure to_multipart is called on the passed body + different_body = CheckoutSessionRequestRequest(credits_to_purchase=50) + different_body.additional_properties["price_id"] = "price_456" + + kwargs_different = checkout_create._get_kwargs(body=different_body) + assert kwargs_different["files"] == different_body.to_multipart() + + +def test_parse_response_success(authenticated_client: AuthenticatedClient): + mock_response_data = {"checkout_url": "http://stripe.com/checkout/sess_123"} + http_response = Response(200, json=mock_response_data) + parsed = checkout_create._parse_response( + client=authenticated_client, response=http_response + ) + assert isinstance(parsed, CheckoutSessionResponse) + assert parsed.checkout_url == "http://stripe.com/checkout/sess_123" + + +@pytest.mark.parametrize( + "status_code, error_message", + [ + (400, "Bad Request Error"), + (404, "Not Found Error"), + (500, "Internal Server Error Message"), + ], +) +def test_parse_response_error( + authenticated_client: AuthenticatedClient, status_code: int, error_message: str +): + mock_response_data = { + "error": error_message, + "details": "More details about the error", + } + http_response = Response(status_code, json=mock_response_data) + parsed = checkout_create._parse_response( + client=authenticated_client, response=http_response + ) + assert isinstance(parsed, GenericErrorResponse) + assert parsed.error == error_message + assert parsed.details == "More details about the error" + + +def test_parse_response_unexpected_status_raise( + authenticated_client: AuthenticatedClient, +): + http_response = Response(503, content=b"Service Unavailable") + authenticated_client.raise_on_unexpected_status = True + with pytest.raises(UnexpectedStatus) as excinfo: + checkout_create._parse_response( + client=authenticated_client, response=http_response + ) + assert excinfo.value.status_code == 503 + assert excinfo.value.content == b"Service Unavailable" + + +def test_parse_response_unexpected_status_no_raise( + authenticated_client: AuthenticatedClient, +): + http_response = Response(503, content=b"Service Unavailable") + authenticated_client.raise_on_unexpected_status = False + parsed = checkout_create._parse_response( + client=authenticated_client, response=http_response + ) + assert parsed is None + + +def test_sync_detailed_success( + authenticated_client: AuthenticatedClient, + checkout_request_body: CheckoutSessionRequestRequest, +): + mock_response_data = {"checkout_url": "http://stripe.com/checkout/sess_abc"} + mock_http_response = Response(200, json=mock_response_data) + + with patch.object( + authenticated_client.get_httpx_client(), + "request", + return_value=mock_http_response, + ) as mock_request: + response = checkout_create.sync_detailed( + client=authenticated_client, body=checkout_request_body + ) + + expected_kwargs = checkout_create._get_kwargs(body=checkout_request_body) + mock_request.assert_called_once_with(**expected_kwargs) + assert response.status_code == 200 + assert isinstance(response.parsed, CheckoutSessionResponse) + assert response.parsed.checkout_url == "http://stripe.com/checkout/sess_abc" + + +@pytest.mark.parametrize("status_code", [400, 404, 500]) +def test_sync_detailed_error_responses( + authenticated_client: AuthenticatedClient, + checkout_request_body: CheckoutSessionRequestRequest, + status_code: int, +): + mock_response_data = { + "error": "Sync Error Occurred", + "details": "Sync error details", + } + mock_http_response = Response(status_code, json=mock_response_data) + + with patch.object( + authenticated_client.get_httpx_client(), + "request", + return_value=mock_http_response, + ) as mock_request: + response = checkout_create.sync_detailed( + client=authenticated_client, body=checkout_request_body + ) + + expected_kwargs = checkout_create._get_kwargs(body=checkout_request_body) + mock_request.assert_called_once_with(**expected_kwargs) + assert response.status_code == status_code + assert isinstance(response.parsed, GenericErrorResponse) + assert response.parsed.error == "Sync Error Occurred" + + +def test_sync_success( + authenticated_client: AuthenticatedClient, + checkout_request_body: CheckoutSessionRequestRequest, +): + mock_parsed_response = MagicMock(spec=CheckoutSessionResponse) + mock_detailed_response = MagicMock() + mock_detailed_response.parsed = mock_parsed_response + + with patch( + "hackagent.api.checkout.checkout_create.sync_detailed", + return_value=mock_detailed_response, + ) as mock_sync_detailed: + parsed = checkout_create.sync( + client=authenticated_client, body=checkout_request_body + ) + + mock_sync_detailed.assert_called_once_with( + client=authenticated_client, body=checkout_request_body + ) + assert parsed == mock_parsed_response + + +@pytest.mark.asyncio +async def test_asyncio_detailed_success( + authenticated_client: AuthenticatedClient, + checkout_request_body: CheckoutSessionRequestRequest, +): + mock_response_data = {"checkout_url": "http://stripe.com/checkout/sess_def"} + mock_http_response = Response(200, json=mock_response_data) + mock_async_httpx_client = AsyncMock() + mock_async_httpx_client.request = AsyncMock(return_value=mock_http_response) + + with patch.object( + AuthenticatedClient, + "get_async_httpx_client", + autospec=True, + return_value=mock_async_httpx_client, + ) as mock_get_client_method: + response = await checkout_create.asyncio_detailed( + client=authenticated_client, body=checkout_request_body + ) + + expected_kwargs = checkout_create._get_kwargs(body=checkout_request_body) + mock_get_client_method.assert_called_once_with(authenticated_client) + mock_async_httpx_client.request.assert_called_once_with(**expected_kwargs) + assert response.status_code == 200 + assert isinstance(response.parsed, CheckoutSessionResponse) + assert response.parsed.checkout_url == "http://stripe.com/checkout/sess_def" + + +@pytest.mark.parametrize("status_code", [400, 404, 500]) +@pytest.mark.asyncio +async def test_asyncio_detailed_error_responses( + authenticated_client: AuthenticatedClient, + checkout_request_body: CheckoutSessionRequestRequest, + status_code: int, +): + mock_response_data = { + "error": "Async Detailed Error", + "details": "Async detailed error details", + } + mock_http_response = Response(status_code, json=mock_response_data) + mock_async_httpx_client = AsyncMock() + mock_async_httpx_client.request = AsyncMock(return_value=mock_http_response) + + with patch.object( + AuthenticatedClient, + "get_async_httpx_client", + autospec=True, + return_value=mock_async_httpx_client, + ) as mock_get_client_method: + response = await checkout_create.asyncio_detailed( + client=authenticated_client, body=checkout_request_body + ) + + expected_kwargs = checkout_create._get_kwargs(body=checkout_request_body) + mock_get_client_method.assert_called_once_with(authenticated_client) + mock_async_httpx_client.request.assert_called_once_with(**expected_kwargs) + assert response.status_code == status_code + assert isinstance(response.parsed, GenericErrorResponse) + assert response.parsed.error == "Async Detailed Error" + + +@pytest.mark.asyncio +async def test_asyncio_success( + authenticated_client: AuthenticatedClient, + checkout_request_body: CheckoutSessionRequestRequest, +): + mock_parsed_response = MagicMock(spec=CheckoutSessionResponse) + mock_detailed_response = MagicMock() + mock_detailed_response.parsed = mock_parsed_response + + async def mock_asyncio_detailed(*args, **kwargs): + return mock_detailed_response + + with patch( + "hackagent.api.checkout.checkout_create.asyncio_detailed", + new=mock_asyncio_detailed, + ) as _: + parsed = await checkout_create.asyncio( + client=authenticated_client, body=checkout_request_body + ) + + assert parsed == mock_parsed_response diff --git a/tests/unit/api/test_organization.py b/tests/unit/api/test_organization.py new file mode 100644 index 00000000..8fbd4c3c --- /dev/null +++ b/tests/unit/api/test_organization.py @@ -0,0 +1,1080 @@ +import pytest +from httpx import Response +from unittest.mock import MagicMock, patch, AsyncMock +import datetime +import uuid + +from hackagent.api.organization import ( + organization_create, + organization_destroy, + organization_list, + organization_me_retrieve, + organization_partial_update, + organization_retrieve, + organization_update, +) +from hackagent.models.organization_request import OrganizationRequest +from hackagent.models.organization import Organization +from hackagent.client import AuthenticatedClient +from hackagent.errors import UnexpectedStatus +from hackagent.types import UNSET +from hackagent.models.paginated_organization_list import PaginatedOrganizationList +from hackagent.models.patched_organization_request import PatchedOrganizationRequest + + +@pytest.fixture +def authenticated_client() -> AuthenticatedClient: + return AuthenticatedClient(base_url="http://localhost:8000", token="test_token") + + +@pytest.fixture +def organization_request_body() -> OrganizationRequest: + body = OrganizationRequest(name="Test Org") + body.additional_properties["company"] = "Test Inc." + body.additional_properties["website"] = "http://test.org" + return body + + +TEST_ORG_REAL_UUID_STR = "123e4567-e89b-12d3-a456-426614174000" # A valid UUID string + + +@pytest.fixture +def organization_response_data() -> dict: + return { + "id": TEST_ORG_REAL_UUID_STR, # Use a valid UUID string + "name": "Test Org", + "owner": { + "id": "user_abc", + "username": "owner_user", + "picture": None, + "credits": 0, + "created_at": "2023-01-01T00:00:00Z", + "updated_at": "2023-01-01T00:00:00Z", + }, + "credits": "1000.00", # Changed from int to string as per model: credits_ (str) + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z", + "credits_last_updated": "2024-01-01T00:00:00Z", # Added missing field + "company": "Test Inc.", + "website": "http://test.org", + "github_url": None, + } + + +def test_create_get_kwargs(organization_request_body: OrganizationRequest): + # Similar to checkout, _get_kwargs has triplicate isinstance checks. + # Testing the multipart case as OrganizationRequest has to_multipart. + kwargs = organization_create._get_kwargs(body=organization_request_body) + assert kwargs["method"] == "post" + assert kwargs["url"] == "/api/organization" + assert kwargs["files"] == organization_request_body.to_multipart() + assert "multipart/form-data" in kwargs["headers"]["Content-Type"] + + +def test_create_parse_response_success( + authenticated_client: AuthenticatedClient, organization_response_data: dict +): + http_response = Response(201, json=organization_response_data) + parsed = organization_create._parse_response( + client=authenticated_client, response=http_response + ) + assert isinstance(parsed, Organization) + assert parsed.id == uuid.UUID(TEST_ORG_REAL_UUID_STR) + assert parsed.name == "Test Org" + assert parsed.additional_properties["owner"]["id"] == "user_abc" + + +def test_create_parse_response_unexpected_status_raise( + authenticated_client: AuthenticatedClient, +): + http_response = Response(500, content=b"Server Error") + authenticated_client.raise_on_unexpected_status = True + with pytest.raises(UnexpectedStatus) as excinfo: + organization_create._parse_response( + client=authenticated_client, response=http_response + ) + assert excinfo.value.status_code == 500 + assert excinfo.value.content == b"Server Error" + + +def test_create_parse_response_unexpected_status_no_raise( + authenticated_client: AuthenticatedClient, +): + http_response = Response(403, content=b"Forbidden") + authenticated_client.raise_on_unexpected_status = False + parsed = organization_create._parse_response( + client=authenticated_client, response=http_response + ) + assert parsed is None + + +def test_create_sync_detailed_success( + authenticated_client: AuthenticatedClient, + organization_request_body: OrganizationRequest, + organization_response_data: dict, +): + mock_http_response = Response(201, json=organization_response_data) + with patch.object( + authenticated_client.get_httpx_client(), + "request", + return_value=mock_http_response, + ) as mock_request: + response = organization_create.sync_detailed( + client=authenticated_client, body=organization_request_body + ) + + expected_kwargs = organization_create._get_kwargs(body=organization_request_body) + mock_request.assert_called_once_with(**expected_kwargs) + assert response.status_code == 201 + assert isinstance(response.parsed, Organization) + assert response.parsed.id == uuid.UUID(TEST_ORG_REAL_UUID_STR) + + +def test_create_sync_success( + authenticated_client: AuthenticatedClient, + organization_request_body: OrganizationRequest, +): + mock_parsed_response = MagicMock(spec=Organization) + mock_detailed_response = MagicMock() + mock_detailed_response.parsed = mock_parsed_response + + with patch( + "hackagent.api.organization.organization_create.sync_detailed", + return_value=mock_detailed_response, + ) as mock_sync_detailed: + parsed = organization_create.sync( + client=authenticated_client, body=organization_request_body + ) + + mock_sync_detailed.assert_called_once_with( + client=authenticated_client, body=organization_request_body + ) + assert parsed == mock_parsed_response + + +@pytest.mark.asyncio +async def test_create_asyncio_detailed_success( + authenticated_client: AuthenticatedClient, + organization_request_body: OrganizationRequest, + organization_response_data: dict, +): + mock_http_response = Response(201, json=organization_response_data) + mock_async_httpx_client = AsyncMock() + mock_async_httpx_client.request = AsyncMock(return_value=mock_http_response) + + with patch.object( + AuthenticatedClient, + "get_async_httpx_client", + autospec=True, + return_value=mock_async_httpx_client, + ) as mock_get_client_method: + response = await organization_create.asyncio_detailed( + client=authenticated_client, body=organization_request_body + ) + + expected_kwargs = organization_create._get_kwargs(body=organization_request_body) + mock_get_client_method.assert_called_once_with(authenticated_client) + mock_async_httpx_client.request.assert_called_once_with(**expected_kwargs) + assert response.status_code == 201 + assert isinstance(response.parsed, Organization) + assert response.parsed.name == "Test Org" + + +@pytest.mark.asyncio +async def test_create_asyncio_success( + authenticated_client: AuthenticatedClient, + organization_request_body: OrganizationRequest, +): + mock_parsed_response = MagicMock(spec=Organization) + mock_detailed_response = MagicMock() + mock_detailed_response.parsed = mock_parsed_response + + async def mock_asyncio_detailed_func(*args, **kwargs): + return mock_detailed_response + + with patch( + "hackagent.api.organization.organization_create.asyncio_detailed", + new=mock_asyncio_detailed_func, + ) as _: + parsed = await organization_create.asyncio( + client=authenticated_client, body=organization_request_body + ) + + assert parsed == mock_parsed_response + + +# --- Tests for organization_destroy --- + +TEST_ORG_UUID = uuid.uuid4() + + +def test_destroy_get_kwargs(): + kwargs = organization_destroy._get_kwargs(id=TEST_ORG_UUID) + assert kwargs["method"] == "delete" + assert kwargs["url"] == f"/api/organization/{TEST_ORG_UUID}" + + +def test_destroy_parse_response_success(authenticated_client: AuthenticatedClient): + http_response = Response(204) # No content for successful delete + parsed = organization_destroy._parse_response( + client=authenticated_client, response=http_response + ) + assert parsed is None + + +def test_destroy_parse_response_unexpected_status_raise( + authenticated_client: AuthenticatedClient, +): + http_response = Response(404, content=b"Not Found") + authenticated_client.raise_on_unexpected_status = True + with pytest.raises(UnexpectedStatus) as excinfo: + organization_destroy._parse_response( + client=authenticated_client, response=http_response + ) + assert excinfo.value.status_code == 404 + assert excinfo.value.content == b"Not Found" + + +def test_destroy_parse_response_unexpected_status_no_raise( + authenticated_client: AuthenticatedClient, +): + http_response = Response(401, content=b"Unauthorized") + authenticated_client.raise_on_unexpected_status = False + parsed = organization_destroy._parse_response( + client=authenticated_client, response=http_response + ) + assert parsed is None # Should still be None as per _parse_response logic + + +def test_destroy_sync_detailed_success(authenticated_client: AuthenticatedClient): + mock_http_response = Response(204) + with patch.object( + authenticated_client.get_httpx_client(), + "request", + return_value=mock_http_response, + ) as mock_request: + response = organization_destroy.sync_detailed( + client=authenticated_client, id=TEST_ORG_UUID + ) + + expected_kwargs = organization_destroy._get_kwargs(id=TEST_ORG_UUID) + mock_request.assert_called_once_with(**expected_kwargs) + assert response.status_code == 204 + assert response.parsed is None + + +@pytest.mark.asyncio +async def test_destroy_asyncio_detailed_success( + authenticated_client: AuthenticatedClient, +): + mock_http_response = Response(204) + mock_async_httpx_client = AsyncMock() + mock_async_httpx_client.request = AsyncMock(return_value=mock_http_response) + + with patch.object( + AuthenticatedClient, + "get_async_httpx_client", + autospec=True, + return_value=mock_async_httpx_client, + ) as mock_get_client_method: + response = await organization_destroy.asyncio_detailed( + client=authenticated_client, id=TEST_ORG_UUID + ) + + expected_kwargs = organization_destroy._get_kwargs(id=TEST_ORG_UUID) + mock_get_client_method.assert_called_once_with(authenticated_client) + mock_async_httpx_client.request.assert_called_once_with(**expected_kwargs) + assert response.status_code == 204 + assert response.parsed is None + + +# --- Tests for organization_list --- + + +@pytest.fixture +def paginated_organization_list_response_data(organization_response_data: dict) -> dict: + return { + "count": 1, + "next": "http://localhost:8000/api/organization?page=2", + "previous": None, + "results": [organization_response_data], + } + + +def test_list_get_kwargs(): + kwargs = organization_list._get_kwargs(page=1) + assert kwargs["method"] == "get" + assert kwargs["url"] == "/api/organization" + assert kwargs["params"]["page"] == 1 + + kwargs_no_page = organization_list._get_kwargs() + assert "page" not in kwargs_no_page["params"] + + +def test_list_parse_response_success( + authenticated_client: AuthenticatedClient, + paginated_organization_list_response_data: dict, +): + http_response = Response(200, json=paginated_organization_list_response_data) + parsed = organization_list._parse_response( + client=authenticated_client, response=http_response + ) + assert isinstance(parsed, PaginatedOrganizationList) + assert parsed.count == 1 + assert parsed.next_ == "http://localhost:8000/api/organization?page=2" + assert len(parsed.results) == 1 + assert parsed.results[0].id == uuid.UUID(TEST_ORG_REAL_UUID_STR) + + +def test_list_parse_response_unexpected_status_raise( + authenticated_client: AuthenticatedClient, +): + http_response = Response(401, content=b"Unauthorized") + authenticated_client.raise_on_unexpected_status = True + with pytest.raises(UnexpectedStatus) as excinfo: + organization_list._parse_response( + client=authenticated_client, response=http_response + ) + assert excinfo.value.status_code == 401 + assert excinfo.value.content == b"Unauthorized" + + +def test_list_parse_response_unexpected_status_no_raise( + authenticated_client: AuthenticatedClient, +): + http_response = Response(403, content=b"Forbidden") + authenticated_client.raise_on_unexpected_status = False + parsed = organization_list._parse_response( + client=authenticated_client, response=http_response + ) + assert parsed is None + + +def test_list_sync_detailed_success( + authenticated_client: AuthenticatedClient, + paginated_organization_list_response_data: dict, +): + mock_http_response = Response(200, json=paginated_organization_list_response_data) + with patch.object( + authenticated_client.get_httpx_client(), + "request", + return_value=mock_http_response, + ) as mock_request: + response = organization_list.sync_detailed(client=authenticated_client, page=1) + + expected_kwargs = organization_list._get_kwargs(page=1) + mock_request.assert_called_once_with(**expected_kwargs) + assert response.status_code == 200 + assert isinstance(response.parsed, PaginatedOrganizationList) + assert response.parsed.count == 1 + + +def test_list_sync_success(authenticated_client: AuthenticatedClient): + mock_parsed_response = MagicMock(spec=PaginatedOrganizationList) + mock_detailed_response = MagicMock() + mock_detailed_response.parsed = mock_parsed_response + + with patch( + "hackagent.api.organization.organization_list.sync_detailed", + return_value=mock_detailed_response, + ) as mock_sync_detailed: + parsed = organization_list.sync(client=authenticated_client, page=1) + + mock_sync_detailed.assert_called_once_with(client=authenticated_client, page=1) + assert parsed == mock_parsed_response + + +@pytest.mark.asyncio +async def test_list_asyncio_detailed_success( + authenticated_client: AuthenticatedClient, + paginated_organization_list_response_data: dict, +): + mock_http_response = Response(200, json=paginated_organization_list_response_data) + mock_async_httpx_client = AsyncMock() + mock_async_httpx_client.request = AsyncMock(return_value=mock_http_response) + + with patch.object( + AuthenticatedClient, + "get_async_httpx_client", + autospec=True, + return_value=mock_async_httpx_client, + ) as mock_get_client_method: + response = await organization_list.asyncio_detailed( + client=authenticated_client, page=1 + ) + + expected_kwargs = organization_list._get_kwargs(page=1) + mock_get_client_method.assert_called_once_with(authenticated_client) + mock_async_httpx_client.request.assert_called_once_with(**expected_kwargs) + assert response.status_code == 200 + assert isinstance(response.parsed, PaginatedOrganizationList) + assert len(response.parsed.results) == 1 + + +@pytest.mark.asyncio +async def test_list_asyncio_success(authenticated_client: AuthenticatedClient): + mock_parsed_response = MagicMock(spec=PaginatedOrganizationList) + mock_detailed_response = MagicMock() + mock_detailed_response.parsed = mock_parsed_response + + async def mock_asyncio_detailed_func(*args, **kwargs): + return mock_detailed_response + + with patch( + "hackagent.api.organization.organization_list.asyncio_detailed", + new=mock_asyncio_detailed_func, + ) as _: + parsed = await organization_list.asyncio(client=authenticated_client, page=1) + + assert parsed == mock_parsed_response + + +# --- Tests for organization_me_retrieve --- + + +def test_me_retrieve_get_kwargs(): + kwargs = organization_me_retrieve._get_kwargs() + assert kwargs["method"] == "get" + assert kwargs["url"] == "/api/organization/me" + + +def test_me_retrieve_parse_response_success( + authenticated_client: AuthenticatedClient, organization_response_data: dict +): + # Re-use organization_response_data fixture for this + http_response = Response(200, json=organization_response_data) + parsed = organization_me_retrieve._parse_response( + client=authenticated_client, response=http_response + ) + assert isinstance(parsed, Organization) + assert parsed.id == uuid.UUID(TEST_ORG_REAL_UUID_STR) + assert parsed.name == "Test Org" + + +def test_me_retrieve_parse_response_unexpected_status_raise( + authenticated_client: AuthenticatedClient, +): + http_response = Response(404, content=b"Not Found") + authenticated_client.raise_on_unexpected_status = True + with pytest.raises(UnexpectedStatus) as excinfo: + organization_me_retrieve._parse_response( + client=authenticated_client, response=http_response + ) + assert excinfo.value.status_code == 404 + assert excinfo.value.content == b"Not Found" + + +def test_me_retrieve_parse_response_unexpected_status_no_raise( + authenticated_client: AuthenticatedClient, +): + http_response = Response(401, content=b"Unauthorized") + authenticated_client.raise_on_unexpected_status = False + parsed = organization_me_retrieve._parse_response( + client=authenticated_client, response=http_response + ) + assert parsed is None + + +def test_me_retrieve_sync_detailed_success( + authenticated_client: AuthenticatedClient, organization_response_data: dict +): + mock_http_response = Response(200, json=organization_response_data) + with patch.object( + authenticated_client.get_httpx_client(), + "request", + return_value=mock_http_response, + ) as mock_request: + response = organization_me_retrieve.sync_detailed(client=authenticated_client) + + expected_kwargs = organization_me_retrieve._get_kwargs() + mock_request.assert_called_once_with(**expected_kwargs) + assert response.status_code == 200 + assert isinstance(response.parsed, Organization) + assert response.parsed.id == uuid.UUID(TEST_ORG_REAL_UUID_STR) + + +def test_me_retrieve_sync_success( + authenticated_client: AuthenticatedClient, organization_response_data: dict +): + # For sync, we need to mock sync_detailed to return a response with a parsed attribute + mock_organization = Organization.from_dict(organization_response_data) + mock_detailed_response = MagicMock() + mock_detailed_response.parsed = mock_organization + + with patch( + "hackagent.api.organization.organization_me_retrieve.sync_detailed", + return_value=mock_detailed_response, + ) as mock_sync_detailed: + parsed = organization_me_retrieve.sync(client=authenticated_client) + + mock_sync_detailed.assert_called_once_with(client=authenticated_client) + assert parsed == mock_organization + assert parsed.name == "Test Org" + + +@pytest.mark.asyncio +async def test_me_retrieve_asyncio_detailed_success( + authenticated_client: AuthenticatedClient, organization_response_data: dict +): + mock_http_response = Response(200, json=organization_response_data) + mock_async_httpx_client = AsyncMock() + mock_async_httpx_client.request = AsyncMock(return_value=mock_http_response) + + with patch.object( + AuthenticatedClient, + "get_async_httpx_client", + autospec=True, + return_value=mock_async_httpx_client, + ) as mock_get_client_method: + response = await organization_me_retrieve.asyncio_detailed( + client=authenticated_client + ) + + expected_kwargs = organization_me_retrieve._get_kwargs() + mock_get_client_method.assert_called_once_with(authenticated_client) + mock_async_httpx_client.request.assert_called_once_with(**expected_kwargs) + assert response.status_code == 200 + assert isinstance(response.parsed, Organization) + assert response.parsed.id == uuid.UUID(TEST_ORG_REAL_UUID_STR) + + +@pytest.mark.asyncio +async def test_me_retrieve_asyncio_success( + authenticated_client: AuthenticatedClient, organization_response_data: dict +): + mock_organization = Organization.from_dict(organization_response_data) + mock_detailed_response = MagicMock() + mock_detailed_response.parsed = mock_organization + + async def mock_asyncio_detailed_func(*args, **kwargs): + return mock_detailed_response + + with patch( + "hackagent.api.organization.organization_me_retrieve.asyncio_detailed", + new=mock_asyncio_detailed_func, + ) as _: + parsed = await organization_me_retrieve.asyncio(client=authenticated_client) + + assert parsed == mock_organization + assert parsed.additional_properties["company"] == "Test Inc." + + +# --- Tests for organization_partial_update --- + + +@pytest.fixture +def patched_organization_request_body() -> PatchedOrganizationRequest: + body = PatchedOrganizationRequest(name="Updated Test Org") + body.additional_properties["website"] = "http://updated.org" + return body + + +@pytest.fixture +def updated_organization_response_data(organization_response_data: dict) -> dict: + # Simulate an update to the original data + updated_data = organization_response_data.copy() + updated_data["name"] = "Updated Test Org" + updated_data["website"] = "http://updated.org" + updated_data["updated_at"] = ( + datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(seconds=1) + ).isoformat() + updated_data["credits_last_updated"] = ( + datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(seconds=1) + ).isoformat() # Ensure this is also updated or present + return updated_data + + +def test_partial_update_get_kwargs( + patched_organization_request_body: PatchedOrganizationRequest, +): + kwargs = organization_partial_update._get_kwargs( + id=TEST_ORG_UUID, body=patched_organization_request_body + ) + assert kwargs["method"] == "patch" + assert kwargs["url"] == f"/api/organization/{TEST_ORG_UUID}" + assert kwargs["files"] == patched_organization_request_body.to_multipart() + assert "multipart/form-data" in kwargs["headers"]["Content-Type"] + + +def test_partial_update_parse_response_success( + authenticated_client: AuthenticatedClient, updated_organization_response_data: dict +): + http_response = Response(200, json=updated_organization_response_data) + parsed = organization_partial_update._parse_response( + client=authenticated_client, response=http_response + ) + assert isinstance(parsed, Organization) + assert parsed.id == uuid.UUID(updated_organization_response_data["id"]) + assert parsed.name == "Updated Test Org" + assert parsed.additional_properties["website"] == "http://updated.org" + + +def test_partial_update_parse_response_unexpected_status_raise( + authenticated_client: AuthenticatedClient, +): + http_response = Response(400, content=b"Bad Request") + authenticated_client.raise_on_unexpected_status = True + with pytest.raises(UnexpectedStatus) as excinfo: + organization_partial_update._parse_response( + client=authenticated_client, response=http_response + ) + assert excinfo.value.status_code == 400 + assert excinfo.value.content == b"Bad Request" + + +def test_partial_update_parse_response_unexpected_status_no_raise( + authenticated_client: AuthenticatedClient, +): + http_response = Response(404, content=b"Not Found") + authenticated_client.raise_on_unexpected_status = False + parsed = organization_partial_update._parse_response( + client=authenticated_client, response=http_response + ) + assert parsed is None + + +def test_partial_update_sync_detailed_success( + authenticated_client: AuthenticatedClient, + patched_organization_request_body: PatchedOrganizationRequest, + updated_organization_response_data: dict, +): + mock_http_response = Response(200, json=updated_organization_response_data) + with patch.object( + authenticated_client.get_httpx_client(), + "request", + return_value=mock_http_response, + ) as mock_request: + response = organization_partial_update.sync_detailed( + client=authenticated_client, + id=TEST_ORG_UUID, + body=patched_organization_request_body, + ) + + expected_kwargs = organization_partial_update._get_kwargs( + id=TEST_ORG_UUID, body=patched_organization_request_body + ) + mock_request.assert_called_once_with(**expected_kwargs) + assert response.status_code == 200 + assert isinstance(response.parsed, Organization) + assert response.parsed.name == "Updated Test Org" + + +def test_partial_update_sync_success( + authenticated_client: AuthenticatedClient, + patched_organization_request_body: PatchedOrganizationRequest, + updated_organization_response_data: dict, +): + mock_organization = Organization.from_dict(updated_organization_response_data) + mock_detailed_response = MagicMock() + mock_detailed_response.parsed = mock_organization + + with patch( + "hackagent.api.organization.organization_partial_update.sync_detailed", + return_value=mock_detailed_response, + ) as mock_sync_detailed: + parsed = organization_partial_update.sync( + client=authenticated_client, + id=TEST_ORG_UUID, + body=patched_organization_request_body, + ) + + mock_sync_detailed.assert_called_once_with( + client=authenticated_client, + id=TEST_ORG_UUID, + body=patched_organization_request_body, + ) + assert parsed == mock_organization + assert parsed.additional_properties["website"] == "http://updated.org" + + +@pytest.mark.asyncio +async def test_partial_update_asyncio_detailed_success( + authenticated_client: AuthenticatedClient, + patched_organization_request_body: PatchedOrganizationRequest, + updated_organization_response_data: dict, +): + mock_http_response = Response(200, json=updated_organization_response_data) + mock_async_httpx_client = AsyncMock() + mock_async_httpx_client.request = AsyncMock(return_value=mock_http_response) + + with patch.object( + AuthenticatedClient, + "get_async_httpx_client", + autospec=True, + return_value=mock_async_httpx_client, + ) as mock_get_client_method: + response = await organization_partial_update.asyncio_detailed( + client=authenticated_client, + id=TEST_ORG_UUID, + body=patched_organization_request_body, + ) + + expected_kwargs = organization_partial_update._get_kwargs( + id=TEST_ORG_UUID, body=patched_organization_request_body + ) + mock_get_client_method.assert_called_once_with(authenticated_client) + mock_async_httpx_client.request.assert_called_once_with(**expected_kwargs) + assert response.status_code == 200 + assert isinstance(response.parsed, Organization) + assert response.parsed.name == "Updated Test Org" + assert response.parsed.additional_properties["website"] == "http://updated.org" + + +@pytest.mark.asyncio +async def test_partial_update_asyncio_success( + authenticated_client: AuthenticatedClient, + patched_organization_request_body: PatchedOrganizationRequest, + updated_organization_response_data: dict, +): + mock_organization = Organization.from_dict(updated_organization_response_data) + mock_detailed_response = MagicMock() + mock_detailed_response.parsed = mock_organization + + async def mock_asyncio_detailed_func(*args, **kwargs): + return mock_detailed_response + + with patch( + "hackagent.api.organization.organization_partial_update.asyncio_detailed", + new=mock_asyncio_detailed_func, + ) as _: + parsed = await organization_partial_update.asyncio( + client=authenticated_client, + id=TEST_ORG_UUID, + body=patched_organization_request_body, + ) + + assert parsed == mock_organization + assert parsed.additional_properties["website"] == "http://updated.org" + + +# --- Tests for organization_retrieve --- + + +def test_retrieve_get_kwargs_specific_org(): + kwargs = organization_retrieve._get_kwargs(id=TEST_ORG_UUID) + assert kwargs["method"] == "get" + assert kwargs["url"] == f"/api/organization/{TEST_ORG_UUID}" + + +def test_retrieve_parse_response_success_specific_org( + authenticated_client: AuthenticatedClient, organization_response_data: dict +): + http_response = Response(200, json=organization_response_data) + parsed = organization_retrieve._parse_response( + client=authenticated_client, response=http_response + ) + assert isinstance(parsed, Organization) + assert parsed.id == uuid.UUID(TEST_ORG_REAL_UUID_STR) + assert parsed.name == "Test Org" + + +def test_retrieve_parse_response_unexpected_status_raise_specific_org( + authenticated_client: AuthenticatedClient, +): + http_response = Response(404, content=b"Not Found") + authenticated_client.raise_on_unexpected_status = True + with pytest.raises(UnexpectedStatus) as excinfo: + organization_retrieve._parse_response( + client=authenticated_client, response=http_response + ) + assert excinfo.value.status_code == 404 + assert excinfo.value.content == b"Not Found" + + +def test_retrieve_parse_response_unexpected_status_no_raise_specific_org( + authenticated_client: AuthenticatedClient, +): + http_response = Response(403, content=b"Forbidden") + authenticated_client.raise_on_unexpected_status = False + parsed = organization_retrieve._parse_response( + client=authenticated_client, response=http_response + ) + assert parsed is None + + +def test_retrieve_sync_detailed_success_specific_org( + authenticated_client: AuthenticatedClient, organization_response_data: dict +): + mock_http_response = Response(200, json=organization_response_data) + with patch.object( + authenticated_client.get_httpx_client(), + "request", + return_value=mock_http_response, + ) as mock_request: + response = organization_retrieve.sync_detailed( + client=authenticated_client, id=TEST_ORG_UUID + ) + + expected_kwargs = organization_retrieve._get_kwargs(id=TEST_ORG_UUID) + mock_request.assert_called_once_with(**expected_kwargs) + assert response.status_code == 200 + assert isinstance(response.parsed, Organization) + assert response.parsed.id == uuid.UUID(TEST_ORG_REAL_UUID_STR) + + +def test_retrieve_sync_success_specific_org( + authenticated_client: AuthenticatedClient, organization_response_data: dict +): + mock_organization = Organization.from_dict(organization_response_data) + mock_detailed_response = MagicMock() + mock_detailed_response.parsed = mock_organization + + with patch( + "hackagent.api.organization.organization_retrieve.sync_detailed", + return_value=mock_detailed_response, + ) as mock_sync_detailed: + parsed = organization_retrieve.sync( + client=authenticated_client, id=TEST_ORG_UUID + ) + + mock_sync_detailed.assert_called_once_with( + client=authenticated_client, id=TEST_ORG_UUID + ) + assert parsed == mock_organization + assert parsed.name == "Test Org" + + +@pytest.mark.asyncio +async def test_retrieve_asyncio_detailed_success_specific_org( + authenticated_client: AuthenticatedClient, organization_response_data: dict +): + mock_http_response = Response(200, json=organization_response_data) + mock_async_httpx_client = AsyncMock() + mock_async_httpx_client.request = AsyncMock(return_value=mock_http_response) + + with patch.object( + AuthenticatedClient, + "get_async_httpx_client", + autospec=True, + return_value=mock_async_httpx_client, + ) as mock_get_client_method: + response = await organization_retrieve.asyncio_detailed( + client=authenticated_client, id=TEST_ORG_UUID + ) + + expected_kwargs = organization_retrieve._get_kwargs(id=TEST_ORG_UUID) + mock_get_client_method.assert_called_once_with(authenticated_client) + mock_async_httpx_client.request.assert_called_once_with(**expected_kwargs) + assert response.status_code == 200 + assert isinstance(response.parsed, Organization) + assert response.parsed.id == uuid.UUID(TEST_ORG_REAL_UUID_STR) + assert response.parsed.name == "Test Org" + + +@pytest.mark.asyncio +async def test_retrieve_asyncio_success_specific_org( + authenticated_client: AuthenticatedClient, organization_response_data: dict +): + mock_organization = Organization.from_dict(organization_response_data) + mock_detailed_response = MagicMock() + mock_detailed_response.parsed = mock_organization + + async def mock_asyncio_detailed_func(*args, **kwargs): + return mock_detailed_response + + with patch( + "hackagent.api.organization.organization_retrieve.asyncio_detailed", + new=mock_asyncio_detailed_func, + ) as _: + parsed = await organization_retrieve.asyncio( + client=authenticated_client, id=TEST_ORG_UUID + ) + + assert parsed == mock_organization + assert parsed.name == "Test Org" + + +# --- Tests for organization_update --- + + +@pytest.fixture +def full_updated_organization_response_data( + organization_request_body: OrganizationRequest, organization_response_data: dict +) -> dict: + updated_data = organization_response_data.copy() + updated_data["name"] = organization_request_body.name + updated_data["company"] = organization_request_body.additional_properties.get( + "company" + ) + updated_data["website"] = organization_request_body.additional_properties.get( + "website" + ) + requested_github_url = organization_request_body.additional_properties.get( + "github_url", UNSET + ) + updated_data["github_url"] = ( + None if requested_github_url is UNSET else requested_github_url + ) + current_time = datetime.datetime.now(datetime.timezone.utc) + updated_data["updated_at"] = ( + current_time + datetime.timedelta(seconds=2) + ).isoformat() + updated_data["credits_last_updated"] = ( + current_time + datetime.timedelta(seconds=2) + ).isoformat() # Add/update this field + # Ensure 'credits' field is present, matching what Organization model expects (string) + if "credits" not in updated_data: + updated_data["credits"] = ( + "1000.00" # Default or carry over, ensure it's a string + ) + return updated_data + + +def test_update_get_kwargs(organization_request_body: OrganizationRequest): + kwargs = organization_update._get_kwargs( + id=TEST_ORG_UUID, body=organization_request_body + ) + assert kwargs["method"] == "put" + assert kwargs["url"] == f"/api/organization/{TEST_ORG_UUID}" + assert kwargs["files"] == organization_request_body.to_multipart() + assert "multipart/form-data" in kwargs["headers"]["Content-Type"] + + +def test_update_parse_response_success( + authenticated_client: AuthenticatedClient, + full_updated_organization_response_data: dict, +): + http_response = Response(200, json=full_updated_organization_response_data) + parsed = organization_update._parse_response( + client=authenticated_client, response=http_response + ) + assert isinstance(parsed, Organization) + assert parsed.id == uuid.UUID(TEST_ORG_REAL_UUID_STR) + assert parsed.name == "Test Org" + assert parsed.additional_properties["company"] == "Test Inc." + assert parsed.additional_properties["website"] == "http://test.org" + + +def test_update_parse_response_unexpected_status_raise( + authenticated_client: AuthenticatedClient, +): + http_response = Response(400, content=b"Invalid Data") + authenticated_client.raise_on_unexpected_status = True + with pytest.raises(UnexpectedStatus) as excinfo: + organization_update._parse_response( + client=authenticated_client, response=http_response + ) + assert excinfo.value.status_code == 400 + assert excinfo.value.content == b"Invalid Data" + + +def test_update_parse_response_unexpected_status_no_raise( + authenticated_client: AuthenticatedClient, +): + http_response = Response(500, content=b"Server Error") + authenticated_client.raise_on_unexpected_status = False + parsed = organization_update._parse_response( + client=authenticated_client, response=http_response + ) + assert parsed is None + + +def test_update_sync_detailed_success( + authenticated_client: AuthenticatedClient, + organization_request_body: OrganizationRequest, + full_updated_organization_response_data: dict, +): + mock_http_response = Response(200, json=full_updated_organization_response_data) + with patch.object( + authenticated_client.get_httpx_client(), + "request", + return_value=mock_http_response, + ) as mock_request: + response = organization_update.sync_detailed( + client=authenticated_client, + id=TEST_ORG_UUID, + body=organization_request_body, + ) + + expected_kwargs = organization_update._get_kwargs( + id=TEST_ORG_UUID, body=organization_request_body + ) + mock_request.assert_called_once_with(**expected_kwargs) + assert response.status_code == 200 + assert isinstance(response.parsed, Organization) + assert response.parsed.name == "Test Org" + + +def test_update_sync_success( + authenticated_client: AuthenticatedClient, + organization_request_body: OrganizationRequest, + full_updated_organization_response_data: dict, +): + mock_organization = Organization.from_dict(full_updated_organization_response_data) + mock_detailed_response = MagicMock() + mock_detailed_response.parsed = mock_organization + + with patch( + "hackagent.api.organization.organization_update.sync_detailed", + return_value=mock_detailed_response, + ) as mock_sync_detailed: + parsed = organization_update.sync( + client=authenticated_client, + id=TEST_ORG_UUID, + body=organization_request_body, + ) + + mock_sync_detailed.assert_called_once_with( + client=authenticated_client, id=TEST_ORG_UUID, body=organization_request_body + ) + assert parsed == mock_organization + assert parsed.additional_properties["company"] == "Test Inc." + + +@pytest.mark.asyncio +async def test_update_asyncio_detailed_success( + authenticated_client: AuthenticatedClient, + organization_request_body: OrganizationRequest, + full_updated_organization_response_data: dict, +): + mock_http_response = Response(200, json=full_updated_organization_response_data) + mock_async_httpx_client = AsyncMock() + mock_async_httpx_client.request = AsyncMock(return_value=mock_http_response) + + with patch.object( + AuthenticatedClient, + "get_async_httpx_client", + autospec=True, + return_value=mock_async_httpx_client, + ) as mock_get_client_method: + response = await organization_update.asyncio_detailed( + client=authenticated_client, + id=TEST_ORG_UUID, + body=organization_request_body, + ) + + expected_kwargs = organization_update._get_kwargs( + id=TEST_ORG_UUID, body=organization_request_body + ) + mock_get_client_method.assert_called_once_with(authenticated_client) + mock_async_httpx_client.request.assert_called_once_with(**expected_kwargs) + assert response.status_code == 200 + assert isinstance(response.parsed, Organization) + assert response.parsed.name == "Test Org" + + +@pytest.mark.asyncio +async def test_update_asyncio_success( + authenticated_client: AuthenticatedClient, + organization_request_body: OrganizationRequest, + full_updated_organization_response_data: dict, +): + mock_organization = Organization.from_dict(full_updated_organization_response_data) + mock_detailed_response = MagicMock() + mock_detailed_response.parsed = mock_organization + + async def mock_asyncio_detailed_func(*args, **kwargs): + return mock_detailed_response + + with patch( + "hackagent.api.organization.organization_update.asyncio_detailed", + new=mock_asyncio_detailed_func, + ) as _: + parsed = await organization_update.asyncio( + client=authenticated_client, + id=TEST_ORG_UUID, + body=organization_request_body, + ) + + assert parsed == mock_organization + assert parsed.additional_properties["company"] == "Test Inc."