From 89297482143ad81be1571f2f1a12d0097914a4f3 Mon Sep 17 00:00:00 2001 From: Christopher Hunter <8398225+crhntr@users.noreply.github.com> Date: Mon, 18 Dec 2023 00:06:52 -0800 Subject: [PATCH] fix fake /api/returns endpoint --- api.go | 18 ++++++++ api_test.go | 24 ++++++++++ portfolio_test.go | 115 ++-------------------------------------------- 3 files changed, 46 insertions(+), 111 deletions(-) diff --git a/api.go b/api.go index 21a6432..eb08b5b 100644 --- a/api.go +++ b/api.go @@ -11,6 +11,8 @@ import ( "os" "strings" + "go.mongodb.org/mongo-driver/bson/primitive" + "github.com/portfoliotree/portfolio/returns" ) @@ -57,6 +59,22 @@ func (pf *Specification) AssetReturns(ctx context.Context) (returns.Table, error return doJSONRequest[returns.Table](http.DefaultClient.Do, req) } +func ParseComponentsFromURL(values url.Values, prefix string) ([]Component, error) { + assetValues, ok := values[prefix+"-id"] + if !ok { + return nil, errors.New("use asset-id parameters to specify asset returns") + } + components := make([]Component, 0, len(assetValues)) + for _, v := range assetValues { + if _, err := primitive.ObjectIDFromHex(v); err == nil { + components = append(components, Component{Type: "Portfolio", ID: v}) + continue + } + components = append(components, Component{Type: "Security", ID: v}) + } + return components, nil +} + func doJSONRequest[T any](do func(r *http.Request) (*http.Response, error), req *http.Request) (T, error) { var result T req.Header.Set("accept", "application/json") diff --git a/api_test.go b/api_test.go index 873e1fa..42136dd 100644 --- a/api_test.go +++ b/api_test.go @@ -2,6 +2,7 @@ package portfolio_test import ( "context" + "os" "testing" "github.com/stretchr/testify/assert" @@ -9,6 +10,29 @@ import ( "github.com/portfoliotree/portfolio" ) +func Test_APIEndpoints(t *testing.T) { + if value, found := os.LookupEnv("CI"); !found || value != "true" { + t.Skip("Skipping test in CI environment") + } + + t.Run("returns", func(t *testing.T) { + pf := portfolio.Specification{ + Assets: []portfolio.Component{ + {ID: "AAPL"}, + {ID: "GOOG"}, + }, + } + table, err := pf.AssetReturns(context.Background()) + assert.NoError(t, err) + if table.NumberOfColumns() != 2 { + t.Errorf("Expected 2 columns, got %d", table.NumberOfColumns()) + } + if table.NumberOfRows() < 10 { + t.Errorf("Expected at least 10 rows, got %d", table.NumberOfRows()) + } + }) +} + func TestSpecification_AssetReturns(t *testing.T) { for _, tt := range []struct { Name string diff --git a/portfolio_test.go b/portfolio_test.go index 51ab335..8e894fc 100644 --- a/portfolio_test.go +++ b/portfolio_test.go @@ -6,7 +6,6 @@ import ( "fmt" "net/http" "net/http/httptest" - "net/url" "os" "path/filepath" "testing" @@ -32,12 +31,12 @@ func TestMain(m *testing.M) { func testdataAssetReturns(crp portfolio.ComponentReturnsProvider) http.HandlerFunc { return func(res http.ResponseWriter, req *http.Request) { - var pf portfolio.Specification - if err := pf.ParseValues(req.URL.Query()); err != nil { - http.Error(res, err.Error(), http.StatusInternalServerError) + assets, err := portfolio.ParseComponentsFromURL(req.URL.Query(), "asset") + if err != nil { + http.Error(res, err.Error(), http.StatusBadRequest) return } - table, err := crp.ComponentReturnsTable(req.Context(), pf.Assets...) + table, err := crp.ComponentReturnsTable(req.Context(), assets...) if err != nil { http.Error(res, err.Error(), http.StatusInternalServerError) return @@ -294,112 +293,6 @@ func Test_Portfolio_Validate(t *testing.T) { } } -func Test_Portfolio_ParseValues(t *testing.T) { - for _, tt := range []struct { - Name string - Values url.Values - In, Out portfolio.Specification - ExpectErr bool - }{ - { - Name: "set everything", - Values: url.Values{ - "name": []string{"X"}, - "asset-id": []string{"y", "z"}, - "benchmark-id": []string{"b"}, - "filepath": []string{"f"}, - "policy-weight": []string{".5", ".5"}, - "policy-rebalance": []string{"Daily"}, - "policy-weights-algorithm": []string{"Static"}, - "policy-update-weights": []string{"Daily"}, - "policy-weight-algorithm-look-back": []string{"1 Week"}, - }, - Out: portfolio.Specification{ - Name: "X", - Assets: []portfolio.Component{ - {ID: "y"}, - {ID: "z"}, - }, - Benchmark: portfolio.Component{ - ID: "b", - }, - Filepath: "f", - Policy: portfolio.Policy{ - RebalancingInterval: "Daily", - WeightsAlgorithm: "Static", - Weights: []float64{0.5, 0.5}, - WeightsUpdatingInterval: "Daily", - WeightsAlgorithmLookBack: "1 Week", - }, - }, - }, - { - Name: "empty values do not override", - Values: url.Values{}, - In: portfolio.Specification{ - Name: "no change", - Benchmark: portfolio.Component{ID: "b"}, - Assets: []portfolio.Component{{ID: "a1"}}, - Filepath: "f", - }, - Out: portfolio.Specification{ - Name: "no change", - Benchmark: portfolio.Component{ID: "b"}, - Assets: []portfolio.Component{{ID: "a1"}}, - Filepath: "f", - }, - }, - } { - t.Run(tt.Name, func(t *testing.T) { - pf := &tt.In - err := pf.ParseValues(tt.Values) - if tt.ExpectErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - } - assert.Equal(t, tt.Out, *pf) - }) - } -} - -func Test_Portfolio_Values(t *testing.T) { - t.Run("encode and decode", func(t *testing.T) { - pf := portfolio.Specification{ - Name: "X", - Assets: []portfolio.Component{ - {ID: "y"}, - {ID: "z"}, - }, - Benchmark: portfolio.Component{ - ID: "b", - }, - Filepath: "f", - Policy: portfolio.Policy{ - RebalancingInterval: "Daily", - WeightsAlgorithm: "Static", - Weights: []float64{0.5, 0.5}, - WeightsUpdatingInterval: "Daily", - WeightsAlgorithmLookBack: "1 Week", - }, - } - - var update portfolio.Specification - e := pf.Values().Encode() - q, err := url.ParseQuery(e) - require.NoError(t, err) - assert.NoError(t, update.ParseValues(q)) - assert.Equal(t, pf, update) - }) - - t.Run("fail to parse float", func(t *testing.T) { - values, err := url.ParseQuery(`policy-weight=x`) - require.NoError(t, err) - var pf portfolio.Specification - assert.Error(t, pf.ParseValues(values)) - }) -} - func TestPortfolio_RemoveAsset(t *testing.T) { t.Run("nil", func(t *testing.T) { var zero portfolio.Specification