121 lines
4.6 KiB
Python
121 lines
4.6 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import tempfile
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
from pi_dictate_service.config import (
|
|
ConfigError,
|
|
CredentialError,
|
|
load_config,
|
|
parse_config,
|
|
provider_keywords,
|
|
read_credential,
|
|
)
|
|
from pi_dictate_service.recorder import audio_level_window_bytes, pw_record_command
|
|
|
|
|
|
class ConfigTests(unittest.TestCase):
|
|
def test_recorder_supports_both_backend_sample_rates(self) -> None:
|
|
self.assertEqual(pw_record_command(16_000)[5], "16000")
|
|
self.assertEqual(pw_record_command(24_000)[5], "24000")
|
|
self.assertEqual(audio_level_window_bytes(24_000), 4_800)
|
|
|
|
def test_rejects_unknown_selected_fields_and_invalid_backend_values(self) -> None:
|
|
for invalid_backend in ([], {}, 1, True, None):
|
|
with self.subTest(backend=invalid_backend):
|
|
with self.assertRaisesRegex(ConfigError, "backend must be"):
|
|
parse_config({"version": 1, "backend": invalid_backend})
|
|
|
|
base = {
|
|
"version": 1,
|
|
"backend": "openai",
|
|
"openai": {
|
|
"model": "gpt-live-transcribe",
|
|
"apiKeyFile": "~/.config/pi-dictate/credentials/openai-api-key",
|
|
"termsPath": "/tmp/terms",
|
|
},
|
|
}
|
|
with patch.dict(os.environ, {"HOME": "/tmp/pi-dictate-test"}):
|
|
invalid = {**base, "openai": {**base["openai"], "endpoint": "https://bad"}}
|
|
with self.assertRaises(ConfigError):
|
|
parse_config(invalid)
|
|
with self.assertRaises(ConfigError):
|
|
parse_config({**base, "version": 2})
|
|
configured = parse_config(
|
|
{**base, "openai": {**base["openai"], "delay": "low"}}
|
|
)
|
|
self.assertEqual(configured.openai.delay, "low")
|
|
with self.assertRaises(ConfigError):
|
|
parse_config({**base, "openai": {**base["openai"], "delay": "fast"}})
|
|
with self.assertRaises(ConfigError):
|
|
parse_config({**base, "openai": {**base["openai"], "termsPath": "relative/terms"}})
|
|
|
|
def test_reads_only_one_trailing_newline_from_a_private_credential(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
path = Path(directory) / "key"
|
|
path.write_text("secret\n", encoding="utf-8")
|
|
path.chmod(0o600)
|
|
self.assertEqual(read_credential(path), "secret")
|
|
|
|
path.chmod(0o640)
|
|
with self.assertRaises(CredentialError):
|
|
read_credential(path)
|
|
|
|
path.chmod(0o600)
|
|
path.write_text("secret\nextra", encoding="utf-8")
|
|
with self.assertRaises(CredentialError):
|
|
read_credential(path)
|
|
|
|
def test_rejects_a_credential_reached_through_a_symlinked_directory(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
actual = root / "actual"
|
|
actual.mkdir()
|
|
key = actual / "key"
|
|
key.write_text("secret", encoding="utf-8")
|
|
key.chmod(0o600)
|
|
linked = root / "credentials"
|
|
linked.symlink_to(actual, target_is_directory=True)
|
|
|
|
with self.assertRaisesRegex(CredentialError, "ancestry"):
|
|
read_credential(linked / "key")
|
|
|
|
def test_config_file_is_regular_private_and_provider_terms_reject_cr(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
config_path = root / "config.json"
|
|
config_path.write_text(
|
|
json.dumps(
|
|
{
|
|
"version": 1,
|
|
"backend": "local",
|
|
"local": {
|
|
"adapter": "nemo-parakeet",
|
|
"modelPath": "/tmp/model.nemo",
|
|
"modelSha256": "0" * 64,
|
|
"termsPath": "/tmp/terms",
|
|
"boostAlpha": 1.0,
|
|
},
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
config_path.chmod(0o600)
|
|
self.assertEqual(load_config(config_path).backend, "local")
|
|
|
|
terms = root / "terms"
|
|
terms.write_bytes(b"bad\rterm")
|
|
with self.assertRaises(ConfigError):
|
|
provider_keywords(terms)
|
|
terms.write_bytes(b"safe\nbad\x00term\n")
|
|
with self.assertRaises(ConfigError):
|
|
provider_keywords(terms)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|