Download tests/test_return_api.py from adoomy/StocKing: direct link, hf CLI and curl.
- Browser
- Download file 11.2 kB
-
https://huggingface.co/spaces/adoomy/StocKing/resolve/main/tests/test_return_api.py
- Command line
-
hf download hf://spaces/adoomy/StocKing/tests/test_return_api.py
-
curl -L -o test_return_api.py https://huggingface.co/spaces/adoomy/StocKing/resolve/main/tests/test_return_api.py
11.2 kB
| from datetime import date, datetime | |
| from unittest.mock import Mock | |
| import pytest | |
| from fastapi.testclient import TestClient | |
| from sqlalchemy import create_engine, inspect, text | |
| from sqlalchemy.orm import sessionmaker | |
| from sqlalchemy.pool import StaticPool | |
| import auth | |
| import main | |
| from database import init_db | |
| from models import Base, Company, PriceSnapshot, AnnualData, User, ReturnAudit, ReturnDocument, ReturnEvent, ReturnPolicy | |
| def api(): | |
| engine = create_engine("sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool) | |
| Base.metadata.create_all(engine) | |
| factory = sessionmaker(bind=engine) | |
| db = factory() | |
| year = date.today().year | |
| db.add_all([Company(ticker="005930", name="테스트", market="KOSPI"), | |
| User(id=1, google_id="admin", email="admin@example.com", is_admin=True), | |
| PriceSnapshot(ticker="005930", current_price=10000, market_cap=10000, updated_at=datetime.utcnow()), | |
| AnnualData(ticker="005930", year=year, is_estimate=True, dividend_per_share=400, net_income=1000, shares_outstanding=100000000), | |
| AnnualData(ticker="005930", year=year+1, is_estimate=True, dividend_per_share=500), | |
| AnnualData(ticker="005930", year=year-1, is_estimate=False, shares_outstanding=100000000)]) | |
| db.commit() | |
| def get_db(): | |
| yield db | |
| main.app.dependency_overrides[main.get_db] = get_db | |
| auth_user = {"value": db.get(User, 1)} | |
| main.app.dependency_overrides[auth.get_current_user] = lambda: auth_user["value"] | |
| main.invalidate_screener_cache() | |
| client = TestClient(main.app) | |
| yield client, db, auth_user, year | |
| main.app.dependency_overrides.clear() | |
| main.invalidate_screener_cache() | |
| db.close() | |
| engine.dispose() | |
| def payload(year, **kwargs): | |
| return {"ticker": "005930", "source_key": "manual:first", "source_url": "https://example.com/ir", "title": "소각 결정", "evidence": "보통주 소각 300억원", "status": "pending", | |
| "events": [{"year": year, "amount": 300, "scope": "common", "evidence": "보통주 소각 300억원"}], **kwargs} | |
| def test_approval_invalidates_cache_and_detail_matches(api): | |
| client, db, _, year = api | |
| assert client.get('/api/screener').json()[0]['est_total_shareholder_yield'] is None | |
| d = client.post('/api/admin/returns/documents', json=payload(year)).json() | |
| assert d['status'] == 'pending' | |
| assert client.get('/api/screener').json()[0]['return_status'] == 'unconfirmed' | |
| response = client.put(f"/api/admin/returns/documents/{d['id']}", json=payload(year, status='approved')) | |
| assert response.status_code == 200, response.text | |
| r = client.get('/api/screener').json()[0] | |
| assert r['est_total_shareholder_yield'] == 7 | |
| detail = client.get(f'/api/stock/005930?year={year}').json() | |
| assert detail['shareholder_returns']['est_total_shareholder_yield'] == 7 | |
| assert 'history' in detail and 'consensus' in detail | |
| assert client.get(f'/api/screener?year={year+1}').json()[0]['est_total_shareholder_yield'] is None | |
| assert client.get(f'/api/stock/005930?year={year-1}').status_code == 400 | |
| assert db.query(ReturnAudit).count() == 2 | |
| assert client.get(f"/api/admin/returns/documents/{d['id']}/audit").status_code == 200 | |
| def test_admin_permissions(api): | |
| client, _, user, year = api | |
| user['value'] = None | |
| assert client.get('/api/admin/returns/documents').status_code == 401 | |
| assert client.post('/api/admin/returns/documents', json=payload(year)).status_code == 401 | |
| user['value'] = User(id=2, is_admin=False) | |
| assert client.get('/api/admin/returns/sources').status_code == 403 | |
| assert client.post('/api/admin/returns/documents', json=payload(year)).status_code == 403 | |
| assert client.get('/api/screener').status_code == 200 | |
| def test_automatic_application_cancel_restore_and_audit(api): | |
| client,db,user,year=api | |
| d=client.post('/api/admin/returns/documents',json=payload(year,status='active')).json() | |
| assert d['status'] == 'active' | |
| assert client.get('/api/screener').json()[0]['est_total_shareholder_yield'] == 7 | |
| path=f"/api/admin/returns/documents/{d['id']}/status" | |
| assert client.post(path,json={'action':'cancel','reason':'중복 공시'}).json()['status'] == 'excluded' | |
| assert client.get('/api/screener').json()[0]['est_total_shareholder_yield'] is None | |
| detail=client.get(f'/api/stock/005930?year={year}').json()['shareholder_returns'] | |
| assert detail['return_sources'][0]['status'] == 'excluded' | |
| assert detail['return_sources'][0]['events'][0]['amount'] == 300 | |
| assert client.post(path,json={'action':'cancel'}).status_code == 200 | |
| assert db.query(ReturnAudit).count() == 2 | |
| assert client.post(path,json={'action':'restore'}).json()['status'] == 'active' | |
| assert client.get('/api/screener').json()[0]['est_total_shareholder_yield'] == 7 | |
| audits=client.get(f"/api/admin/returns/documents/{d['id']}/audit").json() | |
| assert [a['action'] for a in audits] == ['review','cancel','restore'] | |
| assert '중복 공시' in audits[1]['snapshot'] | |
| user['value']=None | |
| assert client.post(path,json={'action':'cancel'}).status_code == 401 | |
| user['value']=User(id=2,is_admin=False) | |
| assert client.post(path,json={'action':'cancel'}).status_code == 403 | |
| def test_restore_reference_does_not_invent_missing_amount(api): | |
| client,db,user,year=api | |
| d=ReturnDocument(ticker='005930',source_key='ref',source_url='https://example.com',title='참고',status='excluded',error='dart_status_014') | |
| db.add(d);db.commit() | |
| response=client.post(f'/api/admin/returns/documents/{d.id}/status',json={'action':'restore'}) | |
| assert response.json()['status'] == 'reference' | |
| assert client.get('/api/screener').json()[0]['est_total_shareholder_yield'] is None | |
| def test_review_list_uses_three_queries_for_many_documents(api): | |
| from sqlalchemy import event | |
| client,db,users,year=api | |
| for i in range(200): | |
| doc=ReturnDocument(ticker='005930',source_key=f'bulk:{i}',source_url='https://example.com/ir',title=f'소각 {i}',status='pending') | |
| db.add(doc);db.flush() | |
| db.add(ReturnEvent(document_id=doc.id,year=year,amount=i,scope='common')) | |
| db.add(ReturnPolicy(document_id=doc.id,year=year,kind='cancellation',amount=i,scope='common')) | |
| db.commit() | |
| assert users['value'].is_admin # Load authentication state before measuring list queries. | |
| queries=[] | |
| def record(conn,cursor,statement,parameters,context,executemany): | |
| if statement.lstrip().upper().startswith('SELECT'): | |
| queries.append(statement) | |
| engine=db.get_bind() | |
| event.listen(engine,'before_cursor_execute',record) | |
| try: | |
| response=client.get('/api/admin/returns/documents') | |
| finally: | |
| event.remove(engine,'before_cursor_execute',record) | |
| assert response.status_code == 200 | |
| documents=response.json() | |
| assert len(documents) == 200 | |
| assert all(len(d['events']) == len(d['policies']) == 1 for d in documents) | |
| assert len(queries) == 3 | |
| def test_supersession_cycle_and_duplicate_keys(api): | |
| client, _, _, year = api | |
| one = client.post('/api/admin/returns/documents', json=payload(year, status='approved')).json() | |
| assert client.post('/api/admin/returns/documents', json=payload(year)).status_code == 409 | |
| two = client.post('/api/admin/returns/documents', json=payload(year, source_key='manual:second', supersedes_id=one['id'], status='approved')).json() | |
| assert client.put(f"/api/admin/returns/documents/{one['id']}", json=payload(year, supersedes_id=two['id'], status='approved')).status_code == 422 | |
| assert client.get('/api/screener').json()[0]['est_cancellation_amount'] == 300 | |
| assert client.post('/api/admin/returns/documents', json=payload(year, source_key='manual:third', supersedes_id=one['id'], status='approved')).status_code == 409 | |
| def test_correction_requires_previous_source(api): | |
| client,_,_,year=api | |
| assert client.post('/api/admin/returns/documents',json=payload(year,title='기재정정 소각 결정',status='approved')).status_code == 422 | |
| def test_external_review_changes_invalidate_worker_cache(api): | |
| from return_api import ReviewInput, write_review | |
| client,db,_,year=api | |
| assert client.get('/api/screener').json()[0]['est_cancellation_amount'] is None | |
| # Simulate another process updating the database without clearing Python caches. | |
| write_review(db,ReviewInput(**payload(year,status='approved'))) | |
| assert client.get('/api/screener').json()[0]['est_cancellation_amount'] == 300 | |
| def test_sources_upsert_and_url_validation(api): | |
| client, db, _, year = api | |
| body = {'ticker':'005930','url':'https://example.com/ir','is_index':True} | |
| one = client.post('/api/admin/returns/sources',json=body).json() | |
| two = client.post('/api/admin/returns/sources',json={**body,'enabled':False}).json() | |
| assert one['id'] == two['id'] and not two['enabled'] | |
| assert client.post('/api/admin/returns/sources',json={**body,'url':'file:///etc/passwd'}).status_code == 422 | |
| def test_price_refresh_clears_cap_and_invalidates_cache(api, monkeypatch): | |
| client, db, _, year = api | |
| client.post('/api/admin/returns/documents',json=payload(year,status='approved')) | |
| assert client.get('/api/screener').json()[0]['est_total_shareholder_yield'] == 7 | |
| import requests | |
| monkeypatch.setattr(requests, 'get', lambda *a, **k: Mock(raise_for_status=lambda:None, json=lambda:{'closePrice':'20,000'})) | |
| response = client.post('/api/refresh/price/005930') | |
| assert response.json()['market_cap'] == 20000 | |
| assert client.get('/api/screener').json()[0]['est_total_shareholder_yield'] == 3.5 | |
| old = db.query(AnnualData).filter_by(is_estimate=False).first() | |
| old.shares_outstanding = None | |
| db.commit() | |
| assert client.post('/api/refresh/price/005930').json()['market_cap'] is None | |
| assert client.get('/api/screener').json()[0]['est_total_shareholder_yield'] is None | |
| def test_existing_sqlite_database_upgrade_preserves_data(monkeypatch): | |
| import database | |
| engine = create_engine('sqlite://', poolclass=StaticPool) | |
| # Simulate the pre-feature schema and a populated company record. | |
| old_tables = [t for name,t in Base.metadata.tables.items() if not name.startswith('return_')] | |
| Base.metadata.create_all(engine, tables=old_tables) | |
| with engine.begin() as conn: | |
| conn.execute(text("INSERT INTO company (ticker,name) VALUES ('005930','기존기업')")) | |
| monkeypatch.setattr(database,'engine',engine) | |
| monkeypatch.setattr(database,'_is_postgres',False) | |
| init_db() | |
| init_db() | |
| assert 'return_document' in inspect(engine).get_table_names() | |
| with engine.connect() as conn: | |
| assert conn.execute(text("SELECT name FROM company WHERE ticker='005930'")).scalar() == '기존기업' | |
| engine.dispose() | |
| def test_postgres_schema_compiles_without_altering_existing_tables(): | |
| from sqlalchemy.schema import CreateTable | |
| from sqlalchemy.dialects import postgresql | |
| for name,table in Base.metadata.tables.items(): | |
| if name.startswith('return_'): | |
| sql = str(CreateTable(table).compile(dialect=postgresql.dialect())) | |
| assert 'CREATE TABLE' in sql | |