You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
417 lines
14 KiB
Python
417 lines
14 KiB
Python
"""Unit tests for Oracle client using pytest-httpx."""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from pytest_httpx import HTTPXMock
|
|
|
|
FIXTURES_DIR = Path(__file__).parent.parent / "fixtures"
|
|
|
|
|
|
def load_fixture(name: str) -> dict:
|
|
return json.loads((FIXTURES_DIR / name).read_text())
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_filings(httpx_mock: HTTPXMock):
|
|
from libs.oracle_client.client import OracleClient
|
|
from libs.oracle_client.filings import FilingsService
|
|
|
|
data = load_fixture("filing_search.json")
|
|
httpx_mock.add_response(json=data, url="http://oracle:18001/api/v1/filings/search/AAPL")
|
|
|
|
async with OracleClient("http://oracle:18001") as client:
|
|
svc = FilingsService(client)
|
|
result = await svc.search_filings("AAPL")
|
|
|
|
assert result.ticker == "AAPL"
|
|
assert len(result.filings) == 2
|
|
assert result.filings[0].form_type == "8-K"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_exhibit(httpx_mock: HTTPXMock):
|
|
from libs.oracle_client.client import OracleClient
|
|
from libs.oracle_client.filings import FilingsService
|
|
|
|
data = load_fixture("exhibit_content.json")
|
|
httpx_mock.add_response(json=data)
|
|
|
|
async with OracleClient("http://oracle:18001") as client:
|
|
svc = FilingsService(client)
|
|
result = await svc.get_exhibit("0000320193-26-000001")
|
|
|
|
assert result.accession_no == "0000320193-26-000001"
|
|
assert len(result.content) > 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_daily_bars(httpx_mock: HTTPXMock):
|
|
from libs.oracle_client.client import OracleClient
|
|
from libs.oracle_client.price import PriceService
|
|
|
|
data = load_fixture("price_data.json")
|
|
httpx_mock.add_response(json=data)
|
|
|
|
async with OracleClient("http://oracle:18001") as client:
|
|
svc = PriceService(client)
|
|
result = await svc.get_daily_bars("AAPL")
|
|
|
|
assert result.ticker == "AAPL"
|
|
assert len(result.bars) > 0
|
|
assert result.bars[0].close > 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_not_found_raises_oracle_not_found(httpx_mock: HTTPXMock):
|
|
from libs.oracle_client.client import OracleClient
|
|
from libs.oracle_client.exceptions import OracleNotFoundError
|
|
from libs.oracle_client.filings import FilingsService
|
|
|
|
httpx_mock.add_response(status_code=404)
|
|
|
|
async with OracleClient("http://oracle:18001") as client:
|
|
svc = FilingsService(client)
|
|
with pytest.raises(OracleNotFoundError):
|
|
await svc.get_exhibit("NONEXISTENT")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_server_error_raises_oracle_server_error(httpx_mock: HTTPXMock):
|
|
from libs.oracle_client.client import OracleClient
|
|
from libs.oracle_client.exceptions import OracleServerError
|
|
from libs.oracle_client.filings import FilingsService
|
|
|
|
# Must provide one response per retry attempt (3 total)
|
|
httpx_mock.add_response(status_code=500)
|
|
httpx_mock.add_response(status_code=500)
|
|
httpx_mock.add_response(status_code=500)
|
|
|
|
async with OracleClient("http://oracle:18001") as client:
|
|
svc = FilingsService(client)
|
|
with pytest.raises(OracleServerError):
|
|
await svc.get_exhibit("ACC123")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_retries_on_transient_error_then_succeeds(httpx_mock: HTTPXMock):
|
|
from libs.oracle_client.client import OracleClient
|
|
from libs.oracle_client.filings import FilingsService
|
|
|
|
data = load_fixture("exhibit_content.json")
|
|
httpx_mock.add_response(status_code=500) # attempt 1 fails
|
|
httpx_mock.add_response(status_code=500) # attempt 2 fails
|
|
httpx_mock.add_response(json=data) # attempt 3 succeeds
|
|
|
|
async with OracleClient("http://oracle:18001") as client:
|
|
svc = FilingsService(client)
|
|
result = await svc.get_exhibit("0000320193-26-000001")
|
|
|
|
assert result.accession_no == "0000320193-26-000001"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_short_volume(httpx_mock: HTTPXMock):
|
|
from libs.oracle_client.client import OracleClient
|
|
from libs.oracle_client.finra import FinraService
|
|
|
|
data = load_fixture("short_volume.json")
|
|
httpx_mock.add_response(json=data)
|
|
|
|
async with OracleClient("http://oracle:18001") as client:
|
|
svc = FinraService(client)
|
|
result = await svc.get_short_volume("AAPL")
|
|
|
|
assert result.symbol == "AAPL"
|
|
assert len(result.data) == 3
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_dividend_history(httpx_mock: HTTPXMock):
|
|
from libs.oracle_client.client import OracleClient
|
|
from libs.oracle_client.dividends import DividendService
|
|
|
|
httpx_mock.add_response(
|
|
json={
|
|
"symbol": "AAPL",
|
|
"dividends": [
|
|
{
|
|
"ticker": "AAPL",
|
|
"ex_dividend_date": "2025-02-10",
|
|
"amount": 0.25,
|
|
"declaration_date": None,
|
|
"record_date": None,
|
|
"payment_date": None,
|
|
"currency": "USD",
|
|
"dividend_type": "regular",
|
|
"frequency": "quarterly",
|
|
"as_of_date": "2025-01-11",
|
|
"source": "yfinance",
|
|
}
|
|
],
|
|
"total_count": 1,
|
|
"annual_yield_estimate": 1.0,
|
|
"metadata": {"note": "ok"},
|
|
}
|
|
)
|
|
|
|
async with OracleClient("http://oracle:18001") as client:
|
|
svc = DividendService(client)
|
|
result = await svc.get_history("AAPL")
|
|
|
|
assert result.symbol == "AAPL"
|
|
assert result.total_count == 1
|
|
assert result.dividends[0].ticker == "AAPL"
|
|
assert result.dividends[0].ex_dividend_date == "2025-02-10"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_upcoming_dividends(httpx_mock: HTTPXMock):
|
|
from libs.oracle_client.client import OracleClient
|
|
from libs.oracle_client.dividends import DividendService
|
|
|
|
httpx_mock.add_response(
|
|
json={
|
|
"dividends": [
|
|
{
|
|
"ticker": "AAPL",
|
|
"ex_dividend_date": "2025-02-10",
|
|
"amount": 0.25,
|
|
"declaration_date": None,
|
|
"record_date": None,
|
|
"payment_date": None,
|
|
"currency": "USD",
|
|
"dividend_type": "regular",
|
|
"frequency": "quarterly",
|
|
"as_of_date": "2025-01-11",
|
|
"source": "yfinance",
|
|
}
|
|
],
|
|
"total_count": 1,
|
|
"metadata": {"as_of_date": "2025-02-07"},
|
|
}
|
|
)
|
|
|
|
async with OracleClient("http://oracle:18001") as client:
|
|
svc = DividendService(client)
|
|
result = await svc.get_upcoming(
|
|
as_of_date="2025-02-07",
|
|
from_ex_date="2025-02-10",
|
|
to_ex_date="2025-02-10",
|
|
symbols=["AAPL", "MSFT"],
|
|
)
|
|
|
|
assert result.total_count == 1
|
|
assert result.dividends[0].ticker == "AAPL"
|
|
assert result.metadata["as_of_date"] == "2025-02-07"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_financial_data(httpx_mock: HTTPXMock):
|
|
from libs.oracle_client.client import OracleClient
|
|
from libs.oracle_client.financial import FinancialService
|
|
|
|
data = load_fixture("financial_data.json")
|
|
httpx_mock.add_response(json=data)
|
|
|
|
async with OracleClient("http://oracle:18001") as client:
|
|
svc = FinancialService(client)
|
|
result = await svc.get_financial_data("AAPL")
|
|
|
|
assert result.ticker == "AAPL"
|
|
assert len(result.periods) == 2
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_company_info(httpx_mock: HTTPXMock):
|
|
from libs.oracle_client.client import OracleClient
|
|
from libs.oracle_client.financial import FinancialService
|
|
|
|
data = load_fixture("financial_data.json")
|
|
httpx_mock.add_response(json=data)
|
|
|
|
async with OracleClient("http://oracle:18001") as client:
|
|
svc = FinancialService(client)
|
|
info = await svc.get_company_info("AAPL")
|
|
|
|
assert info.ticker == "AAPL"
|
|
assert info.name == "Apple Inc."
|
|
assert info.cik == "0000320193"
|
|
assert info.sector == "Technology"
|
|
assert info.exchange == "NASDAQ"
|
|
assert info.country == "US"
|
|
assert info.market_cap == 3500000000000.0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_connection_error_raises_oracle_connection_error(httpx_mock: HTTPXMock):
|
|
"""ConnectError 시 OracleConnectionError가 발생한다 (3회 재시도 후)."""
|
|
import httpx
|
|
|
|
from libs.oracle_client.client import OracleClient
|
|
from libs.oracle_client.exceptions import OracleConnectionError
|
|
|
|
# 3번 모두 ConnectError (max_attempts=3)
|
|
httpx_mock.add_exception(httpx.ConnectError("connection refused"))
|
|
httpx_mock.add_exception(httpx.ConnectError("connection refused"))
|
|
httpx_mock.add_exception(httpx.ConnectError("connection refused"))
|
|
|
|
async with OracleClient("http://oracle:18001") as client:
|
|
with pytest.raises(OracleConnectionError):
|
|
await client.get("/health")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_timeout_raises_oracle_timeout_error(httpx_mock: HTTPXMock):
|
|
"""ReadTimeout 시 OracleTimeoutError가 발생한다 (3회 재시도 후)."""
|
|
import httpx
|
|
|
|
from libs.oracle_client.client import OracleClient
|
|
from libs.oracle_client.exceptions import OracleTimeoutError
|
|
|
|
httpx_mock.add_exception(httpx.ReadTimeout("read timeout"))
|
|
httpx_mock.add_exception(httpx.ReadTimeout("read timeout"))
|
|
httpx_mock.add_exception(httpx.ReadTimeout("read timeout"))
|
|
|
|
async with OracleClient("http://oracle:18001") as client:
|
|
with pytest.raises(OracleTimeoutError):
|
|
await client.get("/health")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_client_without_context_manager_raises():
|
|
"""context manager 없이 get() 호출 시 RuntimeError가 발생한다."""
|
|
from libs.oracle_client.client import OracleClient
|
|
|
|
client = OracleClient("http://oracle:18001")
|
|
with pytest.raises(RuntimeError, match="async context manager"):
|
|
await client.get("/health")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_attention_entity(httpx_mock: HTTPXMock):
|
|
from libs.oracle_client.attention import AttentionService
|
|
from libs.oracle_client.client import OracleClient
|
|
|
|
data = load_fixture("attention_entity.json")
|
|
httpx_mock.add_response(
|
|
json=data,
|
|
url="http://oracle:18001/api/v1/attention/entity/AAPL",
|
|
)
|
|
|
|
async with OracleClient("http://oracle:18001") as client:
|
|
svc = AttentionService(client)
|
|
result = await svc.get_entity("AAPL")
|
|
|
|
assert result.ticker == "AAPL"
|
|
assert result.entity.canonical_name == "Apple"
|
|
assert result.entity.wiki_title == "Apple Inc."
|
|
assert result.status == "exists"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_event_attention(httpx_mock: HTTPXMock):
|
|
import datetime as dt
|
|
|
|
from libs.oracle_client.attention import AttentionService
|
|
from libs.oracle_client.client import OracleClient
|
|
|
|
data = load_fixture("attention_event.json")
|
|
httpx_mock.add_response(
|
|
json=data,
|
|
url="http://oracle:18001/api/v1/attention/event/AAPL?event_date=2024-02-01",
|
|
)
|
|
|
|
async with OracleClient("http://oracle:18001") as client:
|
|
svc = AttentionService(client)
|
|
result = await svc.get_event_attention("AAPL", dt.date(2024, 2, 1))
|
|
|
|
assert result.ticker == "AAPL"
|
|
assert result.event_date == "2024-02-01"
|
|
assert result.wiki.views == 28837
|
|
assert result.news.gdelt_status == "not_collected"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_resolve_attention_entity(httpx_mock: HTTPXMock):
|
|
from libs.oracle_client.attention import AttentionService
|
|
from libs.oracle_client.client import OracleClient
|
|
|
|
data = load_fixture("attention_entity.json")
|
|
data["status"] = "resolved"
|
|
data["message"] = "Entity resolved: wiki_title='Apple Inc.' confidence=0.95"
|
|
httpx_mock.add_response(
|
|
json=data,
|
|
method="POST",
|
|
url="http://oracle:18001/api/v1/attention/admin/resolve/AAPL",
|
|
)
|
|
|
|
async with OracleClient("http://oracle:18001") as client:
|
|
svc = AttentionService(client)
|
|
result = await svc.resolve_entity("AAPL")
|
|
|
|
assert result.status == "resolved"
|
|
assert result.entity.resolver_confidence == 0.95
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_collect_attention_wiki(httpx_mock: HTTPXMock):
|
|
from libs.oracle_client.attention import AttentionService
|
|
from libs.oracle_client.client import OracleClient
|
|
|
|
data = load_fixture("attention_collect.json")
|
|
httpx_mock.add_response(
|
|
json=data,
|
|
method="POST",
|
|
url="http://oracle:18001/api/v1/attention/admin/collect/wiki/AAPL?event_date=2024-02-01",
|
|
)
|
|
|
|
async with OracleClient("http://oracle:18001") as client:
|
|
svc = AttentionService(client)
|
|
result = await svc.collect_wiki("AAPL", "2024-02-01")
|
|
|
|
assert result.ticker == "AAPL"
|
|
assert result.source == "wiki"
|
|
assert result.records_collected == 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_collect_attention_gdelt(httpx_mock: HTTPXMock):
|
|
from libs.oracle_client.attention import AttentionService
|
|
from libs.oracle_client.client import OracleClient
|
|
|
|
data = load_fixture("attention_collect.json")
|
|
data["source"] = "gdelt"
|
|
data["records_collected"] = 12
|
|
httpx_mock.add_response(
|
|
json=data,
|
|
method="POST",
|
|
url="http://oracle:18001/api/v1/attention/admin/collect/gdelt/AAPL?event_date=2024-02-01",
|
|
)
|
|
|
|
async with OracleClient("http://oracle:18001") as client:
|
|
svc = AttentionService(client)
|
|
result = await svc.collect_gdelt("AAPL", "2024-02-01")
|
|
|
|
assert result.source == "gdelt"
|
|
assert result.records_collected == 12
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_health_checks_use_api_v1_health(httpx_mock: HTTPXMock):
|
|
from libs.oracle_client.client import OracleClient
|
|
|
|
httpx_mock.add_response(
|
|
json={"status": "ok"},
|
|
url="http://oracle:18001/api/v1/health",
|
|
)
|
|
httpx_mock.add_response(
|
|
json={"status": "ok"},
|
|
url="http://oracle:18001/api/v1/health",
|
|
)
|
|
|
|
async with OracleClient("http://oracle:18001") as client:
|
|
assert await client.health_check() is True
|
|
assert await client.health_check_fast() is True
|