app is now using historical exchange rates for transactions
This commit is contained in:
+230
-16
@@ -17,32 +17,56 @@ type ExchangeRateResponse struct {
|
||||
Rates map[string]float64 `json:"rates"`
|
||||
}
|
||||
|
||||
// HistoricalExchangeRateResponse represents historical rate response
|
||||
type HistoricalExchangeRateResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Historical bool `json:"historical"`
|
||||
Base string `json:"base"`
|
||||
Date string `json:"date"`
|
||||
Rates map[string]float64 `json:"rates"`
|
||||
TimeLastUpdate int64 `json:"time_last_update_unix"`
|
||||
}
|
||||
|
||||
// CachedRate stores exchange rate with timestamp
|
||||
type CachedRate struct {
|
||||
Rate float64
|
||||
Timestamp time.Time
|
||||
Date string // The date this rate is for (YYYY-MM-DD format)
|
||||
}
|
||||
|
||||
// Currency conversion cache
|
||||
var (
|
||||
exchangeRateCache = make(map[string]CachedRate)
|
||||
cacheMutex sync.RWMutex
|
||||
cacheValidDuration = 1 * time.Hour // Cache rates for 1 hour
|
||||
cacheValidDuration = 24 * time.Hour // Cache historical rates for 24 hours
|
||||
currentRateCache = 1 * time.Hour // Cache current rates for 1 hour
|
||||
)
|
||||
|
||||
// GetExchangeRate fetches the exchange rate from fromCurrency to toCurrency
|
||||
// GetExchangeRate fetches the current exchange rate from fromCurrency to toCurrency
|
||||
func GetExchangeRate(fromCurrency, toCurrency string) (float64, error) {
|
||||
return GetHistoricalExchangeRate(fromCurrency, toCurrency, time.Now())
|
||||
}
|
||||
|
||||
// GetHistoricalExchangeRate fetches the exchange rate for a specific date
|
||||
func GetHistoricalExchangeRate(fromCurrency, toCurrency string, date time.Time) (float64, error) {
|
||||
// If currencies are the same, return 1.0
|
||||
if fromCurrency == toCurrency {
|
||||
return 1.0, nil
|
||||
}
|
||||
|
||||
cacheKey := fmt.Sprintf("%s_%s", fromCurrency, toCurrency)
|
||||
dateStr := date.Format("2006-01-02")
|
||||
cacheKey := fmt.Sprintf("%s_%s_%s", fromCurrency, toCurrency, dateStr)
|
||||
|
||||
// Check cache first
|
||||
cacheMutex.RLock()
|
||||
if cached, exists := exchangeRateCache[cacheKey]; exists {
|
||||
if time.Since(cached.Timestamp) < cacheValidDuration {
|
||||
// Determine cache validity based on whether it's historical or current
|
||||
validDuration := cacheValidDuration
|
||||
if isToday(date) {
|
||||
validDuration = currentRateCache
|
||||
}
|
||||
|
||||
if time.Since(cached.Timestamp) < validDuration {
|
||||
cacheMutex.RUnlock()
|
||||
return cached.Rate, nil
|
||||
}
|
||||
@@ -50,7 +74,7 @@ func GetExchangeRate(fromCurrency, toCurrency string) (float64, error) {
|
||||
cacheMutex.RUnlock()
|
||||
|
||||
// Fetch from API if not in cache or cache is expired
|
||||
rate, err := fetchExchangeRateFromAPI(fromCurrency, toCurrency)
|
||||
rate, err := fetchHistoricalExchangeRateFromAPI(fromCurrency, toCurrency, date)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -60,16 +84,27 @@ func GetExchangeRate(fromCurrency, toCurrency string) (float64, error) {
|
||||
exchangeRateCache[cacheKey] = CachedRate{
|
||||
Rate: rate,
|
||||
Timestamp: time.Now(),
|
||||
Date: dateStr,
|
||||
}
|
||||
cacheMutex.Unlock()
|
||||
|
||||
return rate, nil
|
||||
}
|
||||
|
||||
// fetchExchangeRateFromAPI fetches exchange rate from a free API
|
||||
func fetchExchangeRateFromAPI(fromCurrency, toCurrency string) (float64, error) {
|
||||
// Using exchangerate-api.com (completely free, no API key required)
|
||||
url := fmt.Sprintf("https://api.exchangerate-api.com/v4/latest/%s", fromCurrency)
|
||||
// fetchHistoricalExchangeRateFromAPI fetches exchange rate for a specific date
|
||||
func fetchHistoricalExchangeRateFromAPI(fromCurrency, toCurrency string, date time.Time) (float64, error) {
|
||||
var url string
|
||||
dateStr := date.Format("2006-01-02")
|
||||
today := time.Now().Format("2006-01-02")
|
||||
|
||||
// Use different endpoints for historical vs current rates
|
||||
if dateStr == today {
|
||||
// Current rates
|
||||
url = fmt.Sprintf("https://api.exchangerate-api.com/v4/latest/%s", fromCurrency)
|
||||
} else {
|
||||
// Historical rates - try exchangerate-api.com historical endpoint
|
||||
url = fmt.Sprintf("https://api.exchangerate-api.com/v4/historical/%s/%s", fromCurrency, dateStr)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
@@ -78,7 +113,7 @@ func fetchExchangeRateFromAPI(fromCurrency, toCurrency string) (float64, error)
|
||||
|
||||
req.Header.Set("User-Agent", "Portfolio-Tracker/1.0")
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
client := &http.Client{Timeout: 15 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("error fetching exchange rate: %v", err)
|
||||
@@ -86,6 +121,10 @@ func fetchExchangeRateFromAPI(fromCurrency, toCurrency string) (float64, error)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
// If historical API fails, try fallback strategy
|
||||
if dateStr != today {
|
||||
return fetchHistoricalRateFallback(fromCurrency, toCurrency, date)
|
||||
}
|
||||
return 0, fmt.Errorf("API returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
@@ -94,7 +133,7 @@ func fetchExchangeRateFromAPI(fromCurrency, toCurrency string) (float64, error)
|
||||
return 0, fmt.Errorf("error reading response: %v", err)
|
||||
}
|
||||
|
||||
// Parse exchangerate-api.com response format
|
||||
// Try parsing as standard response first
|
||||
var apiResp struct {
|
||||
Base string `json:"base"`
|
||||
Date string `json:"date"`
|
||||
@@ -113,13 +152,111 @@ func fetchExchangeRateFromAPI(fromCurrency, toCurrency string) (float64, error)
|
||||
return rate, nil
|
||||
}
|
||||
|
||||
// ConvertCurrency converts an amount from one currency to another
|
||||
// fetchHistoricalRateFallback tries alternative methods for historical rates
|
||||
func fetchHistoricalRateFallback(fromCurrency, toCurrency string, date time.Time) (float64, error) {
|
||||
// Try frankfurter.app as fallback for historical rates (free and reliable)
|
||||
dateStr := date.Format("2006-01-02")
|
||||
url := fmt.Sprintf("https://api.frankfurter.app/%s?from=%s&to=%s", dateStr, fromCurrency, toCurrency)
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("error creating fallback request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "Portfolio-Tracker/1.0")
|
||||
|
||||
client := &http.Client{Timeout: 15 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
// If historical rate fails, use current rate as last resort
|
||||
return getCurrentRateAsFallback(fromCurrency, toCurrency)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return getCurrentRateAsFallback(fromCurrency, toCurrency)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return getCurrentRateAsFallback(fromCurrency, toCurrency)
|
||||
}
|
||||
|
||||
var fallbackResp struct {
|
||||
Amount float64 `json:"amount"`
|
||||
Base string `json:"base"`
|
||||
Date string `json:"date"`
|
||||
Rates map[string]float64 `json:"rates"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &fallbackResp); err != nil {
|
||||
return getCurrentRateAsFallback(fromCurrency, toCurrency)
|
||||
}
|
||||
|
||||
if rate, exists := fallbackResp.Rates[toCurrency]; exists {
|
||||
return rate, nil
|
||||
}
|
||||
|
||||
return getCurrentRateAsFallback(fromCurrency, toCurrency)
|
||||
}
|
||||
|
||||
// getCurrentRateAsFallback gets current rate when historical rate is unavailable
|
||||
func getCurrentRateAsFallback(fromCurrency, toCurrency string) (float64, error) {
|
||||
url := fmt.Sprintf("https://api.exchangerate-api.com/v4/latest/%s", fromCurrency)
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("error creating fallback request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "Portfolio-Tracker/1.0")
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("error fetching fallback rate: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return 0, fmt.Errorf("fallback API returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("error reading fallback response: %v", err)
|
||||
}
|
||||
|
||||
var apiResp struct {
|
||||
Base string `json:"base"`
|
||||
Date string `json:"date"`
|
||||
Rates map[string]float64 `json:"rates"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &apiResp); err != nil {
|
||||
return 0, fmt.Errorf("error parsing fallback JSON: %v", err)
|
||||
}
|
||||
|
||||
rate, exists := apiResp.Rates[toCurrency]
|
||||
if !exists {
|
||||
return 0, fmt.Errorf("currency %s not found in fallback response", toCurrency)
|
||||
}
|
||||
|
||||
return rate, nil
|
||||
}
|
||||
|
||||
// ConvertCurrency converts an amount from one currency to another using current rates
|
||||
func ConvertCurrency(amount float64, fromCurrency, toCurrency string) (float64, error) {
|
||||
return ConvertCurrencyHistorical(amount, fromCurrency, toCurrency, time.Now())
|
||||
}
|
||||
|
||||
// ConvertCurrencyHistorical converts an amount using historical exchange rate for a specific date
|
||||
func ConvertCurrencyHistorical(amount float64, fromCurrency, toCurrency string, date time.Time) (float64, error) {
|
||||
if fromCurrency == toCurrency {
|
||||
return amount, nil
|
||||
}
|
||||
|
||||
rate, err := GetExchangeRate(fromCurrency, toCurrency)
|
||||
rate, err := GetHistoricalExchangeRate(fromCurrency, toCurrency, date)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -178,11 +315,16 @@ func FormatCurrencyWithSymbol(amount float64, currency string) string {
|
||||
|
||||
// ConvertAndFormat converts currency and formats it nicely
|
||||
func ConvertAndFormat(amount float64, fromCurrency, toCurrency string) string {
|
||||
return ConvertAndFormatHistorical(amount, fromCurrency, toCurrency, time.Now())
|
||||
}
|
||||
|
||||
// ConvertAndFormatHistorical converts currency using historical rate and formats it
|
||||
func ConvertAndFormatHistorical(amount float64, fromCurrency, toCurrency string, date time.Time) string {
|
||||
if fromCurrency == toCurrency {
|
||||
return FormatCurrencyWithSymbol(amount, toCurrency)
|
||||
}
|
||||
|
||||
convertedAmount, err := ConvertCurrency(amount, fromCurrency, toCurrency)
|
||||
convertedAmount, err := ConvertCurrencyHistorical(amount, fromCurrency, toCurrency, date)
|
||||
if err != nil {
|
||||
// If conversion fails, return original amount with note
|
||||
return fmt.Sprintf("%.2f %s (conv. error)", amount, fromCurrency)
|
||||
@@ -191,13 +333,18 @@ func ConvertAndFormat(amount float64, fromCurrency, toCurrency string) string {
|
||||
return FormatCurrencyWithSymbol(convertedAmount, toCurrency)
|
||||
}
|
||||
|
||||
// BatchConvertCurrency converts multiple amounts in one API call for efficiency
|
||||
// BatchConvertCurrency converts multiple amounts in one API call for efficiency using current rates
|
||||
func BatchConvertCurrency(amounts []float64, fromCurrency, toCurrency string) ([]float64, error) {
|
||||
return BatchConvertCurrencyHistorical(amounts, fromCurrency, toCurrency, time.Now())
|
||||
}
|
||||
|
||||
// BatchConvertCurrencyHistorical converts multiple amounts using historical rate for a specific date
|
||||
func BatchConvertCurrencyHistorical(amounts []float64, fromCurrency, toCurrency string, date time.Time) ([]float64, error) {
|
||||
if fromCurrency == toCurrency {
|
||||
return amounts, nil
|
||||
}
|
||||
|
||||
rate, err := GetExchangeRate(fromCurrency, toCurrency)
|
||||
rate, err := GetHistoricalExchangeRate(fromCurrency, toCurrency, date)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -228,3 +375,70 @@ func GetCacheInfo() map[string]time.Time {
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
// GetDetailedCacheInfo returns detailed information about cached exchange rates
|
||||
func GetDetailedCacheInfo() map[string]CachedRate {
|
||||
cacheMutex.RLock()
|
||||
defer cacheMutex.RUnlock()
|
||||
|
||||
info := make(map[string]CachedRate)
|
||||
for key, cached := range exchangeRateCache {
|
||||
info[key] = cached
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
// CleanExpiredCache removes expired cache entries
|
||||
func CleanExpiredCache() int {
|
||||
cacheMutex.Lock()
|
||||
defer cacheMutex.Unlock()
|
||||
|
||||
removed := 0
|
||||
now := time.Now()
|
||||
|
||||
for key, cached := range exchangeRateCache {
|
||||
// Determine if this is a current or historical rate
|
||||
validDuration := cacheValidDuration
|
||||
if isToday(parseDate(cached.Date)) {
|
||||
validDuration = currentRateCache
|
||||
}
|
||||
|
||||
if now.Sub(cached.Timestamp) > validDuration {
|
||||
delete(exchangeRateCache, key)
|
||||
removed++
|
||||
}
|
||||
}
|
||||
|
||||
return removed
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
// isToday checks if a date is today
|
||||
func isToday(date time.Time) bool {
|
||||
now := time.Now()
|
||||
return date.Year() == now.Year() && date.Month() == now.Month() && date.Day() == now.Day()
|
||||
}
|
||||
|
||||
// parseDate parses a date string in YYYY-MM-DD format
|
||||
func parseDate(dateStr string) time.Time {
|
||||
date, err := time.Parse("2006-01-02", dateStr)
|
||||
if err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
return date
|
||||
}
|
||||
|
||||
// GetExchangeRateWithFallback gets exchange rate with multiple fallback strategies
|
||||
func GetExchangeRateWithFallback(fromCurrency, toCurrency string, date time.Time) (float64, bool, error) {
|
||||
rate, err := GetHistoricalExchangeRate(fromCurrency, toCurrency, date)
|
||||
if err != nil {
|
||||
// Try with current date as fallback
|
||||
currentRate, currentErr := GetExchangeRate(fromCurrency, toCurrency)
|
||||
if currentErr != nil {
|
||||
return 0, false, fmt.Errorf("both historical (%v) and current (%v) rate fetching failed", err, currentErr)
|
||||
}
|
||||
return currentRate, false, nil // false indicates fallback was used
|
||||
}
|
||||
return rate, true, nil // true indicates historical rate was used
|
||||
}
|
||||
|
||||
+223
-14
@@ -153,13 +153,15 @@ func TestGetCacheInfo(t *testing.T) {
|
||||
// Add test data to cache
|
||||
testTime := time.Now()
|
||||
cacheMutex.Lock()
|
||||
exchangeRateCache["EUR_USD"] = CachedRate{
|
||||
exchangeRateCache["EUR_USD_2024-01-01"] = CachedRate{
|
||||
Rate: 1.2,
|
||||
Timestamp: testTime,
|
||||
Date: "2024-01-01",
|
||||
}
|
||||
exchangeRateCache["GBP_EUR"] = CachedRate{
|
||||
exchangeRateCache["GBP_EUR_2024-01-01"] = CachedRate{
|
||||
Rate: 1.15,
|
||||
Timestamp: testTime,
|
||||
Date: "2024-01-01",
|
||||
}
|
||||
cacheMutex.Unlock()
|
||||
|
||||
@@ -169,14 +171,14 @@ func TestGetCacheInfo(t *testing.T) {
|
||||
t.Errorf("Expected 2 cache entries, got %d", len(info))
|
||||
}
|
||||
|
||||
if timestamp, exists := info["EUR_USD"]; !exists {
|
||||
t.Error("Expected EUR_USD entry in cache info")
|
||||
if timestamp, exists := info["EUR_USD_2024-01-01"]; !exists {
|
||||
t.Error("Expected EUR_USD_2024-01-01 entry in cache info")
|
||||
} else if !timestamp.Equal(testTime) {
|
||||
t.Error("Expected timestamp to match test time")
|
||||
}
|
||||
|
||||
if timestamp, exists := info["GBP_EUR"]; !exists {
|
||||
t.Error("Expected GBP_EUR entry in cache info")
|
||||
if timestamp, exists := info["GBP_EUR_2024-01-01"]; !exists {
|
||||
t.Error("Expected GBP_EUR_2024-01-01 entry in cache info")
|
||||
} else if !timestamp.Equal(testTime) {
|
||||
t.Error("Expected timestamp to match test time")
|
||||
}
|
||||
@@ -186,20 +188,227 @@ func TestGetCacheInfo(t *testing.T) {
|
||||
func TestCacheExpiration(t *testing.T) {
|
||||
ClearCache()
|
||||
|
||||
// Add expired entry to cache
|
||||
expiredTime := time.Now().Add(-2 * time.Hour) // 2 hours ago
|
||||
// Add expired entry to cache with a specific rate
|
||||
expiredTime := time.Now().Add(-25 * time.Hour) // 25 hours ago (older than 24h cache)
|
||||
testDate := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
cacheKey := "USD_EUR_2024-01-01"
|
||||
expiredRate := 999.999 // Unrealistic rate to detect if cache is used
|
||||
|
||||
cacheMutex.Lock()
|
||||
exchangeRateCache["TEST_EXPIRED"] = CachedRate{
|
||||
Rate: 1.0,
|
||||
exchangeRateCache[cacheKey] = CachedRate{
|
||||
Rate: expiredRate,
|
||||
Timestamp: expiredTime,
|
||||
Date: "2024-01-01",
|
||||
}
|
||||
cacheMutex.Unlock()
|
||||
|
||||
// This should not use the expired cache (but will fail API call)
|
||||
// We're mainly testing that it doesn't return the cached expired value
|
||||
_, err := GetExchangeRate("TEST", "EXPIRED")
|
||||
// Verify the expired entry exists
|
||||
cacheMutex.RLock()
|
||||
_, exists := exchangeRateCache[cacheKey]
|
||||
cacheMutex.RUnlock()
|
||||
|
||||
if !exists {
|
||||
t.Error("Expected expired cache entry to exist before test")
|
||||
return
|
||||
}
|
||||
|
||||
// This should not use the expired cache, it should try to fetch from API
|
||||
// Even if API fails, it should not return the expired cached rate
|
||||
rate, err := GetHistoricalExchangeRate("USD", "EUR", testDate)
|
||||
|
||||
// Either:
|
||||
// 1. We get a reasonable rate from API (not the cached unrealistic rate)
|
||||
// 2. We get an error (which is fine, means cache wasn't used)
|
||||
if err == nil {
|
||||
t.Error("Expected error due to invalid currency pair, but got none")
|
||||
// If we got a rate, it should not be the expired cached rate
|
||||
if rate == expiredRate {
|
||||
t.Errorf("Function returned expired cached rate %f, should have fetched from API", expiredRate)
|
||||
}
|
||||
// A reasonable EUR/USD rate should be between 0.5 and 2.0
|
||||
if rate < 0.5 || rate > 2.0 {
|
||||
t.Errorf("Got unreasonable exchange rate %f, might be using expired cache", rate)
|
||||
}
|
||||
}
|
||||
// If err != nil, that's fine - it means the API call was attempted and failed
|
||||
// The important thing is that the expired cache was not used
|
||||
}
|
||||
|
||||
// Test historical exchange rate functionality
|
||||
func TestGetHistoricalExchangeRate_SameCurrency(t *testing.T) {
|
||||
testDate := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
rate, err := GetHistoricalExchangeRate("USD", "USD", testDate)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error for same currency, got %v", err)
|
||||
}
|
||||
if rate != 1.0 {
|
||||
t.Errorf("Expected rate 1.0 for same currency, got %f", rate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertCurrencyHistorical_SameCurrency(t *testing.T) {
|
||||
amount := 100.0
|
||||
testDate := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
converted, err := ConvertCurrencyHistorical(amount, "EUR", "EUR", testDate)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error for same currency conversion, got %v", err)
|
||||
}
|
||||
if converted != amount {
|
||||
t.Errorf("Expected %f for same currency conversion, got %f", amount, converted)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchConvertCurrencyHistorical_SameCurrency(t *testing.T) {
|
||||
amounts := []float64{100.0, 200.0, 300.0}
|
||||
currency := "EUR"
|
||||
testDate := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
converted, err := BatchConvertCurrencyHistorical(amounts, currency, currency, testDate)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error for same currency batch conversion, got %v", err)
|
||||
}
|
||||
|
||||
if len(converted) != len(amounts) {
|
||||
t.Errorf("Expected %d converted amounts, got %d", len(amounts), len(converted))
|
||||
}
|
||||
|
||||
for i, amount := range amounts {
|
||||
if converted[i] != amount {
|
||||
t.Errorf("Expected converted[%d] = %f, got %f", i, amount, converted[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertAndFormatHistorical_SameCurrency(t *testing.T) {
|
||||
amount := 100.0
|
||||
currency := "USD"
|
||||
testDate := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
result := ConvertAndFormatHistorical(amount, currency, currency, testDate)
|
||||
expected := FormatCurrencyWithSymbol(amount, currency)
|
||||
|
||||
if result != expected {
|
||||
t.Errorf("ConvertAndFormatHistorical(%f, %s, %s, %v) = %s, expected %s",
|
||||
amount, currency, currency, testDate, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetExchangeRateWithFallback_SameCurrency(t *testing.T) {
|
||||
testDate := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
rate, isHistorical, err := GetExchangeRateWithFallback("USD", "USD", testDate)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error for same currency, got %v", err)
|
||||
}
|
||||
if rate != 1.0 {
|
||||
t.Errorf("Expected rate 1.0 for same currency, got %f", rate)
|
||||
}
|
||||
if !isHistorical {
|
||||
t.Error("Expected isHistorical to be true for same currency")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCleanExpiredCache(t *testing.T) {
|
||||
// Clear cache first
|
||||
ClearCache()
|
||||
|
||||
// Add some test data to cache
|
||||
now := time.Now()
|
||||
expiredTime := now.Add(-25 * time.Hour) // Older than 24 hours
|
||||
recentTime := now.Add(-30 * time.Minute) // Recent
|
||||
|
||||
cacheMutex.Lock()
|
||||
exchangeRateCache["OLD_RATE_2024-01-01"] = CachedRate{
|
||||
Rate: 1.0,
|
||||
Timestamp: expiredTime,
|
||||
Date: "2024-01-01",
|
||||
}
|
||||
exchangeRateCache["NEW_RATE_"+now.Format("2006-01-02")] = CachedRate{
|
||||
Rate: 1.1,
|
||||
Timestamp: recentTime,
|
||||
Date: now.Format("2006-01-02"),
|
||||
}
|
||||
cacheMutex.Unlock()
|
||||
|
||||
// Clean expired cache
|
||||
removed := CleanExpiredCache()
|
||||
|
||||
// Should have removed the old entry
|
||||
if removed != 1 {
|
||||
t.Errorf("Expected 1 removed entry, got %d", removed)
|
||||
}
|
||||
|
||||
// Verify the recent entry is still there
|
||||
info := GetCacheInfo()
|
||||
if len(info) != 1 {
|
||||
t.Errorf("Expected 1 remaining cache entry, got %d", len(info))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDetailedCacheInfo(t *testing.T) {
|
||||
// Clear cache first
|
||||
ClearCache()
|
||||
|
||||
// Add test data to cache
|
||||
testTime := time.Now()
|
||||
cacheMutex.Lock()
|
||||
exchangeRateCache["EUR_USD_2024-01-01"] = CachedRate{
|
||||
Rate: 1.2,
|
||||
Timestamp: testTime,
|
||||
Date: "2024-01-01",
|
||||
}
|
||||
cacheMutex.Unlock()
|
||||
|
||||
info := GetDetailedCacheInfo()
|
||||
|
||||
if len(info) != 1 {
|
||||
t.Errorf("Expected 1 cache entry, got %d", len(info))
|
||||
}
|
||||
|
||||
if cached, exists := info["EUR_USD_2024-01-01"]; !exists {
|
||||
t.Error("Expected EUR_USD_2024-01-01 entry in detailed cache info")
|
||||
} else {
|
||||
if cached.Rate != 1.2 {
|
||||
t.Errorf("Expected rate 1.2, got %f", cached.Rate)
|
||||
}
|
||||
if cached.Date != "2024-01-01" {
|
||||
t.Errorf("Expected date 2024-01-01, got %s", cached.Date)
|
||||
}
|
||||
if !cached.Timestamp.Equal(testTime) {
|
||||
t.Error("Expected timestamp to match test time")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsToday(t *testing.T) {
|
||||
now := time.Now()
|
||||
yesterday := now.AddDate(0, 0, -1)
|
||||
tomorrow := now.AddDate(0, 0, 1)
|
||||
|
||||
if !isToday(now) {
|
||||
t.Error("Expected isToday(now) to be true")
|
||||
}
|
||||
|
||||
if isToday(yesterday) {
|
||||
t.Error("Expected isToday(yesterday) to be false")
|
||||
}
|
||||
|
||||
if isToday(tomorrow) {
|
||||
t.Error("Expected isToday(tomorrow) to be false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDate(t *testing.T) {
|
||||
testDate := "2024-01-01"
|
||||
expected := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
result := parseDate(testDate)
|
||||
|
||||
if !result.Equal(expected) {
|
||||
t.Errorf("Expected parsed date %v, got %v", expected, result)
|
||||
}
|
||||
|
||||
// Test invalid date
|
||||
invalidResult := parseDate("invalid-date")
|
||||
if !invalidResult.IsZero() {
|
||||
t.Error("Expected zero time for invalid date")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user