91e75e620b
CI: cua-driver distro-compat matrix / debian:12 (glibc 2.36) (push) Has been cancelled
CI: SPDX Headers / Check SPDX headers (warn-only) (push) Has been cancelled
CD: Docs MCP Server / build (linux/amd64) (push) Has been cancelled
CD: Docs MCP Server / build (linux/arm64) (push) Has been cancelled
CD: Docs MCP Server / merge (push) Has been cancelled
CI: cua-driver distro-compat matrix / Resolve release version (push) Has been cancelled
CI: cua-driver distro-compat matrix / fedora:41 (glibc 2.40) (push) Has been cancelled
CI: cua-driver distro-compat matrix / rockylinux:9 (glibc 2.34) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:22.04 (glibc 2.35) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:24.04 (glibc 2.39) (push) Has been cancelled
CI: cua-driver distro-compat matrix / Distro compat summary (push) Has been cancelled
CI: Rust Linux unit / Rust Linux unit and compile (push) Has been cancelled
CI: Rust Windows unit / Rust Windows unit and compile (push) Has been cancelled
CI: Nix Linux Rust source / Nix / compositor build (push) Has been cancelled
CI: Nix Linux Rust source / Nix / driver package (push) Has been cancelled
CI: Nix Linux Rust source / Nix / Rust unit tests (push) Has been cancelled
231 lines
7.2 KiB
Python
231 lines
7.2 KiB
Python
"""Tests for auth.store module."""
|
|
|
|
import sqlite3
|
|
import threading
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
from cua_cli.auth.store import (
|
|
CredentialStore,
|
|
clear_credentials,
|
|
get_api_key,
|
|
require_api_key,
|
|
save_api_key,
|
|
)
|
|
|
|
|
|
class TestCredentialStore:
|
|
"""Tests for CredentialStore class."""
|
|
|
|
def test_init_creates_database(self, tmp_path):
|
|
"""Test that initializing creates the database file."""
|
|
db_path = tmp_path / "test.db"
|
|
CredentialStore(db_path)
|
|
|
|
assert db_path.exists()
|
|
|
|
def test_init_creates_parent_directory(self, tmp_path):
|
|
"""Test that initializing creates parent directories."""
|
|
db_path = tmp_path / "nested" / "dir" / "test.db"
|
|
CredentialStore(db_path)
|
|
|
|
assert db_path.parent.exists()
|
|
assert db_path.exists()
|
|
|
|
def test_set_and_get(self, tmp_path):
|
|
"""Test setting and getting a value."""
|
|
store = CredentialStore(tmp_path / "test.db")
|
|
|
|
store.set("test_key", "test_value")
|
|
result = store.get("test_key")
|
|
|
|
assert result == "test_value"
|
|
|
|
def test_get_nonexistent_key(self, tmp_path):
|
|
"""Test getting a nonexistent key returns None."""
|
|
store = CredentialStore(tmp_path / "test.db")
|
|
|
|
result = store.get("nonexistent")
|
|
|
|
assert result is None
|
|
|
|
def test_set_overwrites_existing(self, tmp_path):
|
|
"""Test that setting an existing key overwrites the value."""
|
|
store = CredentialStore(tmp_path / "test.db")
|
|
|
|
store.set("key", "value1")
|
|
store.set("key", "value2")
|
|
result = store.get("key")
|
|
|
|
assert result == "value2"
|
|
|
|
def test_delete_existing_key(self, tmp_path):
|
|
"""Test deleting an existing key returns True."""
|
|
store = CredentialStore(tmp_path / "test.db")
|
|
|
|
store.set("key", "value")
|
|
result = store.delete("key")
|
|
|
|
assert result is True
|
|
assert store.get("key") is None
|
|
|
|
def test_delete_nonexistent_key(self, tmp_path):
|
|
"""Test deleting a nonexistent key returns False."""
|
|
store = CredentialStore(tmp_path / "test.db")
|
|
|
|
result = store.delete("nonexistent")
|
|
|
|
assert result is False
|
|
|
|
def test_clear_removes_all(self, tmp_path):
|
|
"""Test that clear removes all values."""
|
|
store = CredentialStore(tmp_path / "test.db")
|
|
|
|
store.set("key1", "value1")
|
|
store.set("key2", "value2")
|
|
store.clear()
|
|
|
|
assert store.get("key1") is None
|
|
assert store.get("key2") is None
|
|
|
|
def test_wal_mode_enabled(self, tmp_path):
|
|
"""Test that WAL mode is enabled for concurrent access."""
|
|
db_path = tmp_path / "test.db"
|
|
CredentialStore(db_path)
|
|
|
|
conn = sqlite3.connect(db_path)
|
|
try:
|
|
cursor = conn.execute("PRAGMA journal_mode")
|
|
mode = cursor.fetchone()[0]
|
|
assert mode.lower() == "wal"
|
|
finally:
|
|
conn.close()
|
|
|
|
def test_concurrent_access(self, tmp_path):
|
|
"""Test that concurrent access works with WAL mode."""
|
|
db_path = tmp_path / "test.db"
|
|
store = CredentialStore(db_path)
|
|
results = []
|
|
errors = []
|
|
|
|
def writer():
|
|
try:
|
|
for i in range(10):
|
|
store.set(f"key_{i}", f"value_{i}")
|
|
except Exception as e:
|
|
errors.append(e)
|
|
|
|
def reader():
|
|
try:
|
|
for i in range(10):
|
|
result = store.get(f"key_{i}")
|
|
if result is not None:
|
|
results.append(result)
|
|
except Exception as e:
|
|
errors.append(e)
|
|
|
|
threads = [
|
|
threading.Thread(target=writer),
|
|
threading.Thread(target=reader),
|
|
threading.Thread(target=reader),
|
|
]
|
|
|
|
for t in threads:
|
|
t.start()
|
|
for t in threads:
|
|
t.join()
|
|
|
|
assert len(errors) == 0
|
|
|
|
|
|
class TestGetApiKey:
|
|
"""Tests for get_api_key function."""
|
|
|
|
def test_returns_env_var_first(self, monkeypatch, tmp_path):
|
|
"""Test that environment variable takes precedence."""
|
|
# Set up a store with a different value
|
|
with patch("cua_cli.auth.store.CREDENTIALS_DB", tmp_path / "test.db"):
|
|
with patch("cua_cli.auth.store._store", None):
|
|
monkeypatch.setenv("CUA_API_KEY", "env-key")
|
|
save_api_key("stored-key")
|
|
|
|
result = get_api_key()
|
|
|
|
assert result == "env-key"
|
|
|
|
def test_falls_back_to_stored(self, monkeypatch, tmp_path):
|
|
"""Test that stored key is used when env var not set."""
|
|
with patch("cua_cli.auth.store.CREDENTIALS_DB", tmp_path / "test.db"):
|
|
with patch("cua_cli.auth.store._store", None):
|
|
monkeypatch.delenv("CUA_API_KEY", raising=False)
|
|
save_api_key("stored-key")
|
|
|
|
result = get_api_key()
|
|
|
|
assert result == "stored-key"
|
|
|
|
def test_returns_none_when_not_found(self, monkeypatch, tmp_path):
|
|
"""Test that None is returned when no key is found."""
|
|
with patch("cua_cli.auth.store.CREDENTIALS_DB", tmp_path / "test.db"):
|
|
with patch("cua_cli.auth.store._store", None):
|
|
monkeypatch.delenv("CUA_API_KEY", raising=False)
|
|
|
|
result = get_api_key()
|
|
|
|
assert result is None
|
|
|
|
|
|
class TestSaveApiKey:
|
|
"""Tests for save_api_key function."""
|
|
|
|
def test_saves_api_key(self, tmp_path, monkeypatch):
|
|
"""Test that API key is saved correctly."""
|
|
with patch("cua_cli.auth.store.CREDENTIALS_DB", tmp_path / "test.db"):
|
|
with patch("cua_cli.auth.store._store", None):
|
|
monkeypatch.delenv("CUA_API_KEY", raising=False)
|
|
|
|
save_api_key("my-api-key")
|
|
result = get_api_key()
|
|
|
|
assert result == "my-api-key"
|
|
|
|
|
|
class TestClearCredentials:
|
|
"""Tests for clear_credentials function."""
|
|
|
|
def test_clears_all_credentials(self, tmp_path, monkeypatch):
|
|
"""Test that all credentials are cleared."""
|
|
with patch("cua_cli.auth.store.CREDENTIALS_DB", tmp_path / "test.db"):
|
|
with patch("cua_cli.auth.store._store", None):
|
|
monkeypatch.delenv("CUA_API_KEY", raising=False)
|
|
|
|
save_api_key("my-api-key")
|
|
clear_credentials()
|
|
result = get_api_key()
|
|
|
|
assert result is None
|
|
|
|
|
|
class TestRequireApiKey:
|
|
"""Tests for require_api_key function."""
|
|
|
|
def test_returns_key_when_available(self, monkeypatch):
|
|
"""Test that key is returned when available."""
|
|
monkeypatch.setenv("CUA_API_KEY", "my-api-key")
|
|
|
|
result = require_api_key()
|
|
|
|
assert result == "my-api-key"
|
|
|
|
def test_raises_when_not_available(self, monkeypatch, tmp_path):
|
|
"""Test that RuntimeError is raised when no key available."""
|
|
with patch("cua_cli.auth.store.CREDENTIALS_DB", tmp_path / "test.db"):
|
|
with patch("cua_cli.auth.store._store", None):
|
|
monkeypatch.delenv("CUA_API_KEY", raising=False)
|
|
|
|
with pytest.raises(RuntimeError) as exc_info:
|
|
require_api_key()
|
|
|
|
assert "No API key configured" in str(exc_info.value)
|
|
assert "cua auth login" in str(exc_info.value)
|