|
1 | 1 | import pytest |
2 | | -from unittest.mock import patch |
| 2 | +from unittest.mock import patch, Mock |
3 | 3 | from app.db.models import DBPrivateAIKey, DBTeam, DBUser, DBProduct, DBTeamProduct |
4 | 4 | from datetime import datetime, UTC |
5 | 5 | from app.core.security import get_password_hash |
6 | 6 | from requests.exceptions import HTTPError |
| 7 | +from fastapi import status, HTTPException |
7 | 8 | from app.core.resource_limits import ( |
8 | 9 | DEFAULT_MAX_SPEND, |
9 | 10 | DEFAULT_RPM_PER_KEY, |
@@ -1905,3 +1906,190 @@ def test_delete_private_ai_key_litellm_service_unavailable(mock_post, client, ad |
1905 | 1906 | json={"keys": [test_key.litellm_token]} |
1906 | 1907 | ) |
1907 | 1908 | db.commit() |
| 1909 | + |
| 1910 | +@patch("app.services.litellm.requests.post") |
| 1911 | +@patch("app.db.postgres.PostgresManager.create_database") |
| 1912 | +def test_create_private_ai_key_cleanup_on_vector_db_failure(mock_create_db, mock_post, client, test_token, test_region, test_user): |
| 1913 | + """ |
| 1914 | + Given a user creates a private AI key |
| 1915 | + When the vector database creation fails after LiteLLM token is created |
| 1916 | + Then the LiteLLM token should be cleaned up and an error returned |
| 1917 | + """ |
| 1918 | + # Mock successful LiteLLM token creation |
| 1919 | + mock_post.return_value.status_code = 200 |
| 1920 | + mock_post.return_value.json.return_value = {"key": "test-private-key-123"} |
| 1921 | + mock_post.return_value.raise_for_status.return_value = None |
| 1922 | + |
| 1923 | + # Mock vector database creation failure |
| 1924 | + mock_create_db.side_effect = Exception("Database creation failed") |
| 1925 | + |
| 1926 | + response = client.post( |
| 1927 | + "/private-ai-keys/", |
| 1928 | + headers={"Authorization": f"Bearer {test_token}"}, |
| 1929 | + json={ |
| 1930 | + "region_id": test_region.id, |
| 1931 | + "name": "Test AI Key" |
| 1932 | + } |
| 1933 | + ) |
| 1934 | + |
| 1935 | + # Verify the response indicates failure |
| 1936 | + assert response.status_code == 500 |
| 1937 | + assert "Failed to create vector database" in response.json()["detail"] |
| 1938 | + |
| 1939 | + # Verify LiteLLM token was cleaned up (delete API was called) |
| 1940 | + # First call is for token creation, second call is for cleanup |
| 1941 | + assert mock_post.call_count == 2 |
| 1942 | + |
| 1943 | + # Verify the cleanup call |
| 1944 | + cleanup_call = mock_post.call_args_list[1] |
| 1945 | + assert cleanup_call[0][0] == f"{test_region.litellm_api_url}/key/delete" |
| 1946 | + assert cleanup_call[1]["headers"]["Authorization"] == f"Bearer {test_region.litellm_api_key}" |
| 1947 | + assert cleanup_call[1]["json"]["keys"] == ["test-private-key-123"] |
| 1948 | + |
| 1949 | + # Verify no key was stored in the database |
| 1950 | + stored_keys = client.get( |
| 1951 | + "/private-ai-keys/", |
| 1952 | + headers={"Authorization": f"Bearer {test_token}"} |
| 1953 | + ).json() |
| 1954 | + assert len([k for k in stored_keys if k["name"] == "Test AI Key"]) == 0 |
| 1955 | + |
| 1956 | +@patch("app.services.litellm.requests.post") |
| 1957 | +@patch("app.db.postgres.PostgresManager.create_database") |
| 1958 | +@patch("app.db.postgres.PostgresManager.delete_database") |
| 1959 | +@patch("sqlalchemy.orm.Session.commit") |
| 1960 | +def test_create_private_ai_key_cleanup_on_db_storage_failure(mock_commit, mock_delete_db, mock_create_db, mock_post, client, test_token, test_region, test_user): |
| 1961 | + """ |
| 1962 | + Given a user creates a private AI key |
| 1963 | + When the database storage fails after both LiteLLM token and vector DB are created |
| 1964 | + Then both resources should be cleaned up and an error returned |
| 1965 | + """ |
| 1966 | + # Mock successful LiteLLM token creation |
| 1967 | + mock_post.return_value.status_code = 200 |
| 1968 | + mock_post.return_value.json.return_value = {"key": "test-private-key-123"} |
| 1969 | + mock_post.return_value.raise_for_status.return_value = None |
| 1970 | + |
| 1971 | + # Mock successful vector database creation |
| 1972 | + mock_create_db.return_value = { |
| 1973 | + "database_name": "test_db_123", |
| 1974 | + "database_host": "test-host", |
| 1975 | + "database_username": "test_user", |
| 1976 | + "database_password": "test_pass" |
| 1977 | + } |
| 1978 | + |
| 1979 | + # Mock successful vector database deletion |
| 1980 | + mock_delete_db.return_value = None |
| 1981 | + |
| 1982 | + # Mock database storage failure |
| 1983 | + mock_commit.side_effect = Exception("Database storage failed") |
| 1984 | + |
| 1985 | + response = client.post( |
| 1986 | + "/private-ai-keys/", |
| 1987 | + headers={"Authorization": f"Bearer {test_token}"}, |
| 1988 | + json={ |
| 1989 | + "region_id": test_region.id, |
| 1990 | + "name": "Test AI Key" |
| 1991 | + } |
| 1992 | + ) |
| 1993 | + |
| 1994 | + # Verify the response indicates failure |
| 1995 | + assert response.status_code == 500 |
| 1996 | + assert "Failed to create private AI key" in response.json()["detail"] |
| 1997 | + |
| 1998 | + # Verify LiteLLM token was cleaned up |
| 1999 | + assert mock_post.call_count == 2 |
| 2000 | + cleanup_call = mock_post.call_args_list[1] |
| 2001 | + assert cleanup_call[0][0] == f"{test_region.litellm_api_url}/key/delete" |
| 2002 | + assert cleanup_call[1]["json"]["keys"] == ["test-private-key-123"] |
| 2003 | + |
| 2004 | + # Verify vector database was cleaned up |
| 2005 | + mock_delete_db.assert_called_once_with("test_db_123") |
| 2006 | + |
| 2007 | + # Verify no key was stored in the database |
| 2008 | + stored_keys = client.get( |
| 2009 | + "/private-ai-keys/", |
| 2010 | + headers={"Authorization": f"Bearer {test_token}"} |
| 2011 | + ).json() |
| 2012 | + assert len([k for k in stored_keys if k["name"] == "Test AI Key"]) == 0 |
| 2013 | + |
| 2014 | +@patch("app.services.litellm.requests.post") |
| 2015 | +@patch("app.db.postgres.PostgresManager.create_database") |
| 2016 | +def test_create_private_ai_key_cleanup_failure_handling(mock_create_db, mock_post, client, test_token, test_region, test_user): |
| 2017 | + """ |
| 2018 | + Given a user creates a private AI key |
| 2019 | + When the cleanup process itself fails |
| 2020 | + Then the original error should still be returned to the user |
| 2021 | + """ |
| 2022 | + # Mock successful LiteLLM token creation |
| 2023 | + mock_post.return_value.status_code = 200 |
| 2024 | + mock_post.return_value.json.return_value = {"key": "test-private-key-123"} |
| 2025 | + mock_post.return_value.raise_for_status.return_value = None |
| 2026 | + |
| 2027 | + # Mock vector database creation failure |
| 2028 | + mock_create_db.side_effect = Exception("Database creation failed") |
| 2029 | + |
| 2030 | + # Mock cleanup failure - the second call to requests.post will be for cleanup |
| 2031 | + # First call succeeds, second call fails |
| 2032 | + mock_post.side_effect = [ |
| 2033 | + Mock( |
| 2034 | + status_code=200, |
| 2035 | + json=Mock(return_value={"key": "test-private-key-123"}), |
| 2036 | + raise_for_status=Mock(return_value=None) |
| 2037 | + ), |
| 2038 | + Mock( |
| 2039 | + status_code=500, |
| 2040 | + raise_for_status=Mock(side_effect=HTTPError("Cleanup failed")) |
| 2041 | + ) |
| 2042 | + ] |
| 2043 | + |
| 2044 | + response = client.post( |
| 2045 | + "/private-ai-keys/", |
| 2046 | + headers={"Authorization": f"Bearer {test_token}"}, |
| 2047 | + json={ |
| 2048 | + "region_id": test_region.id, |
| 2049 | + "name": "Test AI Key" |
| 2050 | + } |
| 2051 | + ) |
| 2052 | + |
| 2053 | + # Verify the response indicates the original failure, not cleanup failure |
| 2054 | + assert response.status_code == 500 |
| 2055 | + assert "Failed to create vector database" in response.json()["detail"] |
| 2056 | + assert "Database creation failed" in response.json()["detail"] |
| 2057 | + |
| 2058 | + # Verify cleanup was attempted |
| 2059 | + assert mock_post.call_count == 2 |
| 2060 | + |
| 2061 | +@patch("app.services.litellm.requests.post") |
| 2062 | +@patch("app.db.postgres.PostgresManager.create_database") |
| 2063 | +def test_create_private_ai_key_http_exception_preservation(mock_create_db, mock_post, client, test_token, test_region, test_user): |
| 2064 | + """ |
| 2065 | + Given a user creates a private AI key |
| 2066 | + When an HTTPException is raised during creation |
| 2067 | + Then the original HTTPException should be preserved and returned |
| 2068 | + """ |
| 2069 | + # Mock successful LiteLLM token creation |
| 2070 | + mock_post.return_value.status_code = 200 |
| 2071 | + mock_post.return_value.json.return_value = {"key": "test-private-key-123"} |
| 2072 | + mock_post.return_value.raise_for_status.return_value = None |
| 2073 | + |
| 2074 | + # Mock vector database creation failure with HTTPException |
| 2075 | + mock_create_db.side_effect = HTTPException( |
| 2076 | + status_code=status.HTTP_400_BAD_REQUEST, |
| 2077 | + detail="Invalid database configuration" |
| 2078 | + ) |
| 2079 | + |
| 2080 | + response = client.post( |
| 2081 | + "/private-ai-keys/", |
| 2082 | + headers={"Authorization": f"Bearer {test_token}"}, |
| 2083 | + json={ |
| 2084 | + "region_id": test_region.id, |
| 2085 | + "name": "Test AI Key" |
| 2086 | + } |
| 2087 | + ) |
| 2088 | + |
| 2089 | + # Verify the original HTTPException is preserved |
| 2090 | + assert response.status_code == 500 |
| 2091 | + assert "Failed to create vector database" in response.json()["detail"] |
| 2092 | + assert "Invalid database configuration" in response.json()["detail"] |
| 2093 | + |
| 2094 | + # Verify cleanup was attempted |
| 2095 | + assert mock_post.call_count == 2 |
0 commit comments