aixtools 0.1.8__py3-none-any.whl → 0.1.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of aixtools might be problematic. Click here for more details.
- aixtools/_version.py +2 -2
- aixtools/compliance/private_data.py +136 -0
- aixtools/server/__init__.py +6 -0
- aixtools/server/workspace_privacy.py +65 -0
- aixtools/vault/vault.py +40 -3
- {aixtools-0.1.8.dist-info → aixtools-0.1.9.dist-info}/METADATA +1 -1
- {aixtools-0.1.8.dist-info → aixtools-0.1.9.dist-info}/RECORD +15 -11
- docker/mcp-base/Dockerfile +7 -10
- docker/mcp-base/zscaler.crt +28 -0
- scripts/test.sh +13 -6
- tests/unit/compliance/test_private_data.py +329 -0
- tests/unit/vault/test_vault.py +75 -0
- {aixtools-0.1.8.dist-info → aixtools-0.1.9.dist-info}/WHEEL +0 -0
- {aixtools-0.1.8.dist-info → aixtools-0.1.9.dist-info}/entry_points.txt +0 -0
- {aixtools-0.1.8.dist-info → aixtools-0.1.9.dist-info}/top_level.txt +0 -0
aixtools/_version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.1.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 1,
|
|
31
|
+
__version__ = version = '0.1.9'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 1, 9)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from fastmcp import Context
|
|
5
|
+
|
|
6
|
+
from aixtools.server.path import get_workspace_path
|
|
7
|
+
|
|
8
|
+
PRIVATE_DATA_FILE = ".private_data"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PrivateData:
|
|
12
|
+
"""
|
|
13
|
+
Class to manage private data file in the workspace.
|
|
14
|
+
|
|
15
|
+
The information is stored in a JSON file named `.private_data` within the workspace directory.
|
|
16
|
+
If the file does not exist, it indicates that there is no private data.
|
|
17
|
+
|
|
18
|
+
IMPORTANT: All modifications save the data to the file immediately.
|
|
19
|
+
|
|
20
|
+
FIXME: We should add some level of mutex/locking to prevent concurrent writes.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, ctx: Context | None = None):
|
|
24
|
+
self.ctx: Context | None = ctx
|
|
25
|
+
self._has_private_data: bool = False # Flag indicating if private data exists
|
|
26
|
+
self._private_datasets: list[str] = [] # List of private datasets
|
|
27
|
+
self._idap_datasets: list[str] = [] # List of dataset with IDAP
|
|
28
|
+
self.load()
|
|
29
|
+
|
|
30
|
+
def add_private_dataset(self, dataset_name: str) -> None:
|
|
31
|
+
"""
|
|
32
|
+
Add a private dataset to the list.
|
|
33
|
+
Save the state after modification.
|
|
34
|
+
"""
|
|
35
|
+
if dataset_name not in self._private_datasets:
|
|
36
|
+
self._private_datasets.append(dataset_name)
|
|
37
|
+
self._has_private_data = True
|
|
38
|
+
self.save()
|
|
39
|
+
|
|
40
|
+
def add_idap_dataset(self, dataset_name: str) -> None:
|
|
41
|
+
"""
|
|
42
|
+
Add a dataset with IDAP to the list.
|
|
43
|
+
This also adds it to the private datasets if not already present.
|
|
44
|
+
Save the state after modification.
|
|
45
|
+
"""
|
|
46
|
+
if not self.has_idap_dataset(dataset_name):
|
|
47
|
+
self._idap_datasets.append(dataset_name)
|
|
48
|
+
self._has_private_data = True
|
|
49
|
+
# An IDAP dataset is also a private dataset
|
|
50
|
+
if not self.has_private_dataset(dataset_name):
|
|
51
|
+
self._private_datasets.append(dataset_name)
|
|
52
|
+
self.save()
|
|
53
|
+
|
|
54
|
+
def get_private_datasets(self) -> list[str]:
|
|
55
|
+
"""Get the list of private datasets as a copy (to avoid modification)."""
|
|
56
|
+
return list(self._private_datasets)
|
|
57
|
+
|
|
58
|
+
def get_idap_datasets(self) -> list[str]:
|
|
59
|
+
"""Get the list of datasets with IDAP as a copy (to avoid modification)."""
|
|
60
|
+
return list(self._idap_datasets)
|
|
61
|
+
|
|
62
|
+
def has_private_dataset(self, dataset_name: str) -> bool:
|
|
63
|
+
"""Check if a specific private dataset exists."""
|
|
64
|
+
return dataset_name in self._private_datasets
|
|
65
|
+
|
|
66
|
+
def has_idap_dataset(self, dataset_name: str) -> bool:
|
|
67
|
+
"""Check if a specific dataset with IDAP exists."""
|
|
68
|
+
return dataset_name in self._idap_datasets
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def has_private_data(self) -> bool:
|
|
72
|
+
"""Check if private data exists."""
|
|
73
|
+
return self._has_private_data
|
|
74
|
+
|
|
75
|
+
@has_private_data.setter
|
|
76
|
+
def has_private_data(self, value: bool) -> None:
|
|
77
|
+
"""
|
|
78
|
+
Set the flag indicating if private data exists.
|
|
79
|
+
Save the state after modification.
|
|
80
|
+
"""
|
|
81
|
+
self._has_private_data = value
|
|
82
|
+
if not value:
|
|
83
|
+
self._private_datasets = []
|
|
84
|
+
self._idap_datasets = []
|
|
85
|
+
self.save()
|
|
86
|
+
|
|
87
|
+
def _get_private_data_path(self) -> Path:
|
|
88
|
+
"""Get the path to the private data file in the workspace."""
|
|
89
|
+
return get_workspace_path(service_name=None, ctx=self.ctx) / PRIVATE_DATA_FILE
|
|
90
|
+
|
|
91
|
+
def _has_private_data_file(self) -> bool:
|
|
92
|
+
"""Check if the private data file exists in the workspace."""
|
|
93
|
+
private_data_path = self.get_private_data_path() # type: ignore
|
|
94
|
+
return private_data_path.exists()
|
|
95
|
+
|
|
96
|
+
def save(self) -> None:
|
|
97
|
+
"""Save content to the private data file in the workspace."""
|
|
98
|
+
private_data_path = self._get_private_data_path()
|
|
99
|
+
# No private data? Delete the file if it exists
|
|
100
|
+
if not self.has_private_data:
|
|
101
|
+
private_data_path.unlink(missing_ok=True)
|
|
102
|
+
return
|
|
103
|
+
# If there is private data, serialize this object as JSON
|
|
104
|
+
private_data_path.parent.mkdir(parents=True, exist_ok=True)
|
|
105
|
+
with open(private_data_path, "w") as f:
|
|
106
|
+
# Dump class as JSON, excluding the context
|
|
107
|
+
data_dict = self.__dict__.copy()
|
|
108
|
+
data_dict["ctx"] = None
|
|
109
|
+
json_data = json.dumps(data_dict, indent=4)
|
|
110
|
+
f.write(json_data)
|
|
111
|
+
|
|
112
|
+
def load(self) -> None:
|
|
113
|
+
"""Load content from the private data file in the workspace."""
|
|
114
|
+
private_data_path = self._get_private_data_path()
|
|
115
|
+
if not private_data_path.exists():
|
|
116
|
+
# No private data file
|
|
117
|
+
self.has_private_data = False
|
|
118
|
+
self._private_datasets = []
|
|
119
|
+
self._idap_datasets = []
|
|
120
|
+
return
|
|
121
|
+
with open(private_data_path, "r") as f:
|
|
122
|
+
data = json.load(f)
|
|
123
|
+
self.has_private_data = data.get("_has_private_data", False)
|
|
124
|
+
self._private_datasets = data.get("_private_datasets", [])
|
|
125
|
+
self._idap_datasets = data.get("_idap_datasets", [])
|
|
126
|
+
|
|
127
|
+
def __repr__(self) -> str:
|
|
128
|
+
return (
|
|
129
|
+
f"PrivateData(has_private_data={self.has_private_data}, "
|
|
130
|
+
f"private_datasets={self._private_datasets}, "
|
|
131
|
+
f"idap_datasets={self._idap_datasets}), "
|
|
132
|
+
f"file_path={self._get_private_data_path()})"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def __str__(self) -> str:
|
|
136
|
+
return self.__repr__()
|
aixtools/server/__init__.py
CHANGED
|
@@ -13,6 +13,10 @@ from .utils import (
|
|
|
13
13
|
get_session_id_tuple,
|
|
14
14
|
run_in_thread,
|
|
15
15
|
)
|
|
16
|
+
from .workspace_privacy import (
|
|
17
|
+
is_session_private,
|
|
18
|
+
set_session_private,
|
|
19
|
+
)
|
|
16
20
|
|
|
17
21
|
__all__ = [
|
|
18
22
|
"get_workspace_path",
|
|
@@ -20,4 +24,6 @@ __all__ = [
|
|
|
20
24
|
"container_to_host_path",
|
|
21
25
|
"host_to_container_path",
|
|
22
26
|
"run_in_thread",
|
|
27
|
+
"is_session_private",
|
|
28
|
+
"set_session_private",
|
|
23
29
|
]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Workspace privacy utilities for managing session-level privacy flags.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from fastmcp import Context
|
|
8
|
+
|
|
9
|
+
from aixtools.logging.logging_config import get_logger
|
|
10
|
+
from aixtools.server.path import get_workspace_path
|
|
11
|
+
|
|
12
|
+
logger = get_logger(__name__)
|
|
13
|
+
|
|
14
|
+
PRIVACY_FLAG_FILENAME = ".private_data_indicator"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def set_session_private(ctx: Context | tuple[str, str] | None = None) -> bool:
|
|
18
|
+
"""
|
|
19
|
+
Set the current session as private by creating a privacy flag file.
|
|
20
|
+
|
|
21
|
+
Creates an empty file in the session workspace directory
|
|
22
|
+
and sets it as read-only to prevent accidental removal
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
ctx: FastMCP context for user/session identification.
|
|
26
|
+
If None, uses current FastMCP request context from HTTP headers.
|
|
27
|
+
If tuple, first part is a user id (username), second part is session id (aka conversation id)
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
bool: True if privacy flag was successfully created, False otherwise.
|
|
31
|
+
"""
|
|
32
|
+
try:
|
|
33
|
+
workspace_path = Path(get_workspace_path(ctx=ctx))
|
|
34
|
+
privacy_file = workspace_path / PRIVACY_FLAG_FILENAME
|
|
35
|
+
workspace_path.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
privacy_file.touch(exist_ok=True)
|
|
37
|
+
privacy_file.chmod(0o444)
|
|
38
|
+
logger.warning("Session marked as private")
|
|
39
|
+
return True
|
|
40
|
+
except (OSError, ValueError, RuntimeError) as e:
|
|
41
|
+
logger.error("Set current session as private: %s", str(e))
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def is_session_private(ctx: Context | tuple[str, str] | None = None) -> bool:
|
|
46
|
+
"""
|
|
47
|
+
Check if the current session is marked as private.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
ctx: FastMCP context for user/session identification.
|
|
51
|
+
If None, uses current FastMCP request context from HTTP headers.
|
|
52
|
+
If tuple, first part is a user id (username), second part is session id (aka conversation id)
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
bool: True if workspace is private (flag file exists), False otherwise.
|
|
56
|
+
"""
|
|
57
|
+
try:
|
|
58
|
+
workspace_path = Path(get_workspace_path(ctx=ctx))
|
|
59
|
+
privacy_file = workspace_path / PRIVACY_FLAG_FILENAME
|
|
60
|
+
is_private = privacy_file.exists()
|
|
61
|
+
logger.info("Session privacy check, is private: %s", str(is_private))
|
|
62
|
+
return is_private
|
|
63
|
+
except (OSError, ValueError, RuntimeError) as e:
|
|
64
|
+
logger.error("Check session privacy: %s, assuming not private!", str(e))
|
|
65
|
+
return False
|
aixtools/vault/vault.py
CHANGED
|
@@ -27,9 +27,11 @@ class VaultClient:
|
|
|
27
27
|
if not self.client.is_authenticated():
|
|
28
28
|
raise VaultAuthError("Vault client authentication failed. Check vault_token.")
|
|
29
29
|
|
|
30
|
-
def _get_secret_path(self, user_id: str, service_name: str) -> str:
|
|
31
|
-
"""Generate the vault secret path for a user and service."""
|
|
32
|
-
|
|
30
|
+
def _get_secret_path(self, user_id: str, service_name: Optional[str] = None) -> str:
|
|
31
|
+
"""Generate the vault secret path for a user and optionally a service."""
|
|
32
|
+
if service_name:
|
|
33
|
+
return f"{config.VAULT_PATH_PREFIX}/{config.VAULT_ENV}/{user_id}/{service_name}"
|
|
34
|
+
return f"{config.VAULT_PATH_PREFIX}/{config.VAULT_ENV}/{user_id}"
|
|
33
35
|
|
|
34
36
|
def store_user_service_api_key(self, *, user_id: str, service_name: str, user_api_key: str):
|
|
35
37
|
"""
|
|
@@ -98,3 +100,38 @@ class VaultClient:
|
|
|
98
100
|
except Exception as e:
|
|
99
101
|
logger.error("Failed to read complete secret from path %s: %s", secret_path, str(e))
|
|
100
102
|
raise VaultAuthError(e) from e
|
|
103
|
+
|
|
104
|
+
def list_user_secret_keys(self, *, user_id: str) -> list[str]:
|
|
105
|
+
"""
|
|
106
|
+
List all secret keys (service names) for a user, optionally filtered by service name.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
user_id: The user ID to list secrets for
|
|
110
|
+
service_name: Optional service name to filter results. If provided, returns only this service if it exists.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
List of service names (secret keys) for the user. Empty list if no secrets exist.
|
|
114
|
+
"""
|
|
115
|
+
try:
|
|
116
|
+
# List all services for user
|
|
117
|
+
user_path = self._get_secret_path(user_id)
|
|
118
|
+
logger.info("Listing secret keys for user at path %s", user_path)
|
|
119
|
+
|
|
120
|
+
response = self.client.secrets.kv.v2.list_secrets(path=user_path, mount_point=config.VAULT_MOUNT_POINT)
|
|
121
|
+
|
|
122
|
+
if response and "data" in response and "keys" in response["data"]:
|
|
123
|
+
secret_keys = response["data"]["keys"]
|
|
124
|
+
# Remove trailing slashes from directory names if any
|
|
125
|
+
secret_keys = [key.rstrip("/") for key in secret_keys]
|
|
126
|
+
logger.info("Found %d secret keys for user %s", len(secret_keys), user_id)
|
|
127
|
+
return secret_keys
|
|
128
|
+
logger.info("No secret keys found for user %s", user_id)
|
|
129
|
+
return []
|
|
130
|
+
|
|
131
|
+
except InvalidPath:
|
|
132
|
+
# User path does not exist
|
|
133
|
+
logger.warning("User path does not exist for user %s", user_id)
|
|
134
|
+
return []
|
|
135
|
+
except Exception as e:
|
|
136
|
+
logger.error("Failed to list secret keys for user %s: %s", user_id, str(e))
|
|
137
|
+
raise VaultAuthError(e) from e
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
aixtools/__init__.py,sha256=9NGHm7LjsQmsvjTZvw6QFJexSvAU4bCoN_KBk9SCa00,260
|
|
2
|
-
aixtools/_version.py,sha256=
|
|
2
|
+
aixtools/_version.py,sha256=ib8ckvf-NNDfacXd8unW0p5cf-gl57XyQvjoEMc_pvc,704
|
|
3
3
|
aixtools/app.py,sha256=JzQ0nrv_bjDQokllIlGHOV0HEb-V8N6k_nGQH-TEsVU,5227
|
|
4
4
|
aixtools/chainlit.md,sha256=yC37Ly57vjKyiIvK4oUvf4DYxZCwH7iocTlx7bLeGLU,761
|
|
5
5
|
aixtools/context.py,sha256=I_MD40ZnvRm5WPKAKqBUAdXIf8YaurkYUUHSVVy-QvU,598
|
|
@@ -29,6 +29,7 @@ aixtools/agents/__init__.py,sha256=MAW196S2_G7uGqv-VNjvlOETRfuV44WlU1leO7SiR0A,2
|
|
|
29
29
|
aixtools/agents/agent.py,sha256=E1zu70t53RqIbcLI_R09wUtsiYZR1bTnElCQ5PrsrKw,6127
|
|
30
30
|
aixtools/agents/agent_batch.py,sha256=0Zu9yNCRPAQZPjXQ-dIUAmP1uGTVbxVt7xvnMpoJMjU,2251
|
|
31
31
|
aixtools/agents/prompt.py,sha256=VCOVSnhNKsPIT347ouzwM1PH4I9UTm2cSnTh3ZpjRwk,3391
|
|
32
|
+
aixtools/compliance/private_data.py,sha256=aYEd13eHUajs72nQxCXsa7tklXnqyurUaYuNzm8belQ,5258
|
|
32
33
|
aixtools/db/__init__.py,sha256=b8vRhme3egV-aUZbAntnOaDkSXB8UT0Xy5oqQhU_z0Q,399
|
|
33
34
|
aixtools/db/database.py,sha256=caWe95GlxZYlxn2ubDmR-_cQUW0ulkpR3BHunKIaOsw,3369
|
|
34
35
|
aixtools/db/vector_db.py,sha256=be4JGyXj3o8VEfy9L6SO1aAoDET_zazMJkYfjlYHTYQ,4133
|
|
@@ -56,10 +57,11 @@ aixtools/mcp/example_server.py,sha256=1SWCyrLWsAnOa81HC4QbPJo_lBVu0b3SZBWI-qDh1v
|
|
|
56
57
|
aixtools/mcp/fast_mcp_log.py,sha256=XYOS406dVjn5YTHyGRsRvVNQ0SKlRObfrKj6EeLFjHg,1057
|
|
57
58
|
aixtools/mcp/faulty_mcp.py,sha256=uU9vlNGCS_i2k20wocVMaDHTlYjMQxuzjILad9O1cjA,12807
|
|
58
59
|
aixtools/model_patch/model_patch.py,sha256=JT-oHubIn2LeoSwWbzEQ5vLH7crJmFUecHyQfaAFHa0,1813
|
|
59
|
-
aixtools/server/__init__.py,sha256=
|
|
60
|
+
aixtools/server/__init__.py,sha256=37ADJrGLzsmjFsM2ZKUoM9cevH8rBn359WesDxIwoco,585
|
|
60
61
|
aixtools/server/app_mounter.py,sha256=0tJ0tC140ezAjnYdlhpLJQjY-TO8NVw7D8LseYCCVY8,3336
|
|
61
62
|
aixtools/server/path.py,sha256=SaIJxvmhJy3kzx5zJ6d4cKP6kKu2wFFciQkOLGTA4gg,3056
|
|
62
63
|
aixtools/server/utils.py,sha256=tZWITIx6M-luV9yve4j3rPtYGSSA6zWS0JWEAySne_M,2276
|
|
64
|
+
aixtools/server/workspace_privacy.py,sha256=grcj82eHSd7gFbb5f_w9nv4TWp50QyU952l0iIPoChM,2375
|
|
63
65
|
aixtools/testing/__init__.py,sha256=mlmaAR2gmS4SbsYNCxnIprmFpFp-syjgVUkpUszo3mE,166
|
|
64
66
|
aixtools/testing/aix_test_model.py,sha256=dlI3sdyvmu4fUs_K4-oazs_a7cE6V-gnI6RQ0_fPVxg,5925
|
|
65
67
|
aixtools/testing/mock_tool.py,sha256=4I0LxxSkLhGIKM2YxCP3cnYI8IYJjdKhfwGZ3dioXsM,2465
|
|
@@ -77,8 +79,9 @@ aixtools/utils/utils.py,sha256=5911Ej1ES2NU_FKIWA3CWKhKnwgjvi1aDR2aiD6Xv3E,4880
|
|
|
77
79
|
aixtools/utils/chainlit/cl_agent_show.py,sha256=vaRuowp4BRvhxEr5hw0zHEJ7iaSF_5bo_9BH7pGPPpw,4398
|
|
78
80
|
aixtools/utils/chainlit/cl_utils.py,sha256=fxaxdkcZg6uHdM8uztxdPowg3a2f7VR7B26VPY4t-3c,5738
|
|
79
81
|
aixtools/vault/__init__.py,sha256=fsr_NuX3GZ9WZ7dGfe0gp_5-z3URxAfwVRXw7Xyc0dU,141
|
|
80
|
-
aixtools/vault/vault.py,sha256=
|
|
81
|
-
docker/mcp-base/Dockerfile,sha256=
|
|
82
|
+
aixtools/vault/vault.py,sha256=9dZLWdZQk9qN_Q9Djkofw9LUKnJqnrX5H0fGusVLBhA,6037
|
|
83
|
+
docker/mcp-base/Dockerfile,sha256=uislVoTEgRF--AAiyX24sBxlDdfA1ZU5rDM94XYFqvI,1388
|
|
84
|
+
docker/mcp-base/zscaler.crt,sha256=fCUNiOfJlWTA7R4zV1Xyb-XC1_nMHPoYTFGvBj1oz6s,1732
|
|
82
85
|
notebooks/example_faulty_mcp_server.ipynb,sha256=b2Cy3GXfj-gOBZ7SoUzj25F1rxp5u-32EWPHWQ-sxn8,1729
|
|
83
86
|
notebooks/example_mcp_server_stdio.ipynb,sha256=ya4dRKNFU2vQxob-uIhKHGAzINXGQ6MehgKVmSCpHLk,1634
|
|
84
87
|
notebooks/example_raw_mcp_client.ipynb,sha256=uchaG-LuuwEpE2oIkmhZ2s1EDb19AgT1fUv2Jxtjgu8,1795
|
|
@@ -89,7 +92,7 @@ scripts/log_view.sh,sha256=bp8oXFRRbbHpyvHAN85wfDHTVK7vMJOYsBx_-bgECQc,511
|
|
|
89
92
|
scripts/run_example_mcp_server.sh,sha256=f7m7h7O_wo6-nAsYlOXVWIASCOh3Qbuu0XWizlxMhl8,355
|
|
90
93
|
scripts/run_faulty_mcp_server.sh,sha256=u_-8NbPDnJQt6IinNSjh8tc2ed-_MjGyipJXrUXaGR8,291
|
|
91
94
|
scripts/run_server.sh,sha256=5iiB9bB5M2MuOgxVQqu7Oa_tBVtJpt0uB4z9uLu2J50,720
|
|
92
|
-
scripts/test.sh,sha256=
|
|
95
|
+
scripts/test.sh,sha256=KxXWkVqctFRNP8hItJr8K27nDHEkfwNWb1UFhpBQDOk,865
|
|
93
96
|
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
94
97
|
tests/unit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
95
98
|
tests/unit/a2a/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -102,6 +105,7 @@ tests/unit/a2a/google_sdk/pydantic_ai_adapter/test_agent_executor.py,sha256=PcyC
|
|
|
102
105
|
tests/unit/a2a/google_sdk/pydantic_ai_adapter/test_storage.py,sha256=tb67pFfvyWSaDfKaiPDNBQfl6-o17WtCMZh3lQHrYxY,5468
|
|
103
106
|
tests/unit/agents/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
104
107
|
tests/unit/agents/test_prompt.py,sha256=YWFZdH_F774hxw79gsWoTWBPVs8UjOAtJOgNXJ8N9gs,15384
|
|
108
|
+
tests/unit/compliance/test_private_data.py,sha256=GjH7NCp54Bz1S-CmH_mUe53lb53kllOOJEm448OniRI,13693
|
|
105
109
|
tests/unit/google/__init__.py,sha256=eRYHldBi5cFWL7oo2_t5TErI8ESmIjNvBZIcp-w8hSA,45
|
|
106
110
|
tests/unit/google/test_client.py,sha256=fXR4Cozea7bdL2prM-1s9IqUQ9AheklQnHpN-4YM3gg,11005
|
|
107
111
|
tests/unit/mcp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -112,9 +116,9 @@ tests/unit/server/test_utils.py,sha256=kvhzdgNfsJl5tqcRBWg2yTR5GPpyrFCOmEIOuHb39
|
|
|
112
116
|
tests/unit/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
113
117
|
tests/unit/utils/test_files.py,sha256=AKFmXQqXstyKd2PreE4EmQyhQYeqOmu1Sp80MwHrf_Q,5782
|
|
114
118
|
tests/unit/vault/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
115
|
-
tests/unit/vault/test_vault.py,sha256=
|
|
116
|
-
aixtools-0.1.
|
|
117
|
-
aixtools-0.1.
|
|
118
|
-
aixtools-0.1.
|
|
119
|
-
aixtools-0.1.
|
|
120
|
-
aixtools-0.1.
|
|
119
|
+
tests/unit/vault/test_vault.py,sha256=T9V2Opxl3N5sJPftw0Q4lnVOs6urGpAmffe0cz6PUfw,10445
|
|
120
|
+
aixtools-0.1.9.dist-info/METADATA,sha256=NyTMeH1c44cDNbXW7VKy2feT-xConc_uGIx0gSa68KM,18569
|
|
121
|
+
aixtools-0.1.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
122
|
+
aixtools-0.1.9.dist-info/entry_points.txt,sha256=dHoutULEZx7xXSqJrZdViSVjfInJibfLibi2nRXL3SE,56
|
|
123
|
+
aixtools-0.1.9.dist-info/top_level.txt,sha256=ee4eF-0pqu45zCUVml0mWIhnXQgqMQper2-49BBVHLY,40
|
|
124
|
+
aixtools-0.1.9.dist-info/RECORD,,
|
docker/mcp-base/Dockerfile
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
FROM ubuntu:22.04
|
|
2
2
|
|
|
3
3
|
RUN apt-get -y update && \
|
|
4
|
-
apt-get -y install
|
|
4
|
+
apt-get -y install ca-certificates curl gcc git libcap2-bin sudo
|
|
5
|
+
RUN mv /usr/bin/sudo /usr/sbin
|
|
5
6
|
|
|
6
|
-
#
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
update-ca-certificates && \
|
|
12
|
-
export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt && \
|
|
13
|
-
export CURL_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt; \
|
|
14
|
-
fi
|
|
7
|
+
# Add Zscaler CA certificate
|
|
8
|
+
COPY ./zscaler.crt /usr/local/share/ca-certificates/
|
|
9
|
+
RUN update-ca-certificates
|
|
10
|
+
ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
|
|
11
|
+
ENV CURL_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
|
|
15
12
|
|
|
16
13
|
# Install `uv` Python package manager
|
|
17
14
|
RUN bash -o pipefail -c "curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR=/usr/local/bin sh"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
|
2
|
+
MIIE0zCCA7ugAwIBAgIJANu+mC2Jt3uTMA0GCSqGSIb3DQEBCwUAMIGhMQswCQYD
|
|
3
|
+
VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTERMA8GA1UEBxMIU2FuIEpvc2Ux
|
|
4
|
+
FTATBgNVBAoTDFpzY2FsZXIgSW5jLjEVMBMGA1UECxMMWnNjYWxlciBJbmMuMRgw
|
|
5
|
+
FgYDVQQDEw9ac2NhbGVyIFJvb3QgQ0ExIjAgBgkqhkiG9w0BCQEWE3N1cHBvcnRA
|
|
6
|
+
enNjYWxlci5jb20wHhcNMTQxMjE5MDAyNzU1WhcNNDIwNTA2MDAyNzU1WjCBoTEL
|
|
7
|
+
MAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExETAPBgNVBAcTCFNhbiBK
|
|
8
|
+
b3NlMRUwEwYDVQQKEwxac2NhbGVyIEluYy4xFTATBgNVBAsTDFpzY2FsZXIgSW5j
|
|
9
|
+
LjEYMBYGA1UEAxMPWnNjYWxlciBSb290IENBMSIwIAYJKoZIhvcNAQkBFhNzdXBw
|
|
10
|
+
b3J0QHpzY2FsZXIuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
|
|
11
|
+
qT7STSxZRTgEFFf6doHajSc1vk5jmzmM6BWuOo044EsaTc9eVEV/HjH/1DWzZtcr
|
|
12
|
+
fTj+ni205apMTlKBW3UYR+lyLHQ9FoZiDXYXK8poKSV5+Tm0Vls/5Kb8mkhVVqv7
|
|
13
|
+
LgYEmvEY7HPY+i1nEGZCa46ZXCOohJ0mBEtB9JVlpDIO+nN0hUMAYYdZ1KZWCMNf
|
|
14
|
+
5J/aTZiShsorN2A38iSOhdd+mcRM4iNL3gsLu99XhKnRqKoHeH83lVdfu1XBeoQz
|
|
15
|
+
z5V6gA3kbRvhDwoIlTBeMa5l4yRdJAfdpkbFzqiwSgNdhbxTHnYYorDzKfr2rEFM
|
|
16
|
+
dsMU0DHdeAZf711+1CunuQIDAQABo4IBCjCCAQYwHQYDVR0OBBYEFLm33UrNww4M
|
|
17
|
+
hp1d3+wcBGnFTpjfMIHWBgNVHSMEgc4wgcuAFLm33UrNww4Mhp1d3+wcBGnFTpjf
|
|
18
|
+
oYGnpIGkMIGhMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTERMA8G
|
|
19
|
+
A1UEBxMIU2FuIEpvc2UxFTATBgNVBAoTDFpzY2FsZXIgSW5jLjEVMBMGA1UECxMM
|
|
20
|
+
WnNjYWxlciBJbmMuMRgwFgYDVQQDEw9ac2NhbGVyIFJvb3QgQ0ExIjAgBgkqhkiG
|
|
21
|
+
9w0BCQEWE3N1cHBvcnRAenNjYWxlci5jb22CCQDbvpgtibd7kzAMBgNVHRMEBTAD
|
|
22
|
+
AQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAw0NdJh8w3NsJu4KHuVZUrmZgIohnTm0j+
|
|
23
|
+
RTmYQ9IKA/pvxAcA6K1i/LO+Bt+tCX+C0yxqB8qzuo+4vAzoY5JEBhyhBhf1uK+P
|
|
24
|
+
/WVWFZN/+hTgpSbZgzUEnWQG2gOVd24msex+0Sr7hyr9vn6OueH+jj+vCMiAm5+u
|
|
25
|
+
kd7lLvJsBu3AO3jGWVLyPkS3i6Gf+rwAp1OsRrv3WnbkYcFf9xjuaf4z0hRCrLN2
|
|
26
|
+
xFNjavxrHmsH8jPHVvgc1VD0Opja0l/BRVauTrUaoW6tE+wFG5rEcPGS80jjHK4S
|
|
27
|
+
pB5iDj2mUZH1T8lzYtuZy0ZPirxmtsk3135+CKNa2OCAhhFjE0xd
|
|
28
|
+
-----END CERTIFICATE-----
|
scripts/test.sh
CHANGED
|
@@ -15,9 +15,16 @@ touch .env
|
|
|
15
15
|
# Add the project root to PYTHONPATH so imports work correctly
|
|
16
16
|
export PYTHONPATH="${PROJECT_DIR}:${PYTHONPATH:-}"
|
|
17
17
|
|
|
18
|
-
#
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
# Check if a specific test is provided as parameter
|
|
19
|
+
if [ $# -eq 1 ]; then
|
|
20
|
+
# Run specific test without coverage
|
|
21
|
+
echo "Running specific test: $1"
|
|
22
|
+
pytest "$1" -v
|
|
23
|
+
else
|
|
24
|
+
# Run all tests using pytest with coverage
|
|
25
|
+
echo "Running tests with coverage..."
|
|
26
|
+
pytest tests/ -v --cov=aixtools --cov-report=term-missing
|
|
27
|
+
|
|
28
|
+
# Generate HTML coverage report
|
|
29
|
+
# pytest tests/ --cov=aixtools --cov-report=html
|
|
30
|
+
fi
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import unittest
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from aixtools.compliance.private_data import PrivateData, PRIVATE_DATA_FILE
|
|
6
|
+
from aixtools.server.path import get_workspace_path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestPrivateData(unittest.TestCase):
|
|
10
|
+
"""Test cases for PrivateData class without mocking or patching."""
|
|
11
|
+
|
|
12
|
+
def setUp(self):
|
|
13
|
+
"""Set up test environment with temporary directory."""
|
|
14
|
+
# Create a test user and session context
|
|
15
|
+
self.workspace_path = Path(get_workspace_path())
|
|
16
|
+
self.workspace_path.mkdir(parents=True, exist_ok=True)
|
|
17
|
+
self.private_data_file = self.workspace_path / PRIVATE_DATA_FILE
|
|
18
|
+
|
|
19
|
+
def tearDown(self):
|
|
20
|
+
"""Clean up test environment."""
|
|
21
|
+
self.private_data_file.unlink(missing_ok=True)
|
|
22
|
+
|
|
23
|
+
def test_init_no_existing_file(self):
|
|
24
|
+
"""Test initialization when no private data file exists."""
|
|
25
|
+
private_data = PrivateData()
|
|
26
|
+
self.assertFalse(private_data.has_private_data)
|
|
27
|
+
self.assertEqual(private_data.get_private_datasets(), [])
|
|
28
|
+
self.assertEqual(private_data.get_idap_datasets(), [])
|
|
29
|
+
|
|
30
|
+
def test_init_with_existing_file(self):
|
|
31
|
+
"""Test initialization when private data file exists."""
|
|
32
|
+
# Create a test file
|
|
33
|
+
private_data_path = self.workspace_path / PRIVATE_DATA_FILE
|
|
34
|
+
test_data = {
|
|
35
|
+
"_has_private_data": True,
|
|
36
|
+
"_private_datasets": ["dataset1", "dataset2"],
|
|
37
|
+
"_idap_datasets": ["dataset1"],
|
|
38
|
+
"ctx": None
|
|
39
|
+
}
|
|
40
|
+
with open(private_data_path, "w") as f:
|
|
41
|
+
json.dump(test_data, f)
|
|
42
|
+
|
|
43
|
+
private_data = PrivateData()
|
|
44
|
+
|
|
45
|
+
self.assertTrue(private_data.has_private_data)
|
|
46
|
+
self.assertEqual(set(private_data.get_private_datasets()), {"dataset1", "dataset2"})
|
|
47
|
+
self.assertEqual(private_data.get_idap_datasets(), ["dataset1"])
|
|
48
|
+
|
|
49
|
+
def test_add_private_dataset_new(self):
|
|
50
|
+
"""Test adding a new private dataset."""
|
|
51
|
+
private_data = PrivateData()
|
|
52
|
+
|
|
53
|
+
private_data.add_private_dataset("test_dataset")
|
|
54
|
+
|
|
55
|
+
self.assertTrue(private_data.has_private_data)
|
|
56
|
+
self.assertIn("test_dataset", private_data.get_private_datasets())
|
|
57
|
+
|
|
58
|
+
# Verify data is saved to file
|
|
59
|
+
private_data_path = self.workspace_path / PRIVATE_DATA_FILE
|
|
60
|
+
self.assertTrue(private_data_path.exists())
|
|
61
|
+
|
|
62
|
+
with open(private_data_path, "r") as f:
|
|
63
|
+
saved_data = json.load(f)
|
|
64
|
+
|
|
65
|
+
self.assertTrue(saved_data["_has_private_data"])
|
|
66
|
+
self.assertIn("test_dataset", saved_data["_private_datasets"])
|
|
67
|
+
|
|
68
|
+
def test_add_private_dataset_duplicate(self):
|
|
69
|
+
"""Test adding a duplicate private dataset."""
|
|
70
|
+
private_data = PrivateData()
|
|
71
|
+
|
|
72
|
+
private_data.add_private_dataset("test_dataset")
|
|
73
|
+
initial_count = len(private_data.get_private_datasets())
|
|
74
|
+
|
|
75
|
+
private_data.add_private_dataset("test_dataset")
|
|
76
|
+
final_count = len(private_data.get_private_datasets())
|
|
77
|
+
|
|
78
|
+
self.assertEqual(initial_count, final_count)
|
|
79
|
+
self.assertEqual(private_data.get_private_datasets().count("test_dataset"), 1)
|
|
80
|
+
|
|
81
|
+
def test_add_idap_dataset_new(self):
|
|
82
|
+
"""Test adding a new IDAP dataset."""
|
|
83
|
+
private_data = PrivateData()
|
|
84
|
+
|
|
85
|
+
private_data.add_idap_dataset("idap_dataset")
|
|
86
|
+
|
|
87
|
+
self.assertTrue(private_data.has_private_data)
|
|
88
|
+
self.assertIn("idap_dataset", private_data.get_idap_datasets())
|
|
89
|
+
self.assertIn("idap_dataset", private_data.get_private_datasets())
|
|
90
|
+
|
|
91
|
+
# Verify data is saved to file
|
|
92
|
+
private_data_path = self.workspace_path / PRIVATE_DATA_FILE
|
|
93
|
+
self.assertTrue(private_data_path.exists())
|
|
94
|
+
|
|
95
|
+
with open(private_data_path, "r") as f:
|
|
96
|
+
saved_data = json.load(f)
|
|
97
|
+
|
|
98
|
+
self.assertTrue(saved_data["_has_private_data"])
|
|
99
|
+
self.assertIn("idap_dataset", saved_data["_idap_datasets"])
|
|
100
|
+
self.assertIn("idap_dataset", saved_data["_private_datasets"])
|
|
101
|
+
|
|
102
|
+
def test_add_idap_dataset_existing_private(self):
|
|
103
|
+
"""Test adding IDAP dataset when it already exists as private dataset."""
|
|
104
|
+
private_data = PrivateData()
|
|
105
|
+
|
|
106
|
+
private_data.add_private_dataset("existing_dataset")
|
|
107
|
+
private_data.add_idap_dataset("existing_dataset")
|
|
108
|
+
|
|
109
|
+
self.assertTrue(private_data.has_private_data)
|
|
110
|
+
self.assertIn("existing_dataset", private_data.get_idap_datasets())
|
|
111
|
+
self.assertIn("existing_dataset", private_data.get_private_datasets())
|
|
112
|
+
self.assertEqual(private_data.get_private_datasets().count("existing_dataset"), 1)
|
|
113
|
+
|
|
114
|
+
def test_add_idap_dataset_duplicate(self):
|
|
115
|
+
"""Test adding a duplicate IDAP dataset."""
|
|
116
|
+
private_data = PrivateData()
|
|
117
|
+
|
|
118
|
+
private_data.add_idap_dataset("idap_dataset")
|
|
119
|
+
initial_idap_count = len(private_data.get_idap_datasets())
|
|
120
|
+
initial_private_count = len(private_data.get_private_datasets())
|
|
121
|
+
|
|
122
|
+
private_data.add_idap_dataset("idap_dataset")
|
|
123
|
+
final_idap_count = len(private_data.get_idap_datasets())
|
|
124
|
+
final_private_count = len(private_data.get_private_datasets())
|
|
125
|
+
|
|
126
|
+
self.assertEqual(initial_idap_count, final_idap_count)
|
|
127
|
+
self.assertEqual(initial_private_count, final_private_count)
|
|
128
|
+
|
|
129
|
+
def test_has_private_dataset(self):
|
|
130
|
+
"""Test checking if a private dataset exists."""
|
|
131
|
+
private_data = PrivateData()
|
|
132
|
+
|
|
133
|
+
self.assertFalse(private_data.has_private_dataset("nonexistent"))
|
|
134
|
+
|
|
135
|
+
private_data.add_private_dataset("test_dataset")
|
|
136
|
+
|
|
137
|
+
self.assertTrue(private_data.has_private_dataset("test_dataset"))
|
|
138
|
+
self.assertFalse(private_data.has_private_dataset("nonexistent"))
|
|
139
|
+
|
|
140
|
+
def test_has_idap_dataset(self):
|
|
141
|
+
"""Test checking if an IDAP dataset exists."""
|
|
142
|
+
private_data = PrivateData()
|
|
143
|
+
|
|
144
|
+
self.assertFalse(private_data.has_idap_dataset("nonexistent"))
|
|
145
|
+
|
|
146
|
+
private_data.add_idap_dataset("idap_dataset")
|
|
147
|
+
|
|
148
|
+
self.assertTrue(private_data.has_idap_dataset("idap_dataset"))
|
|
149
|
+
self.assertFalse(private_data.has_idap_dataset("nonexistent"))
|
|
150
|
+
|
|
151
|
+
def test_get_datasets_returns_copies(self):
|
|
152
|
+
"""Test that get methods return copies to prevent external modification."""
|
|
153
|
+
private_data = PrivateData()
|
|
154
|
+
private_data.add_private_dataset("private_dataset")
|
|
155
|
+
private_data.add_idap_dataset("idap_dataset")
|
|
156
|
+
|
|
157
|
+
private_datasets = private_data.get_private_datasets()
|
|
158
|
+
idap_datasets = private_data.get_idap_datasets()
|
|
159
|
+
|
|
160
|
+
# Modify the returned lists
|
|
161
|
+
private_datasets.append("external_modification")
|
|
162
|
+
idap_datasets.append("external_modification")
|
|
163
|
+
|
|
164
|
+
# Verify original data is unchanged
|
|
165
|
+
self.assertNotIn("external_modification", private_data.get_private_datasets())
|
|
166
|
+
self.assertNotIn("external_modification", private_data.get_idap_datasets())
|
|
167
|
+
|
|
168
|
+
def test_has_private_data_setter_true(self):
|
|
169
|
+
"""Test setting has_private_data to True."""
|
|
170
|
+
private_data = PrivateData()
|
|
171
|
+
|
|
172
|
+
private_data.has_private_data = True
|
|
173
|
+
|
|
174
|
+
self.assertTrue(private_data.has_private_data)
|
|
175
|
+
|
|
176
|
+
# Verify data is saved to file
|
|
177
|
+
private_data_path = self.workspace_path / PRIVATE_DATA_FILE
|
|
178
|
+
self.assertTrue(private_data_path.exists())
|
|
179
|
+
|
|
180
|
+
def test_has_private_data_setter_false(self):
|
|
181
|
+
"""Test setting has_private_data to False clears all data."""
|
|
182
|
+
private_data = PrivateData()
|
|
183
|
+
private_data.add_private_dataset("dataset1")
|
|
184
|
+
private_data.add_idap_dataset("dataset2")
|
|
185
|
+
|
|
186
|
+
private_data.has_private_data = False
|
|
187
|
+
|
|
188
|
+
self.assertFalse(private_data.has_private_data)
|
|
189
|
+
self.assertEqual(private_data.get_private_datasets(), [])
|
|
190
|
+
self.assertEqual(private_data.get_idap_datasets(), [])
|
|
191
|
+
|
|
192
|
+
# Verify file is deleted
|
|
193
|
+
private_data_path = self.workspace_path / PRIVATE_DATA_FILE
|
|
194
|
+
self.assertFalse(private_data_path.exists())
|
|
195
|
+
|
|
196
|
+
def test_save_with_no_private_data(self):
|
|
197
|
+
"""Test saving when has_private_data is False deletes the file."""
|
|
198
|
+
private_data = PrivateData()
|
|
199
|
+
private_data.add_private_dataset("test_dataset")
|
|
200
|
+
|
|
201
|
+
# Verify file exists
|
|
202
|
+
private_data_path = self.workspace_path / PRIVATE_DATA_FILE
|
|
203
|
+
self.assertTrue(private_data_path.exists())
|
|
204
|
+
|
|
205
|
+
private_data.has_private_data = False
|
|
206
|
+
|
|
207
|
+
# Verify file is deleted
|
|
208
|
+
self.assertFalse(private_data_path.exists())
|
|
209
|
+
|
|
210
|
+
def test_save_with_private_data(self):
|
|
211
|
+
"""Test saving when has_private_data is True creates/updates the file."""
|
|
212
|
+
private_data = PrivateData()
|
|
213
|
+
private_data.add_private_dataset("dataset1")
|
|
214
|
+
private_data.add_idap_dataset("dataset2")
|
|
215
|
+
|
|
216
|
+
private_data_path = self.workspace_path / PRIVATE_DATA_FILE
|
|
217
|
+
self.assertTrue(private_data_path.exists())
|
|
218
|
+
|
|
219
|
+
with open(private_data_path, "r") as f:
|
|
220
|
+
saved_data = json.load(f)
|
|
221
|
+
|
|
222
|
+
self.assertTrue(saved_data["_has_private_data"])
|
|
223
|
+
self.assertIn("dataset1", saved_data["_private_datasets"])
|
|
224
|
+
self.assertIn("dataset2", saved_data["_private_datasets"])
|
|
225
|
+
self.assertIn("dataset2", saved_data["_idap_datasets"])
|
|
226
|
+
self.assertIsNone(saved_data["ctx"])
|
|
227
|
+
|
|
228
|
+
def test_load_from_corrupted_file(self):
|
|
229
|
+
"""Test loading from a corrupted JSON file."""
|
|
230
|
+
private_data_path = self.workspace_path / PRIVATE_DATA_FILE
|
|
231
|
+
with open(private_data_path, "w") as f:
|
|
232
|
+
f.write("invalid json content")
|
|
233
|
+
|
|
234
|
+
with self.assertRaises(json.JSONDecodeError):
|
|
235
|
+
PrivateData()
|
|
236
|
+
|
|
237
|
+
def test_load_from_file_with_missing_fields(self):
|
|
238
|
+
"""Test loading from a file with missing fields uses defaults."""
|
|
239
|
+
private_data_path = self.workspace_path / PRIVATE_DATA_FILE
|
|
240
|
+
test_data = {"_has_private_data": True} # Missing other fields
|
|
241
|
+
with open(private_data_path, "w") as f:
|
|
242
|
+
json.dump(test_data, f)
|
|
243
|
+
|
|
244
|
+
private_data = PrivateData()
|
|
245
|
+
|
|
246
|
+
self.assertTrue(private_data.has_private_data)
|
|
247
|
+
self.assertEqual(private_data.get_private_datasets(), [])
|
|
248
|
+
self.assertEqual(private_data.get_idap_datasets(), [])
|
|
249
|
+
|
|
250
|
+
def test_multiple_operations_persistence(self):
|
|
251
|
+
"""Test that multiple operations are properly persisted."""
|
|
252
|
+
# Create first instance and add data
|
|
253
|
+
private_data1 = PrivateData()
|
|
254
|
+
private_data1.add_private_dataset("dataset1")
|
|
255
|
+
private_data1.add_idap_dataset("dataset2")
|
|
256
|
+
|
|
257
|
+
# Create second instance and verify data is loaded
|
|
258
|
+
private_data2 = PrivateData()
|
|
259
|
+
self.assertTrue(private_data2.has_private_data)
|
|
260
|
+
self.assertIn("dataset1", private_data2.get_private_datasets())
|
|
261
|
+
self.assertIn("dataset2", private_data2.get_private_datasets())
|
|
262
|
+
self.assertIn("dataset2", private_data2.get_idap_datasets())
|
|
263
|
+
|
|
264
|
+
# Add more data with second instance
|
|
265
|
+
private_data2.add_private_dataset("dataset3")
|
|
266
|
+
|
|
267
|
+
# Create third instance and verify all data is present
|
|
268
|
+
private_data3 = PrivateData()
|
|
269
|
+
expected_private = {"dataset1", "dataset2", "dataset3"}
|
|
270
|
+
expected_idap = {"dataset2"}
|
|
271
|
+
|
|
272
|
+
self.assertEqual(set(private_data3.get_private_datasets()), expected_private)
|
|
273
|
+
self.assertEqual(set(private_data3.get_idap_datasets()), expected_idap)
|
|
274
|
+
|
|
275
|
+
def test_str_and_repr(self):
|
|
276
|
+
"""Test string representation methods."""
|
|
277
|
+
private_data = PrivateData()
|
|
278
|
+
private_data.add_private_dataset("dataset1")
|
|
279
|
+
private_data.add_idap_dataset("dataset2")
|
|
280
|
+
|
|
281
|
+
str_repr = str(private_data)
|
|
282
|
+
repr_repr = repr(private_data)
|
|
283
|
+
|
|
284
|
+
self.assertEqual(str_repr, repr_repr)
|
|
285
|
+
self.assertIn("has_private_data=True", str_repr)
|
|
286
|
+
self.assertIn("dataset1", str_repr)
|
|
287
|
+
self.assertIn("dataset2", str_repr)
|
|
288
|
+
self.assertIn(str(self.workspace_path / PRIVATE_DATA_FILE), str_repr)
|
|
289
|
+
|
|
290
|
+
def test_file_operations_create_directories(self):
|
|
291
|
+
"""Test that file operations create necessary directories."""
|
|
292
|
+
# Remove the workspace directory
|
|
293
|
+
import shutil
|
|
294
|
+
shutil.rmtree(self.workspace_path)
|
|
295
|
+
self.assertFalse(self.workspace_path.exists())
|
|
296
|
+
|
|
297
|
+
# Create PrivateData instance and add data
|
|
298
|
+
private_data = PrivateData()
|
|
299
|
+
private_data.add_private_dataset("test_dataset")
|
|
300
|
+
|
|
301
|
+
# Verify directory and file were created
|
|
302
|
+
self.assertTrue(self.workspace_path.exists())
|
|
303
|
+
private_data_path = self.workspace_path / PRIVATE_DATA_FILE
|
|
304
|
+
self.assertTrue(private_data_path.exists())
|
|
305
|
+
|
|
306
|
+
def test_concurrent_modifications_simulation(self):
|
|
307
|
+
"""Test simulation of concurrent modifications (without actual threading)."""
|
|
308
|
+
# Create two instances with same context
|
|
309
|
+
private_data1 = PrivateData()
|
|
310
|
+
private_data2 = PrivateData()
|
|
311
|
+
|
|
312
|
+
# Add data with first instance
|
|
313
|
+
private_data1.add_private_dataset("dataset1")
|
|
314
|
+
|
|
315
|
+
# Second instance should see the changes when reloaded
|
|
316
|
+
private_data2.load()
|
|
317
|
+
self.assertIn("dataset1", private_data2.get_private_datasets())
|
|
318
|
+
|
|
319
|
+
# Add data with second instance
|
|
320
|
+
private_data2.add_idap_dataset("dataset2")
|
|
321
|
+
|
|
322
|
+
# First instance should see the changes when reloaded
|
|
323
|
+
private_data1.load()
|
|
324
|
+
self.assertIn("dataset2", private_data1.get_idap_datasets())
|
|
325
|
+
self.assertIn("dataset2", private_data1.get_private_datasets())
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
if __name__ == "__main__":
|
|
329
|
+
unittest.main()
|
tests/unit/vault/test_vault.py
CHANGED
|
@@ -159,6 +159,81 @@ def test_read_user_service_secret_not_found(patched_vault_client):
|
|
|
159
159
|
)
|
|
160
160
|
|
|
161
161
|
|
|
162
|
+
def test_list_user_secret_keys_success(patched_vault_client):
|
|
163
|
+
"""Test successful listing of all user secret keys."""
|
|
164
|
+
mock_response = {"data": {"keys": ["service1/", "service2/", "service3"]}}
|
|
165
|
+
patched_vault_client.client.secrets.kv.v2.list_secrets.return_value = mock_response
|
|
166
|
+
|
|
167
|
+
result = patched_vault_client.list_user_secret_keys(user_id="test-user")
|
|
168
|
+
|
|
169
|
+
assert result == ["service1", "service2", "service3"]
|
|
170
|
+
patched_vault_client.client.secrets.kv.v2.list_secrets.assert_called_once_with(
|
|
171
|
+
path="path/dev/test-user", mount_point="secret"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_list_user_secret_keys_no_secrets(patched_vault_client):
|
|
176
|
+
"""Test listing user secret keys when no secrets exist."""
|
|
177
|
+
mock_response = {"data": {"keys": []}}
|
|
178
|
+
patched_vault_client.client.secrets.kv.v2.list_secrets.return_value = mock_response
|
|
179
|
+
|
|
180
|
+
result = patched_vault_client.list_user_secret_keys(user_id="test-user")
|
|
181
|
+
|
|
182
|
+
assert result == []
|
|
183
|
+
patched_vault_client.client.secrets.kv.v2.list_secrets.assert_called_once_with(
|
|
184
|
+
path="path/dev/test-user", mount_point="secret"
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def test_list_user_secret_keys_empty_response(patched_vault_client):
|
|
189
|
+
"""Test listing user secret keys when response is empty."""
|
|
190
|
+
patched_vault_client.client.secrets.kv.v2.list_secrets.return_value = {}
|
|
191
|
+
|
|
192
|
+
result = patched_vault_client.list_user_secret_keys(user_id="test-user")
|
|
193
|
+
|
|
194
|
+
assert result == []
|
|
195
|
+
patched_vault_client.client.secrets.kv.v2.list_secrets.assert_called_once_with(
|
|
196
|
+
path="path/dev/test-user", mount_point="secret"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def test_list_user_secret_keys_invalid_path(patched_vault_client):
|
|
201
|
+
"""Test listing user secret keys when user path does not exist."""
|
|
202
|
+
patched_vault_client.client.secrets.kv.v2.list_secrets.side_effect = InvalidPath
|
|
203
|
+
|
|
204
|
+
result = patched_vault_client.list_user_secret_keys(user_id="test-user")
|
|
205
|
+
|
|
206
|
+
assert result == []
|
|
207
|
+
patched_vault_client.client.secrets.kv.v2.list_secrets.assert_called_once_with(
|
|
208
|
+
path="path/dev/test-user", mount_point="secret"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def test_list_user_secret_keys_unexpected_error(patched_vault_client):
|
|
213
|
+
"""Test listing user secret keys when an unexpected exception occurs."""
|
|
214
|
+
patched_vault_client.client.secrets.kv.v2.list_secrets.side_effect = Exception("List error")
|
|
215
|
+
|
|
216
|
+
with pytest.raises(VaultAuthError, match="List error"):
|
|
217
|
+
patched_vault_client.list_user_secret_keys(user_id="test-user")
|
|
218
|
+
|
|
219
|
+
patched_vault_client.client.secrets.kv.v2.list_secrets.assert_called_once_with(
|
|
220
|
+
path="path/dev/test-user", mount_point="secret"
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def test_list_user_secret_keys_strips_trailing_slashes(patched_vault_client):
|
|
225
|
+
"""Test that trailing slashes are properly stripped from directory names."""
|
|
226
|
+
mock_response = {"data": {"keys": ["service1/", "service2/", "service3", "service4/"]}}
|
|
227
|
+
patched_vault_client.client.secrets.kv.v2.list_secrets.return_value = mock_response
|
|
228
|
+
|
|
229
|
+
result = patched_vault_client.list_user_secret_keys(user_id="test-user")
|
|
230
|
+
|
|
231
|
+
assert result == ["service1", "service2", "service3", "service4"]
|
|
232
|
+
patched_vault_client.client.secrets.kv.v2.list_secrets.assert_called_once_with(
|
|
233
|
+
path="path/dev/test-user", mount_point="secret"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
162
237
|
def test_read_user_service_secret_error(patched_vault_client):
|
|
163
238
|
"""Test read_user_service_secret when an unexpected exception occurs."""
|
|
164
239
|
patched_vault_client.client.secrets.kv.v2.read_secret_version.side_effect = Exception("Read error")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|