Skip to content

Commit ff8f384

Browse files
gylimclaude
andcommitted
test: add VCR cassette-based integration tests for quotation API
Record real Upbit API responses as VCR cassettes and add structural validation tests for all 11 quotation endpoints. Closes #37 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent cf444ba commit ff8f384

14 files changed

Lines changed: 722 additions & 1 deletion

tests/__init__.py

Whitespace-only changes.

tests/api/test_quotation_vcr.py

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
from __future__ import annotations
2+
3+
from tests.conftest import upbeat_vcr
4+
from upbeat import Upbeat
5+
from upbeat.types.quotation import (
6+
CandleDay,
7+
CandleMinute,
8+
CandlePeriod,
9+
CandleSecond,
10+
Orderbook,
11+
OrderbookInstrument,
12+
Ticker,
13+
Trade,
14+
)
15+
16+
17+
def _client() -> Upbeat:
18+
return Upbeat(max_retries=0, auto_throttle=False)
19+
20+
21+
# ── Tickers ─────────────────────────────────────────────────────────────
22+
23+
24+
class TestGetTickers:
25+
@upbeat_vcr.use_cassette("quotation/get_tickers.yaml")
26+
def test_returns_valid_tickers(self) -> None:
27+
with _client() as client:
28+
result = client.quotation.get_tickers("KRW-BTC")
29+
30+
assert len(result) >= 1
31+
ticker = result[0]
32+
assert isinstance(ticker, Ticker)
33+
assert ticker.market == "KRW-BTC"
34+
assert ticker.change in ("EVEN", "RISE", "FALL")
35+
assert isinstance(ticker.trade_price, float)
36+
assert isinstance(ticker.timestamp, int)
37+
38+
39+
class TestGetTickersByQuote:
40+
@upbeat_vcr.use_cassette("quotation/get_tickers_by_quote.yaml")
41+
def test_returns_valid_tickers(self) -> None:
42+
with _client() as client:
43+
result = client.quotation.get_tickers_by_quote("KRW")
44+
45+
assert len(result) >= 1
46+
for ticker in result:
47+
assert isinstance(ticker, Ticker)
48+
assert ticker.market.startswith("KRW-")
49+
50+
51+
# ── Candles ─────────────────────────────────────────────────────────────
52+
53+
54+
class TestGetCandlesMinutes:
55+
@upbeat_vcr.use_cassette("quotation/get_candles_minutes.yaml")
56+
def test_returns_valid_candles(self) -> None:
57+
with _client() as client:
58+
result = client.quotation.get_candles_minutes(market="KRW-BTC", unit=1)
59+
60+
assert len(result) >= 1
61+
candle = result[0]
62+
assert isinstance(candle, CandleMinute)
63+
assert candle.market == "KRW-BTC"
64+
assert isinstance(candle.unit, int)
65+
assert isinstance(candle.opening_price, float)
66+
67+
68+
class TestGetCandlesSeconds:
69+
@upbeat_vcr.use_cassette("quotation/get_candles_seconds.yaml")
70+
def test_returns_valid_candles(self) -> None:
71+
with _client() as client:
72+
result = client.quotation.get_candles_seconds(market="KRW-BTC")
73+
74+
assert len(result) >= 1
75+
candle = result[0]
76+
assert isinstance(candle, CandleSecond)
77+
assert candle.market == "KRW-BTC"
78+
assert isinstance(candle.opening_price, float)
79+
80+
81+
class TestGetCandlesDays:
82+
@upbeat_vcr.use_cassette("quotation/get_candles_days.yaml")
83+
def test_returns_valid_candles(self) -> None:
84+
with _client() as client:
85+
result = client.quotation.get_candles_days(market="KRW-BTC")
86+
87+
assert len(result) >= 1
88+
candle = result[0]
89+
assert isinstance(candle, CandleDay)
90+
assert candle.market == "KRW-BTC"
91+
assert isinstance(candle.prev_closing_price, float)
92+
assert isinstance(candle.change_price, float)
93+
assert isinstance(candle.change_rate, float)
94+
95+
96+
class TestGetCandlesWeeks:
97+
@upbeat_vcr.use_cassette("quotation/get_candles_weeks.yaml")
98+
def test_returns_valid_candles(self) -> None:
99+
with _client() as client:
100+
result = client.quotation.get_candles_weeks(market="KRW-BTC")
101+
102+
assert len(result) >= 1
103+
candle = result[0]
104+
assert isinstance(candle, CandlePeriod)
105+
assert candle.market == "KRW-BTC"
106+
assert isinstance(candle.first_day_of_period, str)
107+
108+
109+
class TestGetCandlesMonths:
110+
@upbeat_vcr.use_cassette("quotation/get_candles_months.yaml")
111+
def test_returns_valid_candles(self) -> None:
112+
with _client() as client:
113+
result = client.quotation.get_candles_months(market="KRW-BTC")
114+
115+
assert len(result) >= 1
116+
candle = result[0]
117+
assert isinstance(candle, CandlePeriod)
118+
assert candle.market == "KRW-BTC"
119+
assert isinstance(candle.first_day_of_period, str)
120+
121+
122+
class TestGetCandlesYears:
123+
@upbeat_vcr.use_cassette("quotation/get_candles_years.yaml")
124+
def test_returns_valid_candles(self) -> None:
125+
with _client() as client:
126+
result = client.quotation.get_candles_years(market="KRW-BTC")
127+
128+
assert len(result) >= 1
129+
candle = result[0]
130+
assert isinstance(candle, CandlePeriod)
131+
assert candle.market == "KRW-BTC"
132+
assert isinstance(candle.first_day_of_period, str)
133+
134+
135+
# ── Orderbooks ──────────────────────────────────────────────────────────
136+
137+
138+
class TestGetOrderbooks:
139+
@upbeat_vcr.use_cassette("quotation/get_orderbooks.yaml")
140+
def test_returns_valid_orderbooks(self) -> None:
141+
with _client() as client:
142+
result = client.quotation.get_orderbooks("KRW-BTC")
143+
144+
assert len(result) >= 1
145+
ob = result[0]
146+
assert isinstance(ob, Orderbook)
147+
assert ob.market == "KRW-BTC"
148+
assert len(ob.orderbook_units) >= 1
149+
unit = ob.orderbook_units[0]
150+
assert isinstance(unit.ask_price, float)
151+
assert isinstance(unit.bid_price, float)
152+
153+
154+
class TestGetOrderbookInstruments:
155+
@upbeat_vcr.use_cassette("quotation/get_orderbook_instruments.yaml")
156+
def test_returns_valid_instruments(self) -> None:
157+
with _client() as client:
158+
result = client.quotation.get_orderbook_instruments("KRW-BTC")
159+
160+
assert len(result) >= 1
161+
inst = result[0]
162+
assert isinstance(inst, OrderbookInstrument)
163+
assert inst.market == "KRW-BTC"
164+
assert isinstance(inst.supported_levels, list)
165+
166+
167+
# ── Trades ──────────────────────────────────────────────────────────────
168+
169+
170+
class TestGetTrades:
171+
@upbeat_vcr.use_cassette("quotation/get_trades.yaml")
172+
def test_returns_valid_trades(self) -> None:
173+
with _client() as client:
174+
result = client.quotation.get_trades("KRW-BTC")
175+
176+
assert len(result) >= 1
177+
trade = result[0]
178+
assert isinstance(trade, Trade)
179+
assert trade.market == "KRW-BTC"
180+
assert trade.ask_bid in ("ASK", "BID")
181+
assert isinstance(trade.trade_price, float)
182+
assert isinstance(trade.sequential_id, int)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
interactions:
2+
- request:
3+
body: ''
4+
headers:
5+
Accept:
6+
- '*/*'
7+
Accept-Encoding:
8+
- gzip, deflate
9+
Connection:
10+
- keep-alive
11+
Host:
12+
- api.upbit.com
13+
User-Agent:
14+
- python-httpx/0.28.1
15+
method: GET
16+
uri: https://api.upbit.com/v1/candles/days?market=KRW-BTC
17+
response:
18+
body:
19+
string: '[{"market":"KRW-BTC","candle_date_time_utc":"2026-03-11T00:00:00","candle_date_time_kst":"2026-03-11T09:00:00","opening_price":102417000.00000000,"high_price":103988000.00000000,"low_price":101150000.00000000,"trade_price":103266000.00000000,"timestamp":1773239757526,"candle_acc_trade_price":102978167919.53824000,"candle_acc_trade_volume":1005.84581426,"prev_closing_price":102417000.00000000,"change_price":849000.00000000,"change_rate":0.0082896394}]'
20+
headers:
21+
Cache-Control:
22+
- no-cache, no-store, max-age=0, must-revalidate
23+
Connection:
24+
- keep-alive
25+
Content-Type:
26+
- application/json;charset=UTF-8
27+
Date:
28+
- Wed, 11 Mar 2026 14:36:01 GMT
29+
ETag:
30+
- W/"029ecccae2a72fbe19b9758cb7a721f93"
31+
Expires:
32+
- '0'
33+
Limit-By-Ip:
34+
- 'Yes'
35+
Pragma:
36+
- no-cache
37+
Remaining-Req:
38+
- group=candles; min=600; sec=7
39+
Transfer-Encoding:
40+
- chunked
41+
Vary:
42+
- origin,access-control-request-method,access-control-request-headers,accept-encoding
43+
content-length:
44+
- '454'
45+
status:
46+
code: 200
47+
message: ''
48+
version: 1
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
interactions:
2+
- request:
3+
body: ''
4+
headers:
5+
Accept:
6+
- '*/*'
7+
Accept-Encoding:
8+
- gzip, deflate
9+
Connection:
10+
- keep-alive
11+
Host:
12+
- api.upbit.com
13+
User-Agent:
14+
- python-httpx/0.28.1
15+
method: GET
16+
uri: https://api.upbit.com/v1/candles/minutes/1?market=KRW-BTC
17+
response:
18+
body:
19+
string: '[{"market":"KRW-BTC","candle_date_time_utc":"2026-03-11T14:35:00","candle_date_time_kst":"2026-03-11T23:35:00","opening_price":103159000.00000000,"high_price":103400000.00000000,"low_price":103159000.00000000,"trade_price":103266000.00000000,"timestamp":1773239757526,"candle_acc_trade_price":158764957.73950000,"candle_acc_trade_volume":1.53670146,"unit":1}]'
20+
headers:
21+
Cache-Control:
22+
- no-cache, no-store, max-age=0, must-revalidate
23+
Connection:
24+
- keep-alive
25+
Content-Type:
26+
- application/json;charset=UTF-8
27+
Date:
28+
- Wed, 11 Mar 2026 14:36:01 GMT
29+
ETag:
30+
- W/"06032a2d9289f11a2b389a3b11da2d28b"
31+
Expires:
32+
- '0'
33+
Limit-By-Ip:
34+
- 'Yes'
35+
Pragma:
36+
- no-cache
37+
Remaining-Req:
38+
- group=candles; min=600; sec=9
39+
Transfer-Encoding:
40+
- chunked
41+
Vary:
42+
- origin,access-control-request-method,access-control-request-headers,accept-encoding
43+
content-length:
44+
- '359'
45+
status:
46+
code: 200
47+
message: ''
48+
version: 1
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
interactions:
2+
- request:
3+
body: ''
4+
headers:
5+
Accept:
6+
- '*/*'
7+
Accept-Encoding:
8+
- gzip, deflate
9+
Connection:
10+
- keep-alive
11+
Host:
12+
- api.upbit.com
13+
User-Agent:
14+
- python-httpx/0.28.1
15+
method: GET
16+
uri: https://api.upbit.com/v1/candles/months?market=KRW-BTC
17+
response:
18+
body:
19+
string: '[{"market":"KRW-BTC","candle_date_time_utc":"2026-03-01T00:00:00","candle_date_time_kst":"2026-03-01T09:00:00","opening_price":97626000.00000000,"high_price":107547000.00000000,"low_price":95100000.00000000,"trade_price":103266000.00000000,"timestamp":1773239757526,"candle_acc_trade_price":2509214356853.28538000,"candle_acc_trade_volume":24765.03530608,"first_day_of_period":"2026-03-01"}]'
20+
headers:
21+
Cache-Control:
22+
- no-cache, no-store, max-age=0, must-revalidate
23+
Connection:
24+
- keep-alive
25+
Content-Type:
26+
- application/json;charset=UTF-8
27+
Date:
28+
- Wed, 11 Mar 2026 14:36:01 GMT
29+
ETag:
30+
- W/"0013f0168900e9f44dabecee514a7df8b"
31+
Expires:
32+
- '0'
33+
Limit-By-Ip:
34+
- 'Yes'
35+
Pragma:
36+
- no-cache
37+
Remaining-Req:
38+
- group=candles; min=600; sec=5
39+
Transfer-Encoding:
40+
- chunked
41+
Vary:
42+
- origin,access-control-request-method,access-control-request-headers,accept-encoding
43+
content-length:
44+
- '391'
45+
status:
46+
code: 200
47+
message: ''
48+
version: 1
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
interactions:
2+
- request:
3+
body: ''
4+
headers:
5+
Accept:
6+
- '*/*'
7+
Accept-Encoding:
8+
- gzip, deflate
9+
Connection:
10+
- keep-alive
11+
Host:
12+
- api.upbit.com
13+
User-Agent:
14+
- python-httpx/0.28.1
15+
method: GET
16+
uri: https://api.upbit.com/v1/candles/seconds?market=KRW-BTC
17+
response:
18+
body:
19+
string: '[{"market":"KRW-BTC","candle_date_time_utc":"2026-03-11T14:35:57","candle_date_time_kst":"2026-03-11T23:35:57","opening_price":103264000.00000000,"high_price":103266000.00000000,"low_price":103264000.00000000,"trade_price":103266000.00000000,"timestamp":1773239757526,"candle_acc_trade_price":29806.31824000,"candle_acc_trade_volume":0.00028864}]'
20+
headers:
21+
Cache-Control:
22+
- no-cache, no-store, max-age=0, must-revalidate
23+
Connection:
24+
- keep-alive
25+
Content-Type:
26+
- application/json;charset=UTF-8
27+
Date:
28+
- Wed, 11 Mar 2026 14:36:01 GMT
29+
ETag:
30+
- W/"0c176e8f744f73300f7b966cb289bc998"
31+
Expires:
32+
- '0'
33+
Limit-By-Ip:
34+
- 'Yes'
35+
Pragma:
36+
- no-cache
37+
Remaining-Req:
38+
- group=candles; min=600; sec=8
39+
Transfer-Encoding:
40+
- chunked
41+
Vary:
42+
- origin,access-control-request-method,access-control-request-headers,accept-encoding
43+
content-length:
44+
- '346'
45+
status:
46+
code: 200
47+
message: ''
48+
version: 1

0 commit comments

Comments
 (0)