feat: implement full-platform STT API (v2.3 consensus plan)
Batch+realtime transcription API: faster-whisper engine w/ EngineOwner (single GPU owner, OOM downgrade chain, persisted attempted_profiles), hardware-adaptive device manager (T0-T3 VRAM tiers), Redis/in-proc job queue w/ leases + crash recovery, postprocess (glossary/rules/LLM w/ egress guard), privacy-first result store (UUID keys, source deleted after transcribe), retention sweeper, API-key auth (HMAC digests, scopes, job ownership), WebSocket realtime lane (LocalAgreement), CLI (detect/transcribe/bench/serve/key), Docker, benchmark runner. 127 mock-based tests pass; ruff clean. Includes verification checklist and autoplan review notes.
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
"""KeyStore — 다이제스트 인증, 스코프, 키 생성 테스트."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from luke_scribe.api.deps import KeyStore, Principal
|
||||
from luke_scribe.config import Settings
|
||||
from luke_scribe.errors import AuthError, ScopeDenied
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def settings(tmp_path) -> Settings:
|
||||
return Settings(
|
||||
_env_file=None,
|
||||
api_keys="key-one,key-admin:admin,transcribe",
|
||||
api_key_file=str(tmp_path / "api_keys.json"),
|
||||
)
|
||||
|
||||
|
||||
class TestKeyStoreAuth:
|
||||
def test_plain_key_authenticates(self, settings: Settings):
|
||||
ks = KeyStore(settings)
|
||||
p = ks.authenticate("key-one")
|
||||
assert p.key_id.startswith("k-key-one")
|
||||
assert "transcribe" in p.scopes
|
||||
|
||||
def test_scoped_key(self, settings: Settings):
|
||||
ks = KeyStore(settings)
|
||||
p = ks.authenticate("key-admin")
|
||||
assert {"admin", "transcribe"} <= p.scopes
|
||||
|
||||
def test_wrong_key_rejected(self, settings: Settings):
|
||||
ks = KeyStore(settings)
|
||||
with pytest.raises(AuthError):
|
||||
ks.authenticate("key-wrong")
|
||||
|
||||
def test_empty_key_rejected(self, settings: Settings):
|
||||
ks = KeyStore(settings)
|
||||
with pytest.raises(AuthError):
|
||||
ks.authenticate(None)
|
||||
|
||||
def test_digest_only_on_disk(self, settings: Settings, tmp_path):
|
||||
"""평문 키는 저장하지 않고 다이제스트만 파일에 기록 (Eng P14)."""
|
||||
created = KeyStore(settings).create_key(
|
||||
scopes=["transcribe"], save_path=settings.api_key_file
|
||||
)
|
||||
raw = created["key"]
|
||||
data = json.loads(tmp_path.joinpath("api_keys.json").read_text())
|
||||
assert raw not in json.dumps(data)
|
||||
entry = data["keys"][0]
|
||||
assert entry["digest"] != raw
|
||||
assert len(entry["digest"]) == 64 # sha256 hex
|
||||
|
||||
def test_file_keys_loaded(self, settings: Settings, tmp_path):
|
||||
ks = KeyStore(settings)
|
||||
created = ks.create_key(scopes=["transcribe"], save_path=settings.api_key_file)
|
||||
# 새 인스턴스가 파일에서 로드
|
||||
ks2 = KeyStore(settings)
|
||||
p = ks2.authenticate(created["key"])
|
||||
assert "transcribe" in p.scopes
|
||||
|
||||
|
||||
class TestPrincipalScopes:
|
||||
def test_require_scope_ok(self):
|
||||
p = Principal(key_id="k-1", scopes={"transcribe"})
|
||||
p.require_scope("transcribe") # no raise
|
||||
|
||||
def test_require_scope_denied(self):
|
||||
p = Principal(key_id="k-1", scopes={"transcribe"})
|
||||
with pytest.raises(ScopeDenied):
|
||||
p.require_scope("admin")
|
||||
|
||||
def test_require_scope_empty(self):
|
||||
p = Principal(key_id="k-1", scopes=set())
|
||||
with pytest.raises(ScopeDenied):
|
||||
p.require_scope("transcribe")
|
||||
|
||||
|
||||
class TestDigest:
|
||||
def test_same_key_same_digest(self, settings: Settings):
|
||||
ks = KeyStore(settings)
|
||||
assert ks._digest("abc") == ks._digest("abc")
|
||||
assert ks._digest("abc") != ks._digest("abd")
|
||||
Reference in New Issue
Block a user