Files
web-page-backend/stock/app/test_webai_endpoints.py
gahusb d1b2b6a4ba feat(stock-webai): /api/webai/news-sentiment daily dump
JOINs news_sentiment with krx_master for name fallback. Sorted by
score DESC. Date param defaults to latest. Empty table returns
{date: null, count: 0, items: []}. 4 integration tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 08:40:49 +09:00

174 lines
5.7 KiB
Python

import os
import sqlite3
import pytest
from fastapi.testclient import TestClient
from app.screener.schema import ensure_screener_schema
from app.db import init_db
@pytest.fixture(autouse=True)
def isolated_db_and_auth(tmp_path, monkeypatch):
db_path = tmp_path / "stock.db"
# 기본 stock DB 스키마
monkeypatch.setenv("STOCK_DB_PATH", str(db_path))
init_db()
# screener 스키마 (news_sentiment, krx_master 등)
c = sqlite3.connect(db_path)
ensure_screener_schema(c)
c.close()
# WEBAI_API_KEY 활성화
monkeypatch.setenv("WEBAI_API_KEY", "test-secret")
@pytest.fixture
def client():
from app.main import app
return TestClient(app)
HEADERS_OK = {"X-WebAI-Key": "test-secret"}
def _seed_portfolio(broker="키움", ticker="005930", name="삼성전자",
quantity=100, avg_price=75000.0, purchase_price=75500.0):
from app.db import add_portfolio_item
return add_portfolio_item(broker, ticker, name, quantity, avg_price,
purchase_price=purchase_price)
def test_webai_portfolio_normal_response_includes_pnl_pct(client, monkeypatch):
_seed_portfolio()
# current_price 모킹 — profit_rate 4.67% 만들기
from app import main
monkeypatch.setattr(
main, "get_current_prices_detail",
lambda tickers: {"005930": {"price": 78500.0, "session": "REGULAR", "as_of": "2026-05-15T15:30:00"}}
)
r = client.get("/api/webai/portfolio", headers=HEADERS_OK)
assert r.status_code == 200
body = r.json()
assert len(body["holdings"]) == 1
h = body["holdings"][0]
assert h["pnl_pct"] is not None
assert abs(h["pnl_pct"] - 0.0467) < 0.0005 # 0.0467 ± rounding
def test_webai_portfolio_summary_has_total_pnl_pct(client, monkeypatch):
_seed_portfolio()
from app import main
monkeypatch.setattr(
main, "get_current_prices_detail",
lambda tickers: {"005930": {"price": 78500.0, "session": "REGULAR", "as_of": "x"}}
)
r = client.get("/api/webai/portfolio", headers=HEADERS_OK)
body = r.json()
assert "total_pnl_pct" in body["summary"]
assert abs(body["summary"]["total_pnl_pct"] - 0.0467) < 0.0005
def test_webai_portfolio_pnl_pct_matches_profit_rate_divided_100(client, monkeypatch):
_seed_portfolio()
from app import main
monkeypatch.setattr(
main, "get_current_prices_detail",
lambda tickers: {"005930": {"price": 78500.0, "session": "REGULAR", "as_of": "x"}}
)
r = client.get("/api/webai/portfolio", headers=HEADERS_OK)
h = r.json()["holdings"][0]
assert h["pnl_pct"] == round(h["profit_rate"] / 100, 6)
def test_webai_portfolio_missing_key_returns_401(client):
r = client.get("/api/webai/portfolio")
assert r.status_code == 401
assert "X-WebAI-Key" in r.json()["detail"]
def _seed_news_sentiment(date_str: str, rows: list[tuple]):
"""rows: list of (ticker, score_raw, reason, news_count)."""
db_path = os.environ["STOCK_DB_PATH"]
c = sqlite3.connect(db_path)
for ticker, score, reason, news_count in rows:
c.execute(
"INSERT OR REPLACE INTO news_sentiment "
"(ticker, date, score_raw, reason, news_count, source) "
"VALUES (?, ?, ?, ?, ?, 'articles')",
(ticker, date_str, score, reason, news_count)
)
c.commit()
c.close()
def _seed_krx_master(rows: list[tuple]):
"""rows: list of (ticker, name)."""
db_path = os.environ["STOCK_DB_PATH"]
c = sqlite3.connect(db_path)
import datetime as dt
now = dt.datetime.utcnow().isoformat()
for ticker, name in rows:
c.execute(
"INSERT OR REPLACE INTO krx_master "
"(ticker, name, market, market_cap, updated_at) VALUES (?, ?, 'KOSPI', 0, ?)",
(ticker, name, now)
)
c.commit()
c.close()
def test_webai_news_sentiment_returns_latest_date_when_no_param(client):
_seed_krx_master([("005930", "삼성전자"), ("000660", "SK하이닉스")])
_seed_news_sentiment("2026-05-14", [("005930", 5.0, "old", 5)])
_seed_news_sentiment("2026-05-15", [
("005930", 6.2, "HBM 양산 가시화", 12),
("000660", 5.5, "PPI 우려에도 강세", 8),
])
r = client.get("/api/webai/news-sentiment", headers=HEADERS_OK)
assert r.status_code == 200
body = r.json()
assert body["date"] == "2026-05-15"
assert body["count"] == 2
# sorted by score DESC
assert body["items"][0]["ticker"] == "005930"
assert body["items"][0]["score"] == 6.2
assert body["items"][0]["name"] == "삼성전자"
assert body["items"][0]["reason"] == "HBM 양산 가시화"
def test_webai_news_sentiment_filters_by_date_param(client):
_seed_krx_master([("005930", "삼성전자")])
_seed_news_sentiment("2026-05-14", [("005930", 5.0, "yesterday", 5)])
_seed_news_sentiment("2026-05-15", [("005930", 6.2, "today", 12)])
r = client.get("/api/webai/news-sentiment?date=2026-05-14", headers=HEADERS_OK)
body = r.json()
assert body["date"] == "2026-05-14"
assert body["count"] == 1
assert body["items"][0]["reason"] == "yesterday"
def test_webai_news_sentiment_empty_table_returns_count_zero(client):
r = client.get("/api/webai/news-sentiment", headers=HEADERS_OK)
body = r.json()
assert body["date"] is None
assert body["count"] == 0
assert body["items"] == []
def test_webai_news_sentiment_items_sorted_by_score_desc(client):
_seed_krx_master([("A", "A주"), ("B", "B주"), ("C", "C주")])
_seed_news_sentiment("2026-05-15", [
("A", 1.0, "low", 1),
("B", 9.0, "high", 1),
("C", 5.0, "mid", 1),
])
r = client.get("/api/webai/news-sentiment", headers=HEADERS_OK)
items = r.json()["items"]
assert [i["score"] for i in items] == [9.0, 5.0, 1.0]