Files
go-extractor/sites/coingecko/coingecko_test.go
Steve Dudenhoeffer 349b1b9c6b
All checks were successful
CI / build (pull_request) Successful in 46s
CI / vet (pull_request) Successful in 1m20s
CI / test (pull_request) Successful in 1m23s
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>
2026-02-15 16:47:53 +00:00

199 lines
4.8 KiB
Go

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)
}
}