From 4ca1df00c51b33e4dad5339a6aa5a44201293534 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 09:45:19 +0000 Subject: [PATCH 1/5] Initial plan From b23bf0a08a2050a74189c5ddbd9e0aa0d1c2c77e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 09:51:02 +0000 Subject: [PATCH 2/5] Add /v1/rates/named-symbols endpoint Co-authored-by: kamaal111 <37084924+kamaal111@users.noreply.github.com> --- README.md | 27 +++++ handlers/named_symbols.go | 42 +++++++ handlers/named_symbols_test.go | 121 ++++++++++++++++++++ handlers/service.go | 76 +++++++++++++ routers/rates.go | 1 + test/integration/helpers.go | 5 + test/integration/integration_test.go | 163 +++++++++++++++++++++++++++ 7 files changed, 435 insertions(+) create mode 100644 handlers/named_symbols.go create mode 100644 handlers/named_symbols_test.go diff --git a/README.md b/README.md index be5b71b..412a50d 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,33 @@ curl "http://localhost:8000/v1/rates/symbols" {"date":"2025-12-05","symbols":["EUR","USD","GBP","JPY","CHF","AUD","CAD"]} ``` +### Get All Available Currency Symbols with Names + +``` +GET /v1/rates/named-symbols +``` + +Returns the latest available currency symbols from the database, each with a human-readable name. Returns `404` if no symbols data has been stored yet. + +#### Example Request + +```bash +curl "http://localhost:8000/v1/rates/named-symbols" +``` + +#### Example Response + +```json +{ + "date": "2025-12-05", + "symbols": [ + {"symbol": "EUR", "name": "Euro"}, + {"symbol": "USD", "name": "US Dollar"}, + {"symbol": "GBP", "name": "British Pound Sterling"} + ] +} +``` + ### Get Latest Exchange Rates ``` diff --git a/handlers/named_symbols.go b/handlers/named_symbols.go new file mode 100644 index 0000000..6510442 --- /dev/null +++ b/handlers/named_symbols.go @@ -0,0 +1,42 @@ +package handlers + +import ( + "context" + "encoding/json" + "net/http" + + "github.com/kamaal111/forex-api/database" + "github.com/kamaal111/forex-api/utils" +) + +func GetNamedSymbols(writer http.ResponseWriter, request *http.Request) { + ctx := context.Background() + client, err := database.CreateClient(ctx) + if err != nil { + utils.ErrorHandler(writer, err.Error(), http.StatusInternalServerError) + return + } + defer client.Close() + + repo := NewFirestoreRatesRepository(ctx, client) + service := NewRatesService(repo) + + record, err := service.GetAllNamedSymbols() + if err != nil { + utils.ErrorHandler(writer, err.Error(), http.StatusInternalServerError) + return + } + if record == nil { + utils.ErrorHandler(writer, "symbols not found", http.StatusNotFound) + return + } + + output, err := json.Marshal(record) + if err != nil { + utils.ErrorHandler(writer, err.Error(), http.StatusInternalServerError) + return + } + + writer.Header().Set("content-type", "application/json") + writer.Write(output) +} diff --git a/handlers/named_symbols_test.go b/handlers/named_symbols_test.go new file mode 100644 index 0000000..73114d2 --- /dev/null +++ b/handlers/named_symbols_test.go @@ -0,0 +1,121 @@ +package handlers + +import ( + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/kamaal111/forex-api/utils" +) + +func TestableNamedSymbolsHandler(repo RatesRepository) http.HandlerFunc { + return func(writer http.ResponseWriter, request *http.Request) { + service := NewRatesService(repo) + + record, err := service.GetAllNamedSymbols() + if err != nil { + utils.ErrorHandler(writer, err.Error(), http.StatusInternalServerError) + return + } + if record == nil { + utils.ErrorHandler(writer, "symbols not found", http.StatusNotFound) + return + } + + output, err := json.Marshal(record) + if err != nil { + utils.ErrorHandler(writer, err.Error(), http.StatusInternalServerError) + return + } + + writer.Header().Set("content-type", "application/json") + writer.Write(output) + } +} + +func TestGetNamedSymbolsHandler(t *testing.T) { + tests := []struct { + name string + mockRecord *SymbolsRecord + mockErr error + wantStatusCode int + wantSymbols []NamedSymbol + }{ + { + name: "returns named symbols for symbols that have rates in the database", + mockRecord: &SymbolsRecord{Date: "2025-11-21", Symbols: []string{"EUR", "USD", "GBP"}}, + mockErr: nil, + wantStatusCode: http.StatusOK, + wantSymbols: []NamedSymbol{ + {Symbol: "EUR", Name: "Euro"}, + {Symbol: "USD", Name: "US Dollar"}, + {Symbol: "GBP", Name: "British Pound Sterling"}, + }, + }, + { + name: "returns 404 when no data exists in the database", + mockRecord: nil, + mockErr: nil, + wantStatusCode: http.StatusNotFound, + wantSymbols: nil, + }, + { + name: "returns 500 on database error", + mockRecord: nil, + mockErr: errors.New("database error"), + wantStatusCode: http.StatusInternalServerError, + wantSymbols: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockRepo := &MockRatesRepository{ + GetAllSymbolsFunc: func() (*SymbolsRecord, error) { + return tt.mockRecord, tt.mockErr + }, + } + + handler := TestableNamedSymbolsHandler(mockRepo) + + req := httptest.NewRequest(http.MethodGet, "/v1/rates/named-symbols", nil) + recorder := httptest.NewRecorder() + + handler(recorder, req) + + if recorder.Code != tt.wantStatusCode { + t.Errorf("GetNamedSymbols() status = %d, want %d", recorder.Code, tt.wantStatusCode) + } + + if tt.wantSymbols != nil { + contentType := recorder.Header().Get("content-type") + if contentType != "application/json" { + t.Errorf("GetNamedSymbols() content-type = %q, want %q", contentType, "application/json") + } + + var record NamedSymbolsRecord + if err := json.NewDecoder(recorder.Body).Decode(&record); err != nil { + t.Fatalf("failed to decode response: %v", err) + } + + if len(record.Symbols) != len(tt.wantSymbols) { + t.Errorf("GetNamedSymbols() returned %d symbols, want %d", len(record.Symbols), len(tt.wantSymbols)) + } + + for i, expected := range tt.wantSymbols { + if i >= len(record.Symbols) { + break + } + if record.Symbols[i].Symbol != expected.Symbol { + t.Errorf("GetNamedSymbols() symbols[%d].symbol = %q, want %q", i, record.Symbols[i].Symbol, expected.Symbol) + } + if record.Symbols[i].Name != expected.Name { + t.Errorf("GetNamedSymbols() symbols[%d].name = %q, want %q", i, record.Symbols[i].Name, expected.Name) + } + } + } + }) + } +} diff --git a/handlers/service.go b/handlers/service.go index cb10766..ed52407 100644 --- a/handlers/service.go +++ b/handlers/service.go @@ -17,6 +17,16 @@ type SymbolsRecord struct { Symbols []string `json:"symbols" firestore:"symbols"` } +type NamedSymbol struct { + Symbol string `json:"symbol"` + Name string `json:"name"` +} + +type NamedSymbolsRecord struct { + Date string `json:"date"` + Symbols []NamedSymbol `json:"symbols"` +} + type RatesRepository interface { GetLatestRate(base string) (*ExchangeRateRecord, error) GetAllSymbols() (*SymbolsRecord, error) @@ -64,6 +74,27 @@ func (s *RatesService) GetAllSymbols() (*SymbolsRecord, error) { return s.Repository.GetAllSymbols() } +func (s *RatesService) GetAllNamedSymbols() (*NamedSymbolsRecord, error) { + record, err := s.Repository.GetAllSymbols() + if err != nil { + return nil, err + } + if record == nil { + return nil, nil + } + + named := make([]NamedSymbol, 0, len(record.Symbols)) + for _, symbol := range record.Symbols { + name, ok := CurrencyNames[symbol] + if !ok { + name = symbol + } + named = append(named, NamedSymbol{Symbol: symbol, Name: name}) + } + + return &NamedSymbolsRecord{Date: record.Date, Symbols: named}, nil +} + func NormalizeBase(base string) string { normalized := strings.ToUpper(strings.TrimSpace(base)) if !utils.ArrayContains(Currencies, normalized) { @@ -132,3 +163,48 @@ var Currencies = []string{ "THB", "ZAR", } + +var CurrencyNames = map[string]string{ + "EUR": "Euro", + "USD": "US Dollar", + "JPY": "Japanese Yen", + "BGN": "Bulgarian Lev", + "CYP": "Cypriot Pound", + "CZK": "Czech Koruna", + "DKK": "Danish Krone", + "EEK": "Estonian Kroon", + "GBP": "British Pound Sterling", + "HUF": "Hungarian Forint", + "LTL": "Lithuanian Litas", + "LVL": "Latvian Lats", + "MTL": "Maltese Lira", + "PLN": "Polish Zloty", + "ROL": "Romanian Leu (old)", + "RON": "Romanian Leu", + "SEK": "Swedish Krona", + "SIT": "Slovenian Tolar", + "SKK": "Slovak Koruna", + "CHF": "Swiss Franc", + "ISK": "Icelandic Krona", + "ILS": "Israeli New Shekel", + "NOK": "Norwegian Krone", + "HRK": "Croatian Kuna", + "RUB": "Russian Ruble", + "TRL": "Turkish Lira (old)", + "TRY": "Turkish Lira", + "AUD": "Australian Dollar", + "BRL": "Brazilian Real", + "CAD": "Canadian Dollar", + "CNY": "Chinese Yuan", + "HKD": "Hong Kong Dollar", + "IDR": "Indonesian Rupiah", + "INR": "Indian Rupee", + "KRW": "South Korean Won", + "MXN": "Mexican Peso", + "MYR": "Malaysian Ringgit", + "NZD": "New Zealand Dollar", + "PHP": "Philippine Peso", + "SGD": "Singapore Dollar", + "THB": "Thai Baht", + "ZAR": "South African Rand", +} diff --git a/routers/rates.go b/routers/rates.go index 425ae7e..a80a3a9 100644 --- a/routers/rates.go +++ b/routers/rates.go @@ -9,4 +9,5 @@ import ( func ratesGroup(mux *http.ServeMux) { mux.Handle("/v1/rates/latest", loggerMiddleware(http.HandlerFunc(handlers.GetLatest))) mux.Handle("/v1/rates/symbols", loggerMiddleware(http.HandlerFunc(handlers.GetSymbols))) + mux.Handle("/v1/rates/named-symbols", loggerMiddleware(http.HandlerFunc(handlers.GetNamedSymbols))) } diff --git a/test/integration/helpers.go b/test/integration/helpers.go index e7f7abc..15a6d4d 100644 --- a/test/integration/helpers.go +++ b/test/integration/helpers.go @@ -156,6 +156,11 @@ func (s *ServerProcess) GetSymbols() (*http.Response, error) { return http.Get(url) } +func (s *ServerProcess) GetNamedSymbols() (*http.Response, error) { + url := fmt.Sprintf("%s/v1/rates/named-symbols", s.baseURL) + return http.Get(url) +} + func (s *ServerProcess) GetLatest(base, symbols string) (*http.Response, error) { url := fmt.Sprintf("%s/v1/rates/latest", s.baseURL) if base != "" || symbols != "" { diff --git a/test/integration/integration_test.go b/test/integration/integration_test.go index 3adae6d..c6355d3 100644 --- a/test/integration/integration_test.go +++ b/test/integration/integration_test.go @@ -22,6 +22,16 @@ type ErrorResponse struct { Message string `json:"message"` } +type NamedSymbol struct { + Symbol string `json:"symbol"` + Name string `json:"name"` +} + +type NamedSymbolsRecord struct { + Date string `json:"date"` + Symbols []NamedSymbol `json:"symbols"` +} + func TestGetLatestEndpoint(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") @@ -322,6 +332,159 @@ func TestGetSymbolsEndpoint(t *testing.T) { }) } +func TestGetNamedSymbolsEndpoint(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + + tc := NewTestContext() + if err := tc.Setup(0); err != nil { + t.Fatalf("Failed to setup test context: %v", err) + } + defer tc.Teardown() + + t.Run("returns 404 when no data exists in the database", func(t *testing.T) { + if err := tc.ClearCollection("symbols"); err != nil { + t.Fatalf("Failed to clear collection: %v", err) + } + + resp, err := tc.Server.GetNamedSymbols() + if err != nil { + t.Fatalf("Failed to make request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusNotFound { + t.Errorf("Expected status 404, got %d", resp.StatusCode) + } + }) + + t.Run("returns named symbols with human-readable names", func(t *testing.T) { + if err := tc.ClearCollection("symbols"); err != nil { + t.Fatalf("Failed to clear collection: %v", err) + } + + _, err := tc.DB.Collection("symbols").Doc("2025-11-21").Set(tc.Ctx, map[string]interface{}{ + "date": "2025-11-21", + "symbols": []string{"EUR", "USD", "GBP"}, + }) + if err != nil { + t.Fatalf("Failed to seed data: %v", err) + } + + resp, err := tc.Server.GetNamedSymbols() + if err != nil { + t.Fatalf("Failed to make request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + t.Fatalf("Expected status 200, got %d: %s", resp.StatusCode, string(body)) + } + + var record NamedSymbolsRecord + if err := json.NewDecoder(resp.Body).Decode(&record); err != nil { + t.Fatalf("Failed to decode response: %v", err) + } + + if record.Date != "2025-11-21" { + t.Errorf("Expected date %q, got %q", "2025-11-21", record.Date) + } + + wantSymbols := []NamedSymbol{ + {Symbol: "EUR", Name: "Euro"}, + {Symbol: "USD", Name: "US Dollar"}, + {Symbol: "GBP", Name: "British Pound Sterling"}, + } + + if len(record.Symbols) != len(wantSymbols) { + t.Fatalf("Expected %d symbols, got %d: %v", len(wantSymbols), len(record.Symbols), record.Symbols) + } + + for i, want := range wantSymbols { + got := record.Symbols[i] + if got.Symbol != want.Symbol { + t.Errorf("symbols[%d].symbol = %q, want %q", i, got.Symbol, want.Symbol) + } + if got.Name != want.Name { + t.Errorf("symbols[%d].name = %q, want %q", i, got.Name, want.Name) + } + } + }) + + t.Run("returns only the latest symbols from the database", func(t *testing.T) { + if err := tc.ClearCollection("symbols"); err != nil { + t.Fatalf("Failed to clear collection: %v", err) + } + + _, err := tc.DB.Collection("symbols").Doc("2025-11-20").Set(tc.Ctx, map[string]interface{}{ + "date": "2025-11-20", + "symbols": []string{"EUR", "USD"}, + }) + if err != nil { + t.Fatalf("Failed to seed older data: %v", err) + } + + _, err = tc.DB.Collection("symbols").Doc("2025-11-21").Set(tc.Ctx, map[string]interface{}{ + "date": "2025-11-21", + "symbols": []string{"EUR", "USD", "GBP"}, + }) + if err != nil { + t.Fatalf("Failed to seed latest data: %v", err) + } + + resp, err := tc.Server.GetNamedSymbols() + if err != nil { + t.Fatalf("Failed to make request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + t.Fatalf("Expected status 200, got %d: %s", resp.StatusCode, string(body)) + } + + var record NamedSymbolsRecord + if err := json.NewDecoder(resp.Body).Decode(&record); err != nil { + t.Fatalf("Failed to decode response: %v", err) + } + + if record.Date != "2025-11-21" { + t.Errorf("Expected date %q, got %q", "2025-11-21", record.Date) + } + + if len(record.Symbols) != 3 { + t.Errorf("Expected 3 symbols from latest entry, got %d: %v", len(record.Symbols), record.Symbols) + } + }) + + t.Run("response has application/json content-type", func(t *testing.T) { + if err := tc.ClearCollection("symbols"); err != nil { + t.Fatalf("Failed to clear collection: %v", err) + } + + _, err := tc.DB.Collection("symbols").Doc("2025-11-21").Set(tc.Ctx, map[string]interface{}{ + "date": "2025-11-21", + "symbols": []string{"EUR"}, + }) + if err != nil { + t.Fatalf("Failed to seed data: %v", err) + } + + resp, err := tc.Server.GetNamedSymbols() + if err != nil { + t.Fatalf("Failed to make request: %v", err) + } + defer resp.Body.Close() + + contentType := resp.Header.Get("Content-Type") + if contentType != "application/json" { + t.Errorf("Expected Content-Type application/json, got %s", contentType) + } + }) +} + func TestContentType(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") From 9e70e9ca524121a3d6b7e6200351f660efff4dbb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 10:05:06 +0000 Subject: [PATCH 3/5] Rename named-symbols endpoint to currencies, rename symbols field to data, derive Currencies from CurrencyNames Co-authored-by: kamaal111 <37084924+kamaal111@users.noreply.github.com> --- README.md | 10 ++--- handlers/named_symbols.go | 2 +- handlers/named_symbols_test.go | 28 ++++++------- handlers/service.go | 63 ++++++---------------------- routers/rates.go | 2 +- test/integration/helpers.go | 4 +- test/integration/integration_test.go | 34 +++++++-------- 7 files changed, 53 insertions(+), 90 deletions(-) diff --git a/README.md b/README.md index 412a50d..b3769ac 100644 --- a/README.md +++ b/README.md @@ -110,18 +110,18 @@ curl "http://localhost:8000/v1/rates/symbols" {"date":"2025-12-05","symbols":["EUR","USD","GBP","JPY","CHF","AUD","CAD"]} ``` -### Get All Available Currency Symbols with Names +### Get All Available Currencies with Names ``` -GET /v1/rates/named-symbols +GET /v1/rates/currencies ``` -Returns the latest available currency symbols from the database, each with a human-readable name. Returns `404` if no symbols data has been stored yet. +Returns the latest available currencies from the database, each with a human-readable name. Returns `404` if no symbols data has been stored yet. #### Example Request ```bash -curl "http://localhost:8000/v1/rates/named-symbols" +curl "http://localhost:8000/v1/rates/currencies" ``` #### Example Response @@ -129,7 +129,7 @@ curl "http://localhost:8000/v1/rates/named-symbols" ```json { "date": "2025-12-05", - "symbols": [ + "data": [ {"symbol": "EUR", "name": "Euro"}, {"symbol": "USD", "name": "US Dollar"}, {"symbol": "GBP", "name": "British Pound Sterling"} diff --git a/handlers/named_symbols.go b/handlers/named_symbols.go index 6510442..c4a8208 100644 --- a/handlers/named_symbols.go +++ b/handlers/named_symbols.go @@ -9,7 +9,7 @@ import ( "github.com/kamaal111/forex-api/utils" ) -func GetNamedSymbols(writer http.ResponseWriter, request *http.Request) { +func GetCurrencies(writer http.ResponseWriter, request *http.Request) { ctx := context.Background() client, err := database.CreateClient(ctx) if err != nil { diff --git a/handlers/named_symbols_test.go b/handlers/named_symbols_test.go index 73114d2..084235b 100644 --- a/handlers/named_symbols_test.go +++ b/handlers/named_symbols_test.go @@ -10,7 +10,7 @@ import ( "github.com/kamaal111/forex-api/utils" ) -func TestableNamedSymbolsHandler(repo RatesRepository) http.HandlerFunc { +func TestableCurrenciesHandler(repo RatesRepository) http.HandlerFunc { return func(writer http.ResponseWriter, request *http.Request) { service := NewRatesService(repo) @@ -35,7 +35,7 @@ func TestableNamedSymbolsHandler(repo RatesRepository) http.HandlerFunc { } } -func TestGetNamedSymbolsHandler(t *testing.T) { +func TestGetCurrenciesHandler(t *testing.T) { tests := []struct { name string mockRecord *SymbolsRecord @@ -78,41 +78,41 @@ func TestGetNamedSymbolsHandler(t *testing.T) { }, } - handler := TestableNamedSymbolsHandler(mockRepo) + handler := TestableCurrenciesHandler(mockRepo) - req := httptest.NewRequest(http.MethodGet, "/v1/rates/named-symbols", nil) + req := httptest.NewRequest(http.MethodGet, "/v1/rates/currencies", nil) recorder := httptest.NewRecorder() handler(recorder, req) if recorder.Code != tt.wantStatusCode { - t.Errorf("GetNamedSymbols() status = %d, want %d", recorder.Code, tt.wantStatusCode) + t.Errorf("GetCurrencies() status = %d, want %d", recorder.Code, tt.wantStatusCode) } if tt.wantSymbols != nil { contentType := recorder.Header().Get("content-type") if contentType != "application/json" { - t.Errorf("GetNamedSymbols() content-type = %q, want %q", contentType, "application/json") + t.Errorf("GetCurrencies() content-type = %q, want %q", contentType, "application/json") } - var record NamedSymbolsRecord + var record CurrenciesRecord if err := json.NewDecoder(recorder.Body).Decode(&record); err != nil { t.Fatalf("failed to decode response: %v", err) } - if len(record.Symbols) != len(tt.wantSymbols) { - t.Errorf("GetNamedSymbols() returned %d symbols, want %d", len(record.Symbols), len(tt.wantSymbols)) + if len(record.Data) != len(tt.wantSymbols) { + t.Errorf("GetCurrencies() returned %d symbols, want %d", len(record.Data), len(tt.wantSymbols)) } for i, expected := range tt.wantSymbols { - if i >= len(record.Symbols) { + if i >= len(record.Data) { break } - if record.Symbols[i].Symbol != expected.Symbol { - t.Errorf("GetNamedSymbols() symbols[%d].symbol = %q, want %q", i, record.Symbols[i].Symbol, expected.Symbol) + if record.Data[i].Symbol != expected.Symbol { + t.Errorf("GetCurrencies() data[%d].symbol = %q, want %q", i, record.Data[i].Symbol, expected.Symbol) } - if record.Symbols[i].Name != expected.Name { - t.Errorf("GetNamedSymbols() symbols[%d].name = %q, want %q", i, record.Symbols[i].Name, expected.Name) + if record.Data[i].Name != expected.Name { + t.Errorf("GetCurrencies() data[%d].name = %q, want %q", i, record.Data[i].Name, expected.Name) } } } diff --git a/handlers/service.go b/handlers/service.go index ed52407..ead8989 100644 --- a/handlers/service.go +++ b/handlers/service.go @@ -22,9 +22,9 @@ type NamedSymbol struct { Name string `json:"name"` } -type NamedSymbolsRecord struct { - Date string `json:"date"` - Symbols []NamedSymbol `json:"symbols"` +type CurrenciesRecord struct { + Date string `json:"date"` + Data []NamedSymbol `json:"data"` } type RatesRepository interface { @@ -74,7 +74,7 @@ func (s *RatesService) GetAllSymbols() (*SymbolsRecord, error) { return s.Repository.GetAllSymbols() } -func (s *RatesService) GetAllNamedSymbols() (*NamedSymbolsRecord, error) { +func (s *RatesService) GetAllNamedSymbols() (*CurrenciesRecord, error) { record, err := s.Repository.GetAllSymbols() if err != nil { return nil, err @@ -92,7 +92,7 @@ func (s *RatesService) GetAllNamedSymbols() (*NamedSymbolsRecord, error) { named = append(named, NamedSymbol{Symbol: symbol, Name: name}) } - return &NamedSymbolsRecord{Date: record.Date, Symbols: named}, nil + return &CurrenciesRecord{Date: record.Date, Data: named}, nil } func NormalizeBase(base string) string { @@ -119,51 +119,6 @@ func MakeSymbolsArray(raw string, base string) []string { return symbolsArray } -var Currencies = []string{ - "EUR", - "USD", - "JPY", - "BGN", - "CYP", - "CZK", - "DKK", - "EEK", - "GBP", - "HUF", - "LTL", - "LVL", - "MTL", - "PLN", - "ROL", - "RON", - "SEK", - "SIT", - "SKK", - "CHF", - "ISK", - "ILS", - "NOK", - "HRK", - "RUB", - "TRL", - "TRY", - "AUD", - "BRL", - "CAD", - "CNY", - "HKD", - "IDR", - "INR", - "KRW", - "MXN", - "MYR", - "NZD", - "PHP", - "SGD", - "THB", - "ZAR", -} - var CurrencyNames = map[string]string{ "EUR": "Euro", "USD": "US Dollar", @@ -208,3 +163,11 @@ var CurrencyNames = map[string]string{ "THB": "Thai Baht", "ZAR": "South African Rand", } + +var Currencies = func() []string { + keys := make([]string, 0, len(CurrencyNames)) + for k := range CurrencyNames { + keys = append(keys, k) + } + return keys +}() diff --git a/routers/rates.go b/routers/rates.go index a80a3a9..1787fbf 100644 --- a/routers/rates.go +++ b/routers/rates.go @@ -9,5 +9,5 @@ import ( func ratesGroup(mux *http.ServeMux) { mux.Handle("/v1/rates/latest", loggerMiddleware(http.HandlerFunc(handlers.GetLatest))) mux.Handle("/v1/rates/symbols", loggerMiddleware(http.HandlerFunc(handlers.GetSymbols))) - mux.Handle("/v1/rates/named-symbols", loggerMiddleware(http.HandlerFunc(handlers.GetNamedSymbols))) + mux.Handle("/v1/rates/currencies", loggerMiddleware(http.HandlerFunc(handlers.GetCurrencies))) } diff --git a/test/integration/helpers.go b/test/integration/helpers.go index 15a6d4d..b221e32 100644 --- a/test/integration/helpers.go +++ b/test/integration/helpers.go @@ -156,8 +156,8 @@ func (s *ServerProcess) GetSymbols() (*http.Response, error) { return http.Get(url) } -func (s *ServerProcess) GetNamedSymbols() (*http.Response, error) { - url := fmt.Sprintf("%s/v1/rates/named-symbols", s.baseURL) +func (s *ServerProcess) GetCurrencies() (*http.Response, error) { + url := fmt.Sprintf("%s/v1/rates/currencies", s.baseURL) return http.Get(url) } diff --git a/test/integration/integration_test.go b/test/integration/integration_test.go index c6355d3..16d7f32 100644 --- a/test/integration/integration_test.go +++ b/test/integration/integration_test.go @@ -27,9 +27,9 @@ type NamedSymbol struct { Name string `json:"name"` } -type NamedSymbolsRecord struct { - Date string `json:"date"` - Symbols []NamedSymbol `json:"symbols"` +type CurrenciesRecord struct { + Date string `json:"date"` + Data []NamedSymbol `json:"data"` } func TestGetLatestEndpoint(t *testing.T) { @@ -332,7 +332,7 @@ func TestGetSymbolsEndpoint(t *testing.T) { }) } -func TestGetNamedSymbolsEndpoint(t *testing.T) { +func TestGetCurrenciesEndpoint(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } @@ -348,7 +348,7 @@ func TestGetNamedSymbolsEndpoint(t *testing.T) { t.Fatalf("Failed to clear collection: %v", err) } - resp, err := tc.Server.GetNamedSymbols() + resp, err := tc.Server.GetCurrencies() if err != nil { t.Fatalf("Failed to make request: %v", err) } @@ -372,7 +372,7 @@ func TestGetNamedSymbolsEndpoint(t *testing.T) { t.Fatalf("Failed to seed data: %v", err) } - resp, err := tc.Server.GetNamedSymbols() + resp, err := tc.Server.GetCurrencies() if err != nil { t.Fatalf("Failed to make request: %v", err) } @@ -383,7 +383,7 @@ func TestGetNamedSymbolsEndpoint(t *testing.T) { t.Fatalf("Expected status 200, got %d: %s", resp.StatusCode, string(body)) } - var record NamedSymbolsRecord + var record CurrenciesRecord if err := json.NewDecoder(resp.Body).Decode(&record); err != nil { t.Fatalf("Failed to decode response: %v", err) } @@ -398,17 +398,17 @@ func TestGetNamedSymbolsEndpoint(t *testing.T) { {Symbol: "GBP", Name: "British Pound Sterling"}, } - if len(record.Symbols) != len(wantSymbols) { - t.Fatalf("Expected %d symbols, got %d: %v", len(wantSymbols), len(record.Symbols), record.Symbols) + if len(record.Data) != len(wantSymbols) { + t.Fatalf("Expected %d symbols, got %d: %v", len(wantSymbols), len(record.Data), record.Data) } for i, want := range wantSymbols { - got := record.Symbols[i] + got := record.Data[i] if got.Symbol != want.Symbol { - t.Errorf("symbols[%d].symbol = %q, want %q", i, got.Symbol, want.Symbol) + t.Errorf("data[%d].symbol = %q, want %q", i, got.Symbol, want.Symbol) } if got.Name != want.Name { - t.Errorf("symbols[%d].name = %q, want %q", i, got.Name, want.Name) + t.Errorf("data[%d].name = %q, want %q", i, got.Name, want.Name) } } }) @@ -434,7 +434,7 @@ func TestGetNamedSymbolsEndpoint(t *testing.T) { t.Fatalf("Failed to seed latest data: %v", err) } - resp, err := tc.Server.GetNamedSymbols() + resp, err := tc.Server.GetCurrencies() if err != nil { t.Fatalf("Failed to make request: %v", err) } @@ -445,7 +445,7 @@ func TestGetNamedSymbolsEndpoint(t *testing.T) { t.Fatalf("Expected status 200, got %d: %s", resp.StatusCode, string(body)) } - var record NamedSymbolsRecord + var record CurrenciesRecord if err := json.NewDecoder(resp.Body).Decode(&record); err != nil { t.Fatalf("Failed to decode response: %v", err) } @@ -454,8 +454,8 @@ func TestGetNamedSymbolsEndpoint(t *testing.T) { t.Errorf("Expected date %q, got %q", "2025-11-21", record.Date) } - if len(record.Symbols) != 3 { - t.Errorf("Expected 3 symbols from latest entry, got %d: %v", len(record.Symbols), record.Symbols) + if len(record.Data) != 3 { + t.Errorf("Expected 3 symbols from latest entry, got %d: %v", len(record.Data), record.Data) } }) @@ -472,7 +472,7 @@ func TestGetNamedSymbolsEndpoint(t *testing.T) { t.Fatalf("Failed to seed data: %v", err) } - resp, err := tc.Server.GetNamedSymbols() + resp, err := tc.Server.GetCurrencies() if err != nil { t.Fatalf("Failed to make request: %v", err) } From 4772203ba42e736a8d8205fe1604bbb203d3058d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 12:55:12 +0000 Subject: [PATCH 4/5] Move currencies endpoint to /v1/currencies in its own route group Co-authored-by: kamaal111 <37084924+kamaal111@users.noreply.github.com> --- README.md | 4 ++-- routers/currencies.go | 11 +++++++++++ routers/rates.go | 1 - routers/routers.go | 1 + test/integration/helpers.go | 2 +- 5 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 routers/currencies.go diff --git a/README.md b/README.md index b3769ac..9bc9a90 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ curl "http://localhost:8000/v1/rates/symbols" ### Get All Available Currencies with Names ``` -GET /v1/rates/currencies +GET /v1/currencies ``` Returns the latest available currencies from the database, each with a human-readable name. Returns `404` if no symbols data has been stored yet. @@ -121,7 +121,7 @@ Returns the latest available currencies from the database, each with a human-rea #### Example Request ```bash -curl "http://localhost:8000/v1/rates/currencies" +curl "http://localhost:8000/v1/currencies" ``` #### Example Response diff --git a/routers/currencies.go b/routers/currencies.go new file mode 100644 index 0000000..13bc4a4 --- /dev/null +++ b/routers/currencies.go @@ -0,0 +1,11 @@ +package routers + +import ( + "net/http" + + "github.com/kamaal111/forex-api/handlers" +) + +func currenciesGroup(mux *http.ServeMux) { + mux.Handle("/v1/currencies", loggerMiddleware(http.HandlerFunc(handlers.GetCurrencies))) +} diff --git a/routers/rates.go b/routers/rates.go index 1787fbf..425ae7e 100644 --- a/routers/rates.go +++ b/routers/rates.go @@ -9,5 +9,4 @@ import ( func ratesGroup(mux *http.ServeMux) { mux.Handle("/v1/rates/latest", loggerMiddleware(http.HandlerFunc(handlers.GetLatest))) mux.Handle("/v1/rates/symbols", loggerMiddleware(http.HandlerFunc(handlers.GetSymbols))) - mux.Handle("/v1/rates/currencies", loggerMiddleware(http.HandlerFunc(handlers.GetCurrencies))) } diff --git a/routers/routers.go b/routers/routers.go index a499235..fc69733 100644 --- a/routers/routers.go +++ b/routers/routers.go @@ -17,6 +17,7 @@ func Start() { mux := http.NewServeMux() ratesGroup(mux) + currenciesGroup(mux) mux.Handle("/", loggerMiddleware(http.HandlerFunc(notFound))) log.Printf("Listening on %s...", serverAddress) diff --git a/test/integration/helpers.go b/test/integration/helpers.go index b221e32..46b18e4 100644 --- a/test/integration/helpers.go +++ b/test/integration/helpers.go @@ -157,7 +157,7 @@ func (s *ServerProcess) GetSymbols() (*http.Response, error) { } func (s *ServerProcess) GetCurrencies() (*http.Response, error) { - url := fmt.Sprintf("%s/v1/rates/currencies", s.baseURL) + url := fmt.Sprintf("%s/v1/currencies", s.baseURL) return http.Get(url) } From 9aac4dbd2e494af11d6dec79375f91c769711fd1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 01:51:41 +0000 Subject: [PATCH 5/5] Define route path constants in handlers package; use them in routers and tests Co-authored-by: kamaal111 <37084924+kamaal111@users.noreply.github.com> --- handlers/named_symbols_test.go | 2 +- handlers/rates_test.go | 4 ++-- handlers/routes.go | 7 +++++++ handlers/symbols_test.go | 2 +- routers/currencies.go | 2 +- routers/rates.go | 4 ++-- 6 files changed, 14 insertions(+), 7 deletions(-) create mode 100644 handlers/routes.go diff --git a/handlers/named_symbols_test.go b/handlers/named_symbols_test.go index 084235b..f96394d 100644 --- a/handlers/named_symbols_test.go +++ b/handlers/named_symbols_test.go @@ -80,7 +80,7 @@ func TestGetCurrenciesHandler(t *testing.T) { handler := TestableCurrenciesHandler(mockRepo) - req := httptest.NewRequest(http.MethodGet, "/v1/rates/currencies", nil) + req := httptest.NewRequest(http.MethodGet, CurrenciesPath, nil) recorder := httptest.NewRecorder() handler(recorder, req) diff --git a/handlers/rates_test.go b/handlers/rates_test.go index 232ef1a..1ec1f6f 100644 --- a/handlers/rates_test.go +++ b/handlers/rates_test.go @@ -111,7 +111,7 @@ func TestGetLatestHandler(t *testing.T) { handler := TestableHandler(mockRepo) - req := httptest.NewRequest(http.MethodGet, "/v1/rates/latest"+tt.queryParams, nil) + req := httptest.NewRequest(http.MethodGet, LatestPath+tt.queryParams, nil) recorder := httptest.NewRecorder() handler(recorder, req) @@ -192,7 +192,7 @@ func TestGetLatestHandler_SymbolsFiltering(t *testing.T) { handler := TestableHandler(mockRepo) - req := httptest.NewRequest(http.MethodGet, "/v1/rates/latest?symbols="+tt.symbols, nil) + req := httptest.NewRequest(http.MethodGet, LatestPath+"?symbols="+tt.symbols, nil) recorder := httptest.NewRecorder() handler(recorder, req) diff --git a/handlers/routes.go b/handlers/routes.go new file mode 100644 index 0000000..366366e --- /dev/null +++ b/handlers/routes.go @@ -0,0 +1,7 @@ +package handlers + +const ( + LatestPath = "/v1/rates/latest" + SymbolsPath = "/v1/rates/symbols" + CurrenciesPath = "/v1/currencies" +) diff --git a/handlers/symbols_test.go b/handlers/symbols_test.go index 95cf43c..6a5b927 100644 --- a/handlers/symbols_test.go +++ b/handlers/symbols_test.go @@ -76,7 +76,7 @@ func TestGetSymbolsHandler(t *testing.T) { handler := TestableSymbolsHandler(mockRepo) - req := httptest.NewRequest(http.MethodGet, "/v1/rates/symbols", nil) + req := httptest.NewRequest(http.MethodGet, SymbolsPath, nil) recorder := httptest.NewRecorder() handler(recorder, req) diff --git a/routers/currencies.go b/routers/currencies.go index 13bc4a4..bad5f33 100644 --- a/routers/currencies.go +++ b/routers/currencies.go @@ -7,5 +7,5 @@ import ( ) func currenciesGroup(mux *http.ServeMux) { - mux.Handle("/v1/currencies", loggerMiddleware(http.HandlerFunc(handlers.GetCurrencies))) + mux.Handle(handlers.CurrenciesPath, loggerMiddleware(http.HandlerFunc(handlers.GetCurrencies))) } diff --git a/routers/rates.go b/routers/rates.go index 425ae7e..e39501e 100644 --- a/routers/rates.go +++ b/routers/rates.go @@ -7,6 +7,6 @@ import ( ) func ratesGroup(mux *http.ServeMux) { - mux.Handle("/v1/rates/latest", loggerMiddleware(http.HandlerFunc(handlers.GetLatest))) - mux.Handle("/v1/rates/symbols", loggerMiddleware(http.HandlerFunc(handlers.GetSymbols))) + mux.Handle(handlers.LatestPath, loggerMiddleware(http.HandlerFunc(handlers.GetLatest))) + mux.Handle(handlers.SymbolsPath, loggerMiddleware(http.HandlerFunc(handlers.GetSymbols))) }