1+ import pytest
2+ import pandas as pd
3+ from unittest .mock import patch , Mock
4+ from chaindl .scraper import glassnode
5+
6+ def test_parse_metric_path_basic ():
7+ url = "https://studio.glassnode.com/charts/addresses.ActiveCountWithContracts?a=ETH"
8+ path , asset , snake_case = glassnode ._parse_metric_path (url )
9+ assert asset == "ETH"
10+ assert "active_count_with_contracts" in snake_case
11+ assert path .endswith (snake_case )
12+
13+ def test_parse_metric_path_default_asset ():
14+ url = "https://studio.glassnode.com/charts/addresses.ActiveCountWithContracts"
15+ path , asset , snake_case = glassnode ._parse_metric_path (url )
16+ assert asset == "BTC"
17+
18+
19+ def test_process_metric_json_simple ():
20+ json_data = [{"t" : 1680000000 , "v" : 42 }, {"t" : 1680000600 , "v" : 43 }]
21+ df = glassnode ._process_metric_json (json_data , "metric_name" )
22+ assert isinstance (df , pd .DataFrame )
23+ assert "metric_name" in df .columns
24+ assert df .index .name is 'Date'
25+
26+ def test_process_metric_json_nested ():
27+ json_data = [{"t" : 1680000000 , "o" : {"a" : 1 , "b" : 2 }}, {"t" : 1680000600 , "o" : {"a" : 3 , "b" : 4 }}]
28+ df = glassnode ._process_metric_json (json_data , "ignored" )
29+ assert isinstance (df , pd .DataFrame )
30+ assert "a" in df .columns and "b" in df .columns
31+ assert df .shape == (2 , 2 ) # Only a and b remain
32+
33+ @patch ("chaindl.scraper.glassnode.SESSION.get" )
34+ def test_fetch_json_success (mock_get ):
35+ mock_resp = Mock ()
36+ mock_resp .raise_for_status .return_value = None
37+ mock_resp .json .return_value = {"key" : "value" }
38+ mock_get .return_value = mock_resp
39+ data = glassnode ._fetch_json ("https://fakeurl.com" )
40+ assert data ["key" ] == "value"
41+
42+ @patch ("chaindl.scraper.glassnode.SESSION.get" )
43+ def test_download_includes_price (mock_get ):
44+ # Mock metric response
45+ metric_resp = Mock ()
46+ metric_resp .raise_for_status .return_value = None
47+ metric_resp .json .return_value = [{"t" : 1680000000 , "v" : 100 }]
48+ # Mock price response
49+ price_resp = Mock ()
50+ price_resp .raise_for_status .return_value = None
51+ price_resp .json .return_value = [{"t" : 1680000000 , "v" : 50000 }]
52+
53+ mock_get .side_effect = [Mock (), metric_resp , price_resp ] # First GET for cookies
54+ url = "https://studio.glassnode.com/charts/addresses.ActiveCountWithContracts?a=ETH"
55+
56+ df = glassnode ._download (url )
57+ assert "price_usd_close" in df .columns
58+ assert "active_count_with_contracts" in df .columns
59+ assert isinstance (df , pd .DataFrame )
60+
61+ @patch ("chaindl.scraper.glassnode.SESSION.get" )
62+ def test_download_raises_on_required_plan (mock_get ):
63+ resp = Mock ()
64+ resp .raise_for_status .return_value = None
65+ resp .json .return_value = {"requiredPlan" : True }
66+ mock_get .side_effect = [Mock (), resp ]
67+
68+ url = "https://studio.glassnode.com/charts/addresses.ActiveCountWithContracts?a=ETH"
69+ with pytest .raises (RuntimeError , match = "Data error" ):
70+ glassnode ._download (url )
71+
72+ # Integration tests
73+ def test_download_active_count_btc ():
74+ url = "https://studio.glassnode.com/charts/addresses.ActiveCount?a=BTC"
75+ df = glassnode ._download (url )
76+ assert "active_count" in df .columns
77+ assert "price_usd_close" in df .columns
78+ assert not df .empty
79+
80+ def test_download_stock_to_flow_btc ():
81+ url = "https://studio.glassnode.com/charts/indicators.StockToFlowRatio?a=BTC"
82+ df = glassnode ._download (url )
83+ assert "daysTillHalving" in df .columns
84+ assert "price" in df .columns
85+ assert "ratio" in df .columns
86+ assert "price_usd_close" in df .columns
87+ assert not df .empty
88+
89+ def test_download_invalid_url ():
90+ url = "https://studio.glassnode.com/charts/addresses.NotARealMetric?a=BTC"
91+ with pytest .raises (RuntimeError ):
92+ glassnode ._download (url )
0 commit comments