feature: add CoinGecko cryptocurrency price extractor
Add sites/coingecko package with GetPrice() method that extracts structured crypto price data (name, symbol, price, 24h/7d change, market cap, volume, 24h high/low) from CoinGecko coin pages. Includes mock-based tests and parseLargeNumber helper for T/B/M suffixes. Closes #27 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
198
sites/coingecko/coingecko_test.go
Normal file
198
sites/coingecko/coingecko_test.go
Normal file
@@ -0,0 +1,198 @@
|
||||
package coingecko
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"gitea.stevedudenhoeffer.com/steve/go-extractor"
|
||||
"gitea.stevedudenhoeffer.com/steve/go-extractor/extractortest"
|
||||
)
|
||||
|
||||
func makeBitcoinDoc() *extractortest.MockDocument {
|
||||
return &extractortest.MockDocument{
|
||||
URLValue: "https://www.coingecko.com/en/coins/bitcoin",
|
||||
MockNode: extractortest.MockNode{
|
||||
Children: map[string]extractor.Nodes{
|
||||
"div[data-coin-show-target='coin'] h1": {
|
||||
&extractortest.MockNode{TextValue: "Bitcoin"},
|
||||
},
|
||||
"span[data-coin-symbol]": {
|
||||
&extractortest.MockNode{TextValue: "btc"},
|
||||
},
|
||||
"span[data-converter-target='price']": {
|
||||
&extractortest.MockNode{TextValue: "$97,234.56"},
|
||||
},
|
||||
"span[data-price-change='24h']": {
|
||||
&extractortest.MockNode{TextValue: "-2.34%"},
|
||||
},
|
||||
"span[data-price-change='7d']": {
|
||||
&extractortest.MockNode{TextValue: "5.67%"},
|
||||
},
|
||||
"span[data-coin-stat='market-cap']": {
|
||||
&extractortest.MockNode{TextValue: "$1.92T"},
|
||||
},
|
||||
"span[data-coin-stat='volume']": {
|
||||
&extractortest.MockNode{TextValue: "$28.5B"},
|
||||
},
|
||||
"span[data-coin-stat='high-24h']": {
|
||||
&extractortest.MockNode{TextValue: "$98,100.00"},
|
||||
},
|
||||
"span[data-coin-stat='low-24h']": {
|
||||
&extractortest.MockNode{TextValue: "$95,500.00"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractCoinPrice(t *testing.T) {
|
||||
doc := makeBitcoinDoc()
|
||||
|
||||
data, err := extractCoinPrice(doc)
|
||||
if err != nil {
|
||||
t.Fatalf("extractCoinPrice() error: %v", err)
|
||||
}
|
||||
|
||||
if data.Name != "Bitcoin" {
|
||||
t.Errorf("Name = %q, want %q", data.Name, "Bitcoin")
|
||||
}
|
||||
if data.Symbol != "BTC" {
|
||||
t.Errorf("Symbol = %q, want %q", data.Symbol, "BTC")
|
||||
}
|
||||
if data.Price != 97234.56 {
|
||||
t.Errorf("Price = %v, want 97234.56", data.Price)
|
||||
}
|
||||
if data.Change24h != -2.34 {
|
||||
t.Errorf("Change24h = %v, want -2.34", data.Change24h)
|
||||
}
|
||||
if data.Change7d != 5.67 {
|
||||
t.Errorf("Change7d = %v, want 5.67", data.Change7d)
|
||||
}
|
||||
if data.MarketCap != 1_920_000_000_000 {
|
||||
t.Errorf("MarketCap = %v, want 1920000000000", data.MarketCap)
|
||||
}
|
||||
if data.Volume24h != 28_500_000_000 {
|
||||
t.Errorf("Volume24h = %v, want 28500000000", data.Volume24h)
|
||||
}
|
||||
if data.High24h != 98100.00 {
|
||||
t.Errorf("High24h = %v, want 98100.00", data.High24h)
|
||||
}
|
||||
if data.Low24h != 95500.00 {
|
||||
t.Errorf("Low24h = %v, want 95500.00", data.Low24h)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractCoinPrice_NameFallback(t *testing.T) {
|
||||
doc := &extractortest.MockDocument{
|
||||
MockNode: extractortest.MockNode{
|
||||
Children: map[string]extractor.Nodes{
|
||||
"h1": {
|
||||
&extractortest.MockNode{TextValue: "Ethereum"},
|
||||
},
|
||||
"span[data-converter-target='price']": {
|
||||
&extractortest.MockNode{TextValue: "$3,456.78"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
data, err := extractCoinPrice(doc)
|
||||
if err != nil {
|
||||
t.Fatalf("extractCoinPrice() error: %v", err)
|
||||
}
|
||||
|
||||
if data.Name != "Ethereum" {
|
||||
t.Errorf("Name = %q, want %q", data.Name, "Ethereum")
|
||||
}
|
||||
if data.Price != 3456.78 {
|
||||
t.Errorf("Price = %v, want 3456.78", data.Price)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractCoinPrice_PriceFallback(t *testing.T) {
|
||||
doc := &extractortest.MockDocument{
|
||||
MockNode: extractortest.MockNode{
|
||||
Children: map[string]extractor.Nodes{
|
||||
"span[data-price-target='price']": {
|
||||
&extractortest.MockNode{TextValue: "$0.5432"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
data, err := extractCoinPrice(doc)
|
||||
if err != nil {
|
||||
t.Fatalf("extractCoinPrice() error: %v", err)
|
||||
}
|
||||
|
||||
if data.Price != 0.5432 {
|
||||
t.Errorf("Price = %v, want 0.5432", data.Price)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractCoinPrice_Empty(t *testing.T) {
|
||||
doc := &extractortest.MockDocument{
|
||||
MockNode: extractortest.MockNode{
|
||||
Children: map[string]extractor.Nodes{},
|
||||
},
|
||||
}
|
||||
|
||||
data, err := extractCoinPrice(doc)
|
||||
if err != nil {
|
||||
t.Fatalf("extractCoinPrice() error: %v", err)
|
||||
}
|
||||
|
||||
if data.Name != "" || data.Price != 0 {
|
||||
t.Error("expected zero values for empty doc")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPrice_MockBrowser(t *testing.T) {
|
||||
doc := makeBitcoinDoc()
|
||||
|
||||
browser := &extractortest.MockBrowser{
|
||||
Documents: map[string]*extractortest.MockDocument{
|
||||
"https://www.coingecko.com/en/coins/bitcoin": doc,
|
||||
},
|
||||
}
|
||||
|
||||
data, err := DefaultConfig.GetPrice(context.Background(), browser, "bitcoin")
|
||||
if err != nil {
|
||||
t.Fatalf("GetPrice() error: %v", err)
|
||||
}
|
||||
|
||||
if data.Name != "Bitcoin" {
|
||||
t.Errorf("Name = %q, want %q", data.Name, "Bitcoin")
|
||||
}
|
||||
if data.Price != 97234.56 {
|
||||
t.Errorf("Price = %v, want 97234.56", data.Price)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseLargeNumber(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
want float64
|
||||
}{
|
||||
{"$1.92T", 1_920_000_000_000},
|
||||
{"$28.5B", 28_500_000_000},
|
||||
{"$250M", 250_000_000},
|
||||
{"$1,234,567", 1234567},
|
||||
{"$0.50", 0.50},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := parseLargeNumber(tt.input)
|
||||
if got != tt.want {
|
||||
t.Errorf("parseLargeNumber(%q) = %v, want %v", tt.input, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoinURL(t *testing.T) {
|
||||
got := coinURL("Bitcoin")
|
||||
want := "https://www.coingecko.com/en/coins/bitcoin"
|
||||
if got != want {
|
||||
t.Errorf("coinURL(\"Bitcoin\") = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user