mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-02 10:52:22 +00:00
feat(post-install): rebuild Log2RAM with the journal and task logs kept on disk
This commit is contained in:
@@ -874,7 +874,7 @@ export function NotificationSettings() {
|
||||
// Flatten per-provider API keys
|
||||
if (cfg.ai_api_keys) {
|
||||
for (const [provider, key] of Object.entries(cfg.ai_api_keys)) {
|
||||
if (key) {
|
||||
if (key || provider === "ollama") {
|
||||
flat[`ai_api_key_${provider}`] = key
|
||||
}
|
||||
}
|
||||
@@ -2305,6 +2305,12 @@ export function NotificationSettings() {
|
||||
|
||||
// Restore previously saved model for the new provider (if any)
|
||||
const savedModel = config.ai_models?.[v] || ''
|
||||
setShowSecrets(p => ({ ...p, ai_key: false }))
|
||||
setRevealedSecrets(p => {
|
||||
const next = { ...p }
|
||||
delete next.ai_key
|
||||
return next
|
||||
})
|
||||
|
||||
updateConfig(p => ({
|
||||
...p,
|
||||
@@ -2364,11 +2370,13 @@ export function NotificationSettings() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* API Key (not shown for Ollama) */}
|
||||
{config.ai_provider !== "ollama" && (
|
||||
{/* Ollama gateways may require a key; native servers do not. */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-xs sm:text-sm text-foreground/80">{t("settings.notifications.ai.apiKey")}</Label>
|
||||
<Label htmlFor="notification-ai-api-key" className="text-xs sm:text-sm text-foreground/80">{t("settings.notifications.ai.apiKey")}</Label>
|
||||
{config.ai_provider === "ollama" && (
|
||||
<span className="text-xs text-muted-foreground">({t("settings.notifications.ui.optional")})</span>
|
||||
)}
|
||||
{AI_PROVIDERS.find(p => p.value === config.ai_provider)?.keyUrl && (
|
||||
<a
|
||||
href={AI_PROVIDERS.find(p => p.value === config.ai_provider)?.keyUrl}
|
||||
@@ -2382,9 +2390,10 @@ export function NotificationSettings() {
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
id="notification-ai-api-key"
|
||||
type={showSecrets["ai_key"] ? "text" : "password"}
|
||||
className="h-9 text-sm font-mono"
|
||||
placeholder="sk-..."
|
||||
placeholder={config.ai_provider === "ollama" ? "" : "sk-..."}
|
||||
value={secretValue("ai_key", config.ai_api_keys?.[config.ai_provider] || "")}
|
||||
onChange={e => updateConfig(p => ({
|
||||
...p,
|
||||
@@ -2402,8 +2411,12 @@ export function NotificationSettings() {
|
||||
{showSecrets["ai_key"] ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||
</button>
|
||||
</div>
|
||||
{config.ai_provider === "ollama" && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("settings.notifications.ai.ollamaApiKeyHint")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Model - selector with Load button for all providers */}
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -2312,6 +2312,7 @@
|
||||
"testing": "Testen…",
|
||||
"testConnection": "Testverbindung",
|
||||
"ollamaUrl": "Ollama-URL",
|
||||
"ollamaApiKeyHint": "Bei einem normalen Ollama-Server leer lassen. Wenn dein Proxy oder Gateway einen Schlüssel verlangt, wird er als Authorization: Bearer gesendet. Dieses Feld aktiviert keine Authentifizierung auf dem Server.",
|
||||
"getKey": "Holen Sie sich den Schlüssel",
|
||||
"loadModelsPlaceholder": "Klicken Sie auf „Laden“, um Modelle abzurufen",
|
||||
"selectModel": "Modell auswählen",
|
||||
|
||||
@@ -2439,6 +2439,7 @@
|
||||
"testing": "Testing…",
|
||||
"testConnection": "Test connection",
|
||||
"ollamaUrl": "Ollama URL",
|
||||
"ollamaApiKeyHint": "Leave empty for a standard Ollama server. If your proxy or gateway requires a key, it is sent as Authorization: Bearer. This field does not enable authentication on the server.",
|
||||
"getKey": "Get key",
|
||||
"loadModelsPlaceholder": "Click Load to fetch models",
|
||||
"selectModel": "Select model",
|
||||
|
||||
@@ -2465,6 +2465,7 @@
|
||||
"testing": "Pruebas…",
|
||||
"testConnection": "Conexión de prueba",
|
||||
"ollamaUrl": "URL de Ollama",
|
||||
"ollamaApiKeyHint": "Déjala vacía para un servidor Ollama estándar. Si tu proxy o pasarela requiere una clave, se envía como Authorization: Bearer. Este campo no activa la autenticación en el servidor.",
|
||||
"getKey": "Obtener clave",
|
||||
"loadModelsPlaceholder": "Haz clic en Cargar para buscar modelos",
|
||||
"selectModel": "Seleccionar modelo",
|
||||
|
||||
@@ -2312,6 +2312,7 @@
|
||||
"testing": "Essai…",
|
||||
"testConnection": "Tester la connexion",
|
||||
"ollamaUrl": "URL d'Ollama",
|
||||
"ollamaApiKeyHint": "Laissez ce champ vide pour un serveur Ollama standard. Si votre proxy ou passerelle exige une clé, elle est envoyée dans Authorization: Bearer. Ce champ n'active pas l'authentification sur le serveur.",
|
||||
"getKey": "Obtenir la clé",
|
||||
"loadModelsPlaceholder": "Cliquez sur Charger pour récupérer les modèles",
|
||||
"selectModel": "Sélectionnez le modèle",
|
||||
|
||||
@@ -2312,6 +2312,7 @@
|
||||
"testing": "Test in corso…",
|
||||
"testConnection": "Prova connessione",
|
||||
"ollamaUrl": "URL Ollama",
|
||||
"ollamaApiKeyHint": "Lascia vuoto per un server Ollama standard. Se il proxy o gateway richiede una chiave, viene inviata come Authorization: Bearer. Questo campo non abilita l'autenticazione sul server.",
|
||||
"getKey": "Ottieni chiave",
|
||||
"loadModelsPlaceholder": "Fai clic su Carica per recuperare i modelli",
|
||||
"selectModel": "Seleziona modello",
|
||||
|
||||
@@ -2312,6 +2312,7 @@
|
||||
"testing": "Testando…",
|
||||
"testConnection": "Conexão de teste",
|
||||
"ollamaUrl": "URL de Ollama",
|
||||
"ollamaApiKeyHint": "Deixa vazio para um servidor Ollama padrão. Se o proxy ou gateway exigir uma chave, esta é enviada como Authorization: Bearer. Este campo não ativa a autenticação no servidor.",
|
||||
"getKey": "Obter chave",
|
||||
"loadModelsPlaceholder": "Clique em Carregar para buscar modelos",
|
||||
"selectModel": "Selecione o modelo",
|
||||
|
||||
@@ -2311,6 +2311,7 @@
|
||||
"testing": "Testujem…",
|
||||
"testConnection": "Otestovať pripojenie",
|
||||
"ollamaUrl": "URL služby Ollama",
|
||||
"ollamaApiKeyHint": "Pre bežný server Ollama nechajte prázdne. Ak proxy alebo brána vyžaduje kľúč, odosiela sa ako Authorization: Bearer. Toto pole nezapína autentifikáciu na serveri.",
|
||||
"getKey": "Získať kľúč",
|
||||
"loadModelsPlaceholder": "Kliknutím na Načítať získate modely",
|
||||
"selectModel": "Vyberte model",
|
||||
|
||||
@@ -2312,6 +2312,7 @@
|
||||
"testing": "Testning…",
|
||||
"testConnection": "Testa anslutningen",
|
||||
"ollamaUrl": "Ollama URL",
|
||||
"ollamaApiKeyHint": "Lämna tomt för en vanlig Ollama-server. Om din proxy eller gateway kräver en nyckel skickas den som Authorization: Bearer. Fältet aktiverar inte autentisering på servern.",
|
||||
"getKey": "Få nyckel",
|
||||
"loadModelsPlaceholder": "Klicka på Ladda för att hämta modeller",
|
||||
"selectModel": "Välj modell",
|
||||
|
||||
@@ -7,6 +7,16 @@ from typing import Optional
|
||||
from .base import AIProvider, AIProviderError
|
||||
|
||||
|
||||
def ollama_headers(api_key: str = "") -> dict:
|
||||
"""Authenticate protected Ollama gateways without requiring a key locally."""
|
||||
if not isinstance(api_key, str) or len(api_key) > 512 or '\r' in api_key or '\n' in api_key:
|
||||
raise AIProviderError("Invalid Ollama API key")
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
if api_key.strip():
|
||||
headers['Authorization'] = f'Bearer {api_key.strip()}'
|
||||
return headers
|
||||
|
||||
|
||||
class OllamaProvider(AIProvider):
|
||||
"""Ollama provider for local AI execution."""
|
||||
|
||||
@@ -18,7 +28,7 @@ class OllamaProvider(AIProvider):
|
||||
"""Initialize Ollama provider.
|
||||
|
||||
Args:
|
||||
api_key: Not used for Ollama (local execution)
|
||||
api_key: Optional Bearer token for an authenticated Ollama gateway
|
||||
model: Model name (user must select from loaded models)
|
||||
base_url: Ollama server URL (default: http://localhost:11434)
|
||||
"""
|
||||
@@ -57,9 +67,7 @@ class OllamaProvider(AIProvider):
|
||||
}
|
||||
}
|
||||
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
headers = ollama_headers(self.api_key)
|
||||
|
||||
# Cloud models (e.g., kimi-k2.5:cloud, minimax-m2.7:cloud) need longer timeout
|
||||
# because requests go through: ProxMenux -> Ollama -> Cloud Provider -> back
|
||||
@@ -96,7 +104,8 @@ class OllamaProvider(AIProvider):
|
||||
# First check if server is running
|
||||
try:
|
||||
url = f"{self.base_url.rstrip('/')}/api/tags"
|
||||
req = urllib.request.Request(url, method='GET', headers={'User-Agent': 'ProxMenux/1.0'})
|
||||
headers = {'User-Agent': 'ProxMenux/1.0', **ollama_headers(self.api_key)}
|
||||
req = urllib.request.Request(url, method='GET', headers=headers)
|
||||
with urllib.request.urlopen(req, timeout=5) as resp:
|
||||
data = json.loads(resp.read().decode('utf-8'))
|
||||
|
||||
@@ -122,6 +131,13 @@ class OllamaProvider(AIProvider):
|
||||
'message': f"Model '{self.model}' not found. Available: {', '.join(display_models)}{'...' if len(full_model_names) > 5 else ''}",
|
||||
'model': self.model
|
||||
}
|
||||
except urllib.error.HTTPError as error:
|
||||
message = (
|
||||
f"HTTP {error.code}: Ollama authentication failed. Check the optional API key."
|
||||
if error.code in (401, 403)
|
||||
else f"HTTP {error.code}: Ollama request failed"
|
||||
)
|
||||
return {'success': False, 'message': message, 'model': self.model}
|
||||
except urllib.error.URLError:
|
||||
return {
|
||||
'success': False,
|
||||
|
||||
@@ -446,7 +446,7 @@ def get_provider_models():
|
||||
Request body:
|
||||
{
|
||||
"provider": "gemini|groq|openai|openrouter|ollama|anthropic",
|
||||
"api_key": "your-api-key", // Not needed for ollama
|
||||
"api_key": "your-api-key", // Optional for protected Ollama gateways
|
||||
"ollama_url": "http://localhost:11434", // Only for ollama
|
||||
"openai_base_url": "https://custom.endpoint/v1" // Optional for openai
|
||||
}
|
||||
@@ -498,9 +498,10 @@ def get_provider_models():
|
||||
if provider == 'ollama':
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
from ai_providers.ollama_provider import ollama_headers
|
||||
|
||||
url = f"{ollama_url.rstrip('/')}/api/tags"
|
||||
req = urllib.request.Request(url, method='GET')
|
||||
req = urllib.request.Request(url, method='GET', headers=ollama_headers(api_key))
|
||||
req.add_header('User-Agent', 'ProxMenux-Monitor/1.1')
|
||||
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
|
||||
@@ -86,6 +86,7 @@ SENSITIVE_KEYS = {
|
||||
'ai_api_key_anthropic',
|
||||
'ai_api_key_openai',
|
||||
'ai_api_key_openrouter',
|
||||
'ai_api_key_ollama',
|
||||
# `telegram.bot_token` — was previously listed as `telegram.token`,
|
||||
# which never matched the real config_key (`bot_token` in
|
||||
# CHANNEL_TYPES). The mismatch silently sent Telegram bot tokens
|
||||
@@ -3019,6 +3020,7 @@ class NotificationManager:
|
||||
current_provider = self._config.get('ai_provider', 'groq')
|
||||
_raw_keys = {
|
||||
'groq': self._config.get('ai_api_key_groq', ''),
|
||||
'ollama': self._config.get('ai_api_key_ollama', ''),
|
||||
'gemini': self._config.get('ai_api_key_gemini', ''),
|
||||
'anthropic': self._config.get('ai_api_key_anthropic', ''),
|
||||
'openai': self._config.get('ai_api_key_openai', ''),
|
||||
@@ -3026,7 +3028,7 @@ class NotificationManager:
|
||||
}
|
||||
ai_api_keys = {
|
||||
'groq': _mask_if_set(_raw_keys['groq']),
|
||||
'ollama': '', # Ollama doesn't need API key
|
||||
'ollama': _mask_if_set(_raw_keys['ollama']),
|
||||
'gemini': _mask_if_set(_raw_keys['gemini']),
|
||||
'anthropic': _mask_if_set(_raw_keys['anthropic']),
|
||||
'openai': _mask_if_set(_raw_keys['openai']),
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
"""Optional Ollama gateway authentication, with no real keys or network calls."""
|
||||
|
||||
import ast
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sqlite3
|
||||
import sys
|
||||
import tempfile
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
from unittest import mock
|
||||
import urllib.error
|
||||
|
||||
|
||||
SCRIPTS = Path(__file__).resolve().parents[1]
|
||||
if str(SCRIPTS) not in sys.path:
|
||||
sys.path.insert(0, str(SCRIPTS))
|
||||
|
||||
from ai_providers.ollama_provider import OllamaProvider, ollama_headers
|
||||
from ai_providers.base import AIProviderError
|
||||
import notification_manager as manager_module
|
||||
|
||||
TOKEN = "test-only-ollama-token"
|
||||
MODEL = "test-model:latest"
|
||||
|
||||
|
||||
def routes(manager, payload):
|
||||
"""Use the actual handlers without importing Flask or starting a server."""
|
||||
source = SCRIPTS / "flask_notification_routes.py"
|
||||
names = {"_resolve_masked_api_key", "get_provider_models",
|
||||
"test_ai_connection", "reveal_notification_secret"}
|
||||
functions = []
|
||||
for node in ast.parse(source.read_text()).body:
|
||||
if isinstance(node, ast.FunctionDef) and node.name in names:
|
||||
node.decorator_list = []
|
||||
functions.append(node)
|
||||
scope = {
|
||||
"__file__": str(source), "json": json,
|
||||
"request": SimpleNamespace(get_json=lambda **kwargs: payload),
|
||||
"jsonify": lambda value: value,
|
||||
"notification_manager": manager,
|
||||
"SENSITIVE_PLACEHOLDER": manager_module.SENSITIVE_PLACEHOLDER,
|
||||
"validate_external_url": manager_module.validate_external_url,
|
||||
"load_verified_models": lambda: {},
|
||||
}
|
||||
module = ast.fix_missing_locations(ast.Module(body=functions, type_ignores=[]))
|
||||
exec(compile(module, str(source), "exec"), scope)
|
||||
return scope
|
||||
|
||||
|
||||
class OllamaAuthenticationTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.requests = []
|
||||
self.required_token = TOKEN
|
||||
patcher = mock.patch("urllib.request.urlopen", side_effect=self.respond)
|
||||
patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
|
||||
def respond(self, request, timeout=None):
|
||||
self.requests.append(request)
|
||||
if self.required_token and request.get_header("Authorization") != "Bearer " + self.required_token:
|
||||
raise urllib.error.HTTPError(request.full_url, 401, "Unauthorized", {},
|
||||
io.BytesIO(b"sensitive response body"))
|
||||
if request.full_url.endswith("/api/tags"):
|
||||
data = {"models": [{"name": MODEL}]}
|
||||
elif request.full_url.endswith("/api/chat"):
|
||||
data = {"message": {"content": "CONNECTION_OK"}}
|
||||
else:
|
||||
raise AssertionError("Unexpected endpoint: " + request.full_url)
|
||||
return io.BytesIO(json.dumps(data).encode())
|
||||
|
||||
def test_key_is_optional_and_no_authorization_header_is_sent_when_empty(self):
|
||||
self.required_token = ""
|
||||
for key in ("", " "):
|
||||
provider = OllamaProvider(api_key=key, model=MODEL)
|
||||
self.assertTrue(provider.test_connection()["success"])
|
||||
self.assertTrue(self.requests)
|
||||
self.assertTrue(all(r.get_header("Authorization") is None for r in self.requests))
|
||||
self.assertFalse(OllamaProvider.REQUIRES_API_KEY)
|
||||
|
||||
def test_connection_authenticates_both_model_lookup_and_generation(self):
|
||||
result = OllamaProvider(api_key=TOKEN, model=MODEL).test_connection()
|
||||
self.assertTrue(result["success"], result)
|
||||
self.assertEqual([r.get_method() for r in self.requests], ["GET", "POST"])
|
||||
self.assertTrue(all(r.get_header("Authorization") == "Bearer " + TOKEN for r in self.requests))
|
||||
|
||||
def test_authentication_failure_is_not_reported_as_an_unreachable_server(self):
|
||||
result = OllamaProvider(api_key="wrong-test-token", model=MODEL).test_connection()
|
||||
self.assertFalse(result["success"])
|
||||
self.assertIn("HTTP 401", result["message"])
|
||||
self.assertNotIn("Cannot connect", result["message"])
|
||||
self.assertNotIn("wrong-test-token", json.dumps(result))
|
||||
self.assertNotIn("sensitive response", json.dumps(result))
|
||||
|
||||
def test_invalid_header_values_are_rejected_without_echoing_them(self):
|
||||
for value in ("test-secret\r\nInjected: header", "x" * 513, None):
|
||||
with self.subTest(value_type=type(value).__name__):
|
||||
with self.assertRaisesRegex(AIProviderError, "^Invalid Ollama API key$"):
|
||||
ollama_headers(value)
|
||||
self.assertEqual(self.requests, [])
|
||||
|
||||
def test_model_loading_uses_the_saved_key_when_the_browser_sends_a_mask(self):
|
||||
manager = SimpleNamespace(_config={"ai_api_key_ollama": TOKEN})
|
||||
handlers = routes(manager, {"provider": "ollama", "api_key": manager_module.SENSITIVE_PLACEHOLDER,
|
||||
"ollama_url": "http://localhost:11434"})
|
||||
result = handlers["get_provider_models"]()
|
||||
self.assertTrue(result["success"], result)
|
||||
self.assertEqual(result["models"], [MODEL])
|
||||
self.assertEqual(self.requests[0].get_header("Authorization"), "Bearer " + TOKEN)
|
||||
self.assertNotIn(TOKEN, json.dumps(result))
|
||||
|
||||
def test_model_loading_without_a_key_still_works_for_native_ollama(self):
|
||||
self.required_token = ""
|
||||
handlers = routes(SimpleNamespace(_config={}), {
|
||||
"provider": "ollama", "ollama_url": "http://localhost:11434"})
|
||||
result = handlers["get_provider_models"]()
|
||||
self.assertTrue(result["success"], result)
|
||||
self.assertIsNone(self.requests[0].get_header("Authorization"))
|
||||
|
||||
def test_test_ai_route_reuses_the_saved_key(self):
|
||||
handlers = routes(SimpleNamespace(_config={"ai_api_key_ollama": TOKEN}), {
|
||||
"provider": "ollama", "model": MODEL,
|
||||
"api_key": manager_module.SENSITIVE_PLACEHOLDER,
|
||||
"ollama_url": "http://localhost:11434"})
|
||||
result = handlers["test_ai_connection"]()
|
||||
self.assertTrue(result["success"], result)
|
||||
self.assertTrue(all(r.get_header("Authorization") == "Bearer " + TOKEN for r in self.requests))
|
||||
|
||||
def test_reveal_endpoint_recognizes_the_ollama_secret(self):
|
||||
handlers = routes(SimpleNamespace(_config={"ai_api_key_ollama": TOKEN}), {"ai_provider": "ollama"})
|
||||
self.assertEqual(handlers["reveal_notification_secret"](), ({"value": TOKEN}, 200))
|
||||
|
||||
def test_real_notification_provider_receives_the_ollama_key(self):
|
||||
manager = manager_module.NotificationManager()
|
||||
manager._config = {"ai_enabled": "true", "ai_provider": "ollama",
|
||||
"ai_api_key_ollama": TOKEN, "ai_model_ollama": MODEL,
|
||||
"ai_ollama_url": "http://localhost:11434"}
|
||||
from notification_templates import AIEnhancer
|
||||
rewriter = AIEnhancer(manager._build_ai_config())
|
||||
self.assertIsInstance(rewriter._provider, OllamaProvider)
|
||||
self.assertEqual(rewriter._provider.generate("system", "user"), "CONNECTION_OK")
|
||||
self.assertEqual(self.requests[0].get_header("Authorization"), "Bearer " + TOKEN)
|
||||
|
||||
|
||||
class OllamaKeyStorageTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
directory = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(directory.cleanup)
|
||||
self.db = Path(directory.name) / "settings.db"
|
||||
with sqlite3.connect(str(self.db)) as conn:
|
||||
conn.execute("CREATE TABLE user_settings (setting_key TEXT PRIMARY KEY, setting_value TEXT, updated_at TEXT)")
|
||||
for name, value in (("DB_PATH", self.db), ("ENCRYPTION_KEY_FILE", Path(directory.name) / "key")):
|
||||
patcher = mock.patch.object(manager_module, name, value)
|
||||
patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
self.manager = manager_module.NotificationManager()
|
||||
self.manager._rebuild_channels = mock.Mock()
|
||||
|
||||
def stored_value(self):
|
||||
with sqlite3.connect(str(self.db)) as conn:
|
||||
return conn.execute("SELECT setting_value FROM user_settings WHERE setting_key = ?",
|
||||
("notification.ai_api_key_ollama",)).fetchone()[0]
|
||||
|
||||
def test_key_is_encrypted_masked_and_loaded_for_runtime_use(self):
|
||||
result = self.manager.save_settings({"ai_provider": "ollama", "ai_api_key_ollama": TOKEN})
|
||||
self.assertTrue(result["success"], result)
|
||||
stored = self.stored_value()
|
||||
self.assertTrue(stored.startswith("ENC2:"))
|
||||
self.assertNotIn(TOKEN, stored)
|
||||
self.assertEqual(manager_module.decrypt_sensitive_value(stored), TOKEN)
|
||||
self.assertEqual(self.manager.get_settings()["config"]["ai_api_keys"]["ollama"],
|
||||
manager_module.SENSITIVE_PLACEHOLDER)
|
||||
fresh = manager_module.NotificationManager()
|
||||
fresh._load_config()
|
||||
self.assertEqual(fresh._build_ai_config()["ai_api_key"], TOKEN)
|
||||
|
||||
def test_masked_round_trip_keeps_the_key_and_an_empty_value_removes_it(self):
|
||||
self.assertTrue(self.manager.save_settings({"ai_api_key_ollama": TOKEN})["success"])
|
||||
stored = self.stored_value()
|
||||
self.assertTrue(self.manager.save_settings({"ai_api_key_ollama": manager_module.SENSITIVE_PLACEHOLDER})["success"])
|
||||
self.assertEqual(self.stored_value(), stored)
|
||||
self.assertTrue(self.manager.save_settings({"ai_api_key_ollama": ""})["success"])
|
||||
self.assertEqual(self.stored_value(), "")
|
||||
self.assertEqual(self.manager.get_settings()["config"]["ai_api_keys"]["ollama"], "")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user