diff --git a/languages/python/bitwarden_sdk/bitwarden_client.py b/languages/python/bitwarden_sdk/bitwarden_client.py index ac51e8dd9..d8ee2934e 100644 --- a/languages/python/bitwarden_sdk/bitwarden_client.py +++ b/languages/python/bitwarden_sdk/bitwarden_client.py @@ -1,16 +1,40 @@ import json from typing import Any, List, Optional from uuid import UUID + import bitwarden_py -from .schemas import (ClientSettings, Command, ResponseForSecretIdentifiersResponse, ResponseForSecretResponse, - ResponseForSecretsResponse, ResponseForSecretsDeleteResponse, SecretCreateRequest, - SecretGetRequest, SecretsGetRequest, SecretIdentifiersRequest, SecretPutRequest, - SecretsCommand, SecretsDeleteRequest, SecretsSyncRequest, AccessTokenLoginRequest, - ResponseForSecretsSyncResponse, ResponseForAccessTokenLoginResponse, - ResponseForProjectResponse, ProjectsCommand, ProjectCreateRequest, ProjectGetRequest, - ProjectPutRequest, ProjectsListRequest, ResponseForProjectsResponse, - ResponseForProjectsDeleteResponse, ProjectsDeleteRequest) +from .schemas import ( + AccessTokenLoginRequest, + ClientSettings, + Command, + GeneratorsCommand, + PasswordGeneratorRequest, + ProjectCreateRequest, + ProjectGetRequest, + ProjectPutRequest, + ProjectsCommand, + ProjectsDeleteRequest, + ProjectsListRequest, + ResponseForAccessTokenLoginResponse, + ResponseForProjectResponse, + ResponseForProjectsDeleteResponse, + ResponseForProjectsResponse, + ResponseForSecretIdentifiersResponse, + ResponseForSecretResponse, + ResponseForSecretsDeleteResponse, + ResponseForSecretsResponse, + ResponseForSecretsSyncResponse, + ResponseForString, + SecretCreateRequest, + SecretGetRequest, + SecretIdentifiersRequest, + SecretPutRequest, + SecretsCommand, + SecretsDeleteRequest, + SecretsGetRequest, + SecretsSyncRequest, +) class BitwardenClient: @@ -30,11 +54,14 @@ def secrets(self): def projects(self): return ProjectsClient(self) + def generators(self): + return GeneratorsClient(self) + def _run_command(self, command: Command) -> Any: response_json = self.inner.run_command(json.dumps(command.to_dict())) response = json.loads(response_json) - if response["success"] == False: + if response["success"] is False: raise Exception(response["errorMessage"]) return response @@ -44,127 +71,529 @@ class AuthClient: def __init__(self, client: BitwardenClient): self.client = client - def login_access_token(self, access_token: str, - state_file: str = None) -> ResponseForAccessTokenLoginResponse: + def login_access_token( + self, access_token: str, state_file: str = None + ) -> ResponseForAccessTokenLoginResponse: result = self.client._run_command( - Command(login_access_token=AccessTokenLoginRequest(access_token, state_file)) + Command( + login_access_token=AccessTokenLoginRequest(access_token, state_file) + ) ) return ResponseForAccessTokenLoginResponse.from_dict(result) class SecretsClient: + """ + A client for managing secrets in Bitwarden Secrets Manager. + + This client provides methods to create, read, update, delete, and synchronize secrets. + All operations require authentication with an access token. + """ + def __init__(self, client: BitwardenClient): self.client = client def get(self, id: str) -> ResponseForSecretResponse: + """ + Retrieve a single secret by its UUID. If you need to retrieve multiple secrets, + consider using the get_by_ids() method to minimize network requests. + + Args: + id (str): The UUID of the secret to retrieve + + Returns: + ResponseForSecretResponse: A response containing the secret data if successful, + or error information if the operation failed + + Raises: + Exception: If the request fails due to network issues, authentication problems, + if the secret doesn't exist, or read access is denied + + Note: + Requires authentication with an access token that has read access to the + project containing the secret. + """ result = self.client._run_command( Command(secrets=SecretsCommand(get=SecretGetRequest(id))) ) return ResponseForSecretResponse.from_dict(result) def get_by_ids(self, ids: List[UUID]) -> ResponseForSecretsResponse: + """ + Retrieve multiple secrets by their UUIDs. + + Args: + ids (List[UUID]): A list of UUIDs of the secrets to retrieve + + Returns: + ResponseForSecretsResponse: A response containing a list of secret data if successful, + or error information if the operation failed + + Raises: + Exception: If the request fails due to network issues, authentication problems, + if the secrets don't exist, or read access is denied + + Note: + Requires authentication with an access token that has read access to the + project containing the secrets. + """ result = self.client._run_command( - Command(secrets=SecretsCommand( - get_by_ids=SecretsGetRequest(ids)) - )) + Command(secrets=SecretsCommand(get_by_ids=SecretsGetRequest(ids))) + ) return ResponseForSecretsResponse.from_dict(result) def create( - self, - organization_id: UUID, - key: str, - value: str, - note: Optional[str], - project_ids: Optional[List[UUID]] = None, + self, + organization_id: UUID, + key: str, + value: str, + note: Optional[str], + project_ids: Optional[List[UUID]] = None, ) -> ResponseForSecretResponse: + """ + Create a new secret in the specified organization. + + Args: + organization_id (UUID): The UUID of the organization where the secret will be created + key (str): The name of the secret + value (str): The secret value to store (e.g., password, API key, certificate) + note (Optional[str]): Optional note or description of the secret. If None, an empty string is used + project_ids (Optional[List[UUID]]): Optional list of project IDs that this secret should be associated with + + Returns: + ResponseForSecretResponse: A response containing the newly created secret data if successful, + or error information if the operation failed + + Raises: + Exception: If the request fails due to network issues, authentication problems, + invalid input data, or write access is denied + + Note: + Requires authentication with an access token that has write permissions + for the specified organization. + """ if note is None: # secrets api does not accept empty notes note = "" result = self.client._run_command( - Command(secrets=SecretsCommand( - create=SecretCreateRequest(key, note, organization_id, value, project_ids))) + Command( + secrets=SecretsCommand( + create=SecretCreateRequest( + key, note, organization_id, value, project_ids + ) + ) + ) ) return ResponseForSecretResponse.from_dict(result) def list(self, organization_id: str) -> ResponseForSecretIdentifiersResponse: + """ + List all secret identifiers for the specified organization. + + This method returns basic information (ID, key, organization ID) for all secrets + that the authenticated user has access to within the organization. It does not include + secret values. To retrieve the actual secret values, use the get() or get_by_ids() methods + with the IDs returned by this method. + + Args: + organization_id (str): The UUID of the organization to list secrets from + + Returns: + ResponseForSecretIdentifiersResponse: A response containing a list of secret identifiers + if successful, or error information if the operation failed + + Raises: + Exception: If the request fails due to network issues, authentication problems, + if the organization doesn't exist, or access is denied + + Note: + Requires authentication with an access token that has read permissions + for the specified organization. + """ result = self.client._run_command( - Command(secrets=SecretsCommand( - list=SecretIdentifiersRequest(organization_id))) + Command( + secrets=SecretsCommand(list=SecretIdentifiersRequest(organization_id)) + ) ) return ResponseForSecretIdentifiersResponse.from_dict(result) def update( - self, - organization_id: str, - id: str, - key: str, - value: str, - note: Optional[str], - project_ids: Optional[List[UUID]] = None, + self, + organization_id: str, + id: str, + key: str, + value: str, + note: Optional[str], + project_ids: Optional[List[UUID]] = None, ) -> ResponseForSecretResponse: + """ + Update an existing secret with new data. + + Args: + organization_id (str): The UUID of the organization containing the secret + id (str): The UUID of the secret to update + key (str): The updated name of the secret + value (str): The updated secret value + note (Optional[str]): Updated note or description for the secret. If None, an empty string is used + project_ids (Optional[List[UUID]]): Updated list of project IDs that this secret should be associated with + + Returns: + ResponseForSecretResponse: A response containing the updated secret data if successful, + or error information if the operation failed + + Raises: + Exception: If the request fails due to network issues, authentication problems, + insufficient permissions, or if the secret doesn't exist + + Note: + Requires authentication with an access token that has write permissions + for the secret. All fields are updated with the provided values, so ensure + all parameters contain the desired final state of the secret. + """ if note is None: # secrets api does not accept empty notes note = "" result = self.client._run_command( - Command(secrets=SecretsCommand(update=SecretPutRequest( - id, key, note, organization_id, value, project_ids))) + Command( + secrets=SecretsCommand( + update=SecretPutRequest( + id, key, note, organization_id, value, project_ids + ) + ) + ) ) return ResponseForSecretResponse.from_dict(result) def delete(self, ids: List[str]) -> ResponseForSecretsDeleteResponse: + """ + Delete one or more secrets by their UUID(s). + + Args: + ids (List[str]): A list of UUIDs for the secrets to delete + + Returns: + ResponseForSecretsDeleteResponse: A response containing the results of the deletion + operation, including any errors for individual secrets + + Raises: + Exception: If the request fails due to network issues or authentication problems + + Note: + Requires authentication with an access token that has write permissions + for the secret(s). The response will contain individual success/failure status + for each secret ID provided. Some secrets may be successfully deleted while + others fail due to permissions or other issues. + """ result = self.client._run_command( Command(secrets=SecretsCommand(delete=SecretsDeleteRequest(ids))) ) return ResponseForSecretsDeleteResponse.from_dict(result) - def sync(self, organization_id: str, last_synced_date: Optional[str]) -> ResponseForSecretsSyncResponse: + def sync( + self, organization_id: str, last_synced_date: Optional[str] + ) -> ResponseForSecretsSyncResponse: + """ + Synchronize secrets for the specified organization since a given date. If no + last_synced_date is provided, all secrets will be returned. + + This method retrieves all secrets accessible by the authenticated machine account. + If a last_synced_date is provided, it will only return secrets if there have been + changes since that date. This is useful for efficient incremental synchronization. + + Args: + organization_id (str): The UUID of the organization to sync secrets from + last_synced_date (Optional[str]): Optional datetime string representing + when secrets were last synchronized. If provided, + only changes since this date will be included + + Returns: + ResponseForSecretsSyncResponse: A response containing sync results with a flag + indicating if changes occurred, and the secret data + if changes were detected + + Raises: + Exception: If the request fails due to network issues, authentication problems, + or if the organization doesn't exist or access is denied + + Note: + Requires authentication with an access token that has read permissions + for the specified organization. Use this method for efficient bulk operations + and synchronization workflows. + """ result = self.client._run_command( - Command(secrets=SecretsCommand(sync=SecretsSyncRequest(organization_id, last_synced_date))) + Command( + secrets=SecretsCommand( + sync=SecretsSyncRequest(organization_id, last_synced_date) + ) + ) ) return ResponseForSecretsSyncResponse.from_dict(result) class ProjectsClient: + """ + A client for managing projects in Bitwarden Secrets Manager. + + This client provides methods to create, read, update, delete, and list projects + within Bitwarden organizations. Projects are used to organize and control access + to secrets. All operations require authentication with an access token. + """ + def __init__(self, client: BitwardenClient): self.client = client def get(self, id: str) -> ResponseForProjectResponse: + """ + Retrieve a project by its UUID. + + Args: + id (str): The UUID of the project to retrieve + + Returns: + ResponseForProjectResponse: A response containing the project data if successful, + or error information if the operation failed + + Raises: + Exception: If the request fails due to network issues, authentication problems, + or if the project doesn't exist or access is denied + + Note: + Requires authentication with an access token that has read permissions + for the project's organization. + """ result = self.client._run_command( Command(projects=ProjectsCommand(get=ProjectGetRequest(id))) ) return ResponseForProjectResponse.from_dict(result) - def create(self, - organization_id: str, - name: str, - ) -> ResponseForProjectResponse: + def create( + self, + organization_id: str, + name: str, + ) -> ResponseForProjectResponse: + """ + Create a new project in the specified organization. + + Args: + organization_id (str): The UUID of the organization where the project will be created + name (str): The name of the project + + Returns: + ResponseForProjectResponse: A response containing the newly created project data if successful, + or error information if the operation failed + + Raises: + Exception: If the request fails due to network issues, authentication problems, + insufficient permissions, or invalid input data + + Note: + Requires authentication with an access token that has create permissions + for the specified organization. + """ result = self.client._run_command( - Command(projects=ProjectsCommand( - create=ProjectCreateRequest(name, organization_id))) + Command( + projects=ProjectsCommand( + create=ProjectCreateRequest(name, organization_id) + ) + ) ) return ResponseForProjectResponse.from_dict(result) def list(self, organization_id: str) -> ResponseForProjectsResponse: + """ + List all projects for the specified organization. + + This method returns information about all projects that the authenticated account + has access to within the organization. + + Args: + organization_id (str): The UUID of the organization to list projects from + + Returns: + ResponseForProjectsResponse: A response containing a list of project data if successful, + or error information if the operation failed + + Raises: + Exception: If the request fails due to network issues, authentication problems, + if the organization doesn't exist, or access is denied + + Note: + Requires authentication with an access token that has read permissions + for the specified organization. + """ result = self.client._run_command( - Command(projects=ProjectsCommand( - list=ProjectsListRequest(organization_id))) + Command(projects=ProjectsCommand(list=ProjectsListRequest(organization_id))) ) return ResponseForProjectsResponse.from_dict(result) def update( - self, - organization_id: str, - id: str, - name: str, + self, + organization_id: str, + id: str, + name: str, ) -> ResponseForProjectResponse: + """ + Update an existing project with new data. + + Args: + organization_id (str): The UUID of the organization containing the project + id (str): The UUID of the project to update + name (str): The updated name of the project + + Returns: + ResponseForProjectResponse: A response containing the updated project data if successful, + or error information if the operation failed + + Raises: + Exception: If the request fails due to network issues, authentication problems, + insufficient permissions, invalid input data, or if the project doesn't exist + + Note: + Requires authentication with an access token that has write permissions + for the project. The project name will be updated to the provided value. + """ result = self.client._run_command( - Command(projects=ProjectsCommand(update=ProjectPutRequest( - id, name, organization_id))) + Command( + projects=ProjectsCommand( + update=ProjectPutRequest(id, name, organization_id) + ) + ) ) return ResponseForProjectResponse.from_dict(result) def delete(self, ids: List[str]) -> ResponseForProjectsDeleteResponse: + """ + Delete one or more projects by their UUIDs. + + Args: + ids (List[str]): A list of UUIDs of the projects to delete + + Returns: + ResponseForProjectsDeleteResponse: A response containing the results of the deletion + operation, including any errors for individual projects + + Raises: + Exception: If the request fails due to network issues or authentication problems + + Note: + Requires authentication with an access token that has delete permissions + for the projects. The response will contain individual success/failure status + for each project ID provided. Some projects may be successfully deleted while + others fail due to permissions or other issues. + """ result = self.client._run_command( Command(projects=ProjectsCommand(delete=ProjectsDeleteRequest(ids))) ) return ResponseForProjectsDeleteResponse.from_dict(result) + + +class GeneratorsClient: + """ + A client to generate secrets. Does not require authentication. + """ + + def __init__(self, client: BitwardenClient): + self.client = client + + def generate( + self, + length: int = 24, + avoid_ambiguous: bool = True, + lowercase: bool = True, + uppercase: bool = True, + numbers: bool = True, + special: bool = True, + min_lowercase: Optional[int] = None, + min_number: Optional[int] = None, + min_special: Optional[int] = None, + min_uppercase: Optional[int] = None, + ) -> str: + """ + Generate a secret. + + Args: + length (int): Length of the password (default: 24) + avoid_ambiguous (bool): Exclude ambiguous characters like 0/O, 1/l/I (default: True) + lowercase (bool): Include the lowercase character set (default: True) + uppercase (bool): Include the uppercase character set (default: True) + numbers (bool): Include the numeric character set (default: True) + special (bool): Include the special character set (default: True) + min_lowercase (Optional[int]): Minimum lowercase characters to include (default: None) + min_uppercase (Optional[int]): Minimum uppercase characters to include (default: None) + min_number (Optional[int]): Minimum numeric characters to include (default: None) + min_special (Optional[int]): Minimum special characters to include (default: None) + + Returns: + str: + Generated secret as a string + + Raises: + ValueError: + If at least one of lowercase, uppercase, numbers, or special characters are + not greater than 0 + + ValueError: + If one of min_lowercase, min_uppercase, min_number, or min_special is a negative + number + + ValueError: + If one of min_lowercase, min_uppercase, min_number, or min_special is provided, + but that character set is disabled + + ValueError: + If the sum of minimum character set requirements exceeds requested secret length + + Exception: + If secret generation fails for any other reason. This would generally indicate a problem + with the FFI layer or system configuration. + """ + if length <= 0: + raise ValueError("length must be greater than 0") + + if not any([lowercase, uppercase, numbers, special]): + raise ValueError( + "At least one of lowercase, uppercase, numbers, or special must be enabled" + ) + + def _validate_min(name: str, value: Optional[int], enabled: bool) -> int: + if value is None: + return 0 + if value < 0: + raise ValueError(f"{name} cannot be negative") + if not enabled and value > 0: + raise ValueError(f"{name} > 0 but its character set is disabled") + return int(value) + + min_lc = _validate_min("min_lowercase", min_lowercase, lowercase) + min_uc = _validate_min("min_uppercase", min_uppercase, uppercase) + min_num = _validate_min("min_number", min_number, numbers) + min_sp = _validate_min("min_special", min_special, special) + + if (min_lc + min_uc + min_num + min_sp) > length: + raise ValueError("Sum of minimum requirements exceeds requested length") + + # create the password generator request + password_request = PasswordGeneratorRequest( + avoid_ambiguous=bool(avoid_ambiguous), + length=int(length), + lowercase=bool(lowercase), + uppercase=bool(uppercase), + numbers=bool(numbers), + special=bool(special), + min_lowercase=min_lc if min_lowercase is not None else None, + min_uppercase=min_uc if min_uppercase is not None else None, + min_number=min_num if min_number is not None else None, + min_special=min_sp if min_special is not None else None, + ) + + result = self.client._run_command( + command=Command( + generators=GeneratorsCommand(generate_password=password_request) + ) + ) + response = ResponseForString.from_dict(result) + + if not response.success: + raise Exception(response.error_message or "Secret generation failed") + + return response.data diff --git a/languages/python/example.py b/languages/python/example.py index ee3690dd0..2ff9af126 100755 --- a/languages/python/example.py +++ b/languages/python/example.py @@ -21,6 +21,25 @@ logging.basicConfig(level=logging.DEBUG) organization_id = os.getenv("ORGANIZATION_ID") +# -- Example Generator Commands -- +# Note: using the generator does not require authentication with a server +generated_secret = client.generators().generate() # using default params +very_strong_secret = client.generators().generate( + length=128, + avoid_ambiguous=False, + lowercase=True, + uppercase=True, + numbers=True, + special=True, + min_lowercase=2, + min_uppercase=2, + min_number=4, + min_special=4, +) + +print(f"generated secret: {generated_secret}") +print(f"very strong secret: {very_strong_secret}") + # Set the state file location # Note: the path must exist, the file will be created & managed by the sdk state_path = os.getenv("STATE_FILE") diff --git a/languages/python/test/crud.py b/languages/python/test/crud.py index 570d5ac23..693c57263 100755 --- a/languages/python/test/crud.py +++ b/languages/python/test/crud.py @@ -1,4 +1,4 @@ -import logging +import logging # noqa: F401 import uuid import os import sys @@ -32,6 +32,7 @@ # Track test failures test_failures = 0 + def run_test(operation_name, test_func): global test_failures try: @@ -76,7 +77,9 @@ def test_secret_edit(): return "something-new" in secret.data.key def test_secret_get_by_ids(): - secrets_retrieved = client.secrets().get_by_ids([uuid.uuid4(), uuid.uuid4(), uuid.uuid4()]) + secrets_retrieved = client.secrets().get_by_ids( + [uuid.uuid4(), uuid.uuid4(), uuid.uuid4()] + ) return secrets_retrieved.data.data[0].key == "FERRIS" def test_secret_sync(): @@ -94,8 +97,6 @@ def test_secret_sync(): return True - - def test_secret_delete(): result = client.secrets().delete([uuid.uuid4(), uuid.uuid4(), uuid.uuid4()]) return result.success is True @@ -124,9 +125,7 @@ def test_project_create(): def test_project_edit(): updated = client.projects().update( - organization_id, - uuid.uuid4(), - "new-project-name" + organization_id, uuid.uuid4(), "new-project-name" ) return "new-project-name" in updated.data.name @@ -141,6 +140,198 @@ def test_project_delete(): run_test("project delete", test_project_delete) +def generators(): + LOWERCASE_CHARACTERS = "abcdefghijklmnopqrstuvwxyz" + UPPERCASE_CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + NUMERIC_CHARACTERS = "0123456789" + SPECIAL_CHARACTERS = "!@#$%^&*" # https://github.com/bitwarden/sdk-internal/blob/d7ce769/crates/bitwarden-generators/src/password.rs#L80 + AMBIGUOUS_CHARACTERS = "IOl01" # https://github.com/bitwarden/sdk-internal/blob/d7ce769/crates/bitwarden-generators/src/password.rs#L77-L79 + + def test_generator_with_default_params(): + generated_secret = client.generators().generate() + + # should be exactly 24 chars + len = generated_secret.__len__() + if len != 24: + return False + + # should contain lowercase chars + if not any(c in LOWERCASE_CHARACTERS for c in generated_secret): + return False + + # should contain uppercase chars + if not any(c in UPPERCASE_CHARACTERS for c in generated_secret): + return False + + # should contain numeric chars + if not any(c in NUMERIC_CHARACTERS for c in generated_secret): + return False + + # should contain special chars: + if not any(c in SPECIAL_CHARACTERS for c in generated_secret): + return False + + return True + + def test_generator_with_all_params(): + very_strong_secret = client.generators().generate( + length=128, + avoid_ambiguous=False, + lowercase=True, + uppercase=True, + numbers=True, + special=True, + min_lowercase=2, + min_uppercase=2, + min_number=4, + min_special=4, + ) + + # should be exactly 128 chars + len = very_strong_secret.__len__() + if len != 128: + return False + + # should contain ambiguous chars: + if not any(c in AMBIGUOUS_CHARACTERS for c in very_strong_secret): + return False + + # should contain lowercase chars: + if not any(c in LOWERCASE_CHARACTERS for c in very_strong_secret): + return False + + # should contain uppercase chars: + if not any(c in UPPERCASE_CHARACTERS for c in very_strong_secret): + return False + + # should contain special chars: + if not any(c in SPECIAL_CHARACTERS for c in very_strong_secret): + return False + + # should contain at least 2 lowercase chars: + lowercase_count = sum( + 1 for c in very_strong_secret if c in LOWERCASE_CHARACTERS + ) + if lowercase_count < 2: + return False + + # should contain at least 2 uppercase chars: + uppercase_count = sum( + 1 for c in very_strong_secret if c in UPPERCASE_CHARACTERS + ) + if uppercase_count < 2: + return False + + # should contain at least 4 numeric chars: + numeric_count = sum(1 for c in very_strong_secret if c in NUMERIC_CHARACTERS) + if numeric_count < 4: + return False + + # should contain at least 4 special chars: + special_count = sum(1 for c in very_strong_secret if c in SPECIAL_CHARACTERS) + if special_count < 4: + return False + + return True + + def test_generator_all_char_sets_disabled(): + """Test that disabling all character sets raises ValueError""" + try: + client.generators().generate( + lowercase=False, + uppercase=False, + numbers=False, + special=False, + ) + # if we get here, the test failed - no exception was raised + return False + except ValueError: + # expected behavior + return True + except Exception: + # unexpected exception type + return False + + def test_generator_negative_min_values(): + """Test that negative minimum values raise ValueError""" + test_cases = [ + {"min_lowercase": -1}, + {"min_uppercase": -1}, + {"min_number": -1}, + {"min_special": -1}, + ] + + for params in test_cases: + try: + client.generators().generate(**params) + # if we get here, the test failed - no exception was raised + return False + except ValueError: + # expected behavior + continue + except Exception: + # unexpected exception type + return False + + return True + + def test_generator_contradicting_minimum_char_sets(): + """Test that setting min values for disabled character sets raises ValueError""" + test_cases = [ + {"lowercase": False, "min_lowercase": 1}, + {"uppercase": False, "min_uppercase": 1}, + {"numbers": False, "min_number": 1}, + {"special": False, "min_special": 1}, + ] + + for params in test_cases: + try: + client.generators().generate(**params) + # if we get here, the test failed - no exception was raised + return False + except ValueError: + # expected behavior + continue + except Exception: + # unexpected exception type + return False + + return True + + def test_generator_with_min_char_sets_greater_than_length(): + """Test that setting sum of min values greater than length raises ValueError""" + try: + client.generators().generate( + length=5, + min_lowercase=2, + min_uppercase=2, + min_number=2, + ) + # if we get here, the test failed - no exception was raised + return False + except ValueError: + # expected behavior + return True + except Exception: + # unexpected exception type + return False + + run_test("generate with default params", test_generator_with_default_params) + run_test("generate with all params", test_generator_with_all_params) + run_test( + "generate with all char sets disabled", test_generator_all_char_sets_disabled + ) + run_test("generate with negative min values", test_generator_negative_min_values) + run_test( + "generate with contradicting minimum char sets", + test_generator_contradicting_minimum_char_sets, + ) + run_test( + "generate with min char sets greater than length", + test_generator_with_min_char_sets_greater_than_length, + ) + + def main(): print("Testing secrets...") secrets() @@ -148,12 +339,16 @@ def main(): print("Testing projects...") projects() + print() + + print("Testing secrets generator...") + generators() if test_failures > 0: print(f"\n❌ {test_failures} test(s) failed") sys.exit(1) else: - print(f"\n✅ All tests passed") + print("\n✅ All tests passed") sys.exit(0)