Skip to content

Commit f552241

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 f552241

14 files changed

Lines changed: 7980 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: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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: |-
20+
[
21+
{
22+
"market": "KRW-BTC",
23+
"candle_date_time_utc": "2026-03-11T00:00:00",
24+
"candle_date_time_kst": "2026-03-11T09:00:00",
25+
"opening_price": 102417000.0,
26+
"high_price": 103988000.0,
27+
"low_price": 101150000.0,
28+
"trade_price": 103460000.0,
29+
"timestamp": 1773240059132,
30+
"candle_acc_trade_price": 104890599315.22821,
31+
"candle_acc_trade_volume": 1024.30999241,
32+
"prev_closing_price": 102417000.0,
33+
"change_price": 1043000.0,
34+
"change_rate": 0.0101838562
35+
}
36+
]
37+
headers:
38+
Cache-Control:
39+
- no-cache, no-store, max-age=0, must-revalidate
40+
Connection:
41+
- keep-alive
42+
Content-Type:
43+
- application/json;charset=UTF-8
44+
Date:
45+
- Wed, 11 Mar 2026 14:40:59 GMT
46+
ETag:
47+
- W/"0e2f0e810adeff5a52edede46f10a87be"
48+
Expires:
49+
- '0'
50+
Limit-By-Ip:
51+
- 'Yes'
52+
Pragma:
53+
- no-cache
54+
Remaining-Req:
55+
- group=candles; min=600; sec=7
56+
Transfer-Encoding:
57+
- chunked
58+
Vary:
59+
- origin,access-control-request-method,access-control-request-headers,accept-encoding
60+
content-length:
61+
- '455'
62+
status:
63+
code: 200
64+
message: ''
65+
version: 1
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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: |-
20+
[
21+
{
22+
"market": "KRW-BTC",
23+
"candle_date_time_utc": "2026-03-11T14:40:00",
24+
"candle_date_time_kst": "2026-03-11T23:40:00",
25+
"opening_price": 103600000.0,
26+
"high_price": 103600000.0,
27+
"low_price": 103425000.0,
28+
"trade_price": 103460000.0,
29+
"timestamp": 1773240059132,
30+
"candle_acc_trade_price": 164899503.14206,
31+
"candle_acc_trade_volume": 1.5933512,
32+
"unit": 1
33+
}
34+
]
35+
headers:
36+
Cache-Control:
37+
- no-cache, no-store, max-age=0, must-revalidate
38+
Connection:
39+
- keep-alive
40+
Content-Type:
41+
- application/json;charset=UTF-8
42+
Date:
43+
- Wed, 11 Mar 2026 14:40:59 GMT
44+
ETag:
45+
- W/"0a8e7aea04abff309a8a41bc33b086c11"
46+
Expires:
47+
- '0'
48+
Limit-By-Ip:
49+
- 'Yes'
50+
Pragma:
51+
- no-cache
52+
Remaining-Req:
53+
- group=candles; min=600; sec=9
54+
Transfer-Encoding:
55+
- chunked
56+
Vary:
57+
- origin,access-control-request-method,access-control-request-headers,accept-encoding
58+
content-length:
59+
- '359'
60+
status:
61+
code: 200
62+
message: ''
63+
version: 1
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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: |-
20+
[
21+
{
22+
"market": "KRW-BTC",
23+
"candle_date_time_utc": "2026-03-01T00:00:00",
24+
"candle_date_time_kst": "2026-03-01T09:00:00",
25+
"opening_price": 97626000.0,
26+
"high_price": 107547000.0,
27+
"low_price": 95100000.0,
28+
"trade_price": 103458000.0,
29+
"timestamp": 1773240058227,
30+
"candle_acc_trade_price": 2511125524827.9736,
31+
"candle_acc_trade_volume": 24783.48727285,
32+
"first_day_of_period": "2026-03-01"
33+
}
34+
]
35+
headers:
36+
Cache-Control:
37+
- no-cache, no-store, max-age=0, must-revalidate
38+
Connection:
39+
- keep-alive
40+
Content-Type:
41+
- application/json;charset=UTF-8
42+
Date:
43+
- Wed, 11 Mar 2026 14:40:59 GMT
44+
ETag:
45+
- W/"0c3baff016ae2faa833b0ddc4d154ba07"
46+
Expires:
47+
- '0'
48+
Limit-By-Ip:
49+
- 'Yes'
50+
Pragma:
51+
- no-cache
52+
Remaining-Req:
53+
- group=candles; min=600; sec=5
54+
Transfer-Encoding:
55+
- chunked
56+
Vary:
57+
- origin,access-control-request-method,access-control-request-headers,accept-encoding
58+
content-length:
59+
- '391'
60+
status:
61+
code: 200
62+
message: ''
63+
version: 1

0 commit comments

Comments
 (0)