Repository: cycloidio/terracost
Pull request #52 (2021-07-14 21:43:20)

Correct planned cost calculation for resource diff
Fixed issue #51 where prior component cost was used to calculate planned cost for resource diff. Also added tests to cover prior and planned cost calculation for resource diff.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c0d7652..0b47ea5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
 ## [Unreleased]
 
+## [0.4.1] _2021-07-15_
+
+### Fixed
+
+- Correct calculation for planned cost in resource diff
+  ([Issue #51](https://github.com/cycloidio/terracost/issues/51))
+
 ## [0.4.0] _2021-07-13_
 
 ### Added
diff --git a/cost/cost.go b/cost/cost.go
index 8657d32..e4d2de8 100644
--- a/cost/cost.go
+++ b/cost/cost.go
@@ -44,6 +44,11 @@ func (c Cost) Hourly() decimal.Decimal {
 // Add adds the values of two Cost structs.
 // If the currency of both costs doesn't match, error is returned.
 func (c Cost) Add(c2 Cost) (Cost, error) {
+	// if cost addition iz Zero, ignore it
+	if c2 == Zero {
+		return c, nil
+	}
+
 	// If there is no currency, use the currency of the addition
 	if c.Currency == "" {
 		c.Currency = c2.Currency
diff --git a/cost/resource.go b/cost/resource.go
index 7d5065f..f0dd7a1 100644
--- a/cost/resource.go
+++ b/cost/resource.go
@@ -42,7 +42,7 @@ func (rd ResourceDiff) PriorCost() (Cost, error) {
 	for _, cd := range rd.ComponentDiffs {
 		total, err = total.Add(cd.PriorCost())
 		if err != nil {
-			return Zero, fmt.Errorf("failed calculating prior cost : %w", err)
+			return Zero, fmt.Errorf("failed calculating prior cost: %w", err)
 		}
 	}
 	return total, nil
@@ -54,7 +54,7 @@ func (rd ResourceDiff) PlannedCost() (Cost, error) {
 	total := Zero
 	var err error
 	for _, cd := range rd.ComponentDiffs {
-		total, err = total.Add(cd.PriorCost())
+		total, err = total.Add(cd.PlannedCost())
 		if err != nil {
 			return Zero, fmt.Errorf("failed calculating planned cost: %w", err)
 		}


Test patch:
diff --git a/cost/resource_test.go b/cost/resource_test.go
index cd26d51..00d7be2 100644
--- a/cost/resource_test.go
+++ b/cost/resource_test.go
@@ -1,9 +1,12 @@
 package cost_test
 
 import (
+	"fmt"
 	"testing"
 
+	"github.com/shopspring/decimal"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 
 	"github.com/cycloidio/terracost/cost"
 )
@@ -103,3 +106,130 @@ func TestResourceDiff_Valid(t *testing.T) {
 		})
 	}
 }
+
+func TestResourceDiff_Cost(t *testing.T) {
+	t.Run("Success", func(t *testing.T) {
+		rd := &cost.ResourceDiff{
+			ComponentDiffs: map[string]*cost.ComponentDiff{
+				"comp1": {
+					Prior: &cost.Component{
+						Rate:     cost.NewMonthly(decimal.NewFromFloat(104.6), "USD"),
+						Quantity: decimal.NewFromInt(1),
+					},
+					Planned: &cost.Component{
+						Rate:     cost.NewMonthly(decimal.NewFromFloat(208.8), "USD"),
+						Quantity: decimal.NewFromInt(1),
+					},
+				},
+				"comp2": {
+					Prior: &cost.Component{
+						Rate:     cost.NewMonthly(decimal.NewFromFloat(10.5), "USD"),
+						Quantity: decimal.NewFromInt(2),
+					},
+					Planned: &cost.Component{
+						Rate:     cost.NewMonthly(decimal.NewFromFloat(20.7), "USD"),
+						Quantity: decimal.NewFromInt(2),
+					},
+				},
+			},
+		}
+
+		prior, err := rd.PriorCost()
+		require.NoError(t, err)
+		assertDecimalEqual(t, decimal.NewFromFloat(125.6), prior.Monthly())
+		assert.Equal(t, "USD", prior.Currency)
+
+		planned, err := rd.PlannedCost()
+		require.NoError(t, err)
+		assertDecimalEqual(t, decimal.NewFromFloat(250.2), planned.Decimal)
+		assert.Equal(t, "USD", planned.Currency)
+	})
+	t.Run("PriorWithError", func(t *testing.T) {
+		rd := &cost.ResourceDiff{
+			ComponentDiffs: map[string]*cost.ComponentDiff{
+				"comp1": {
+					Prior: &cost.Component{
+						Rate:     cost.NewMonthly(decimal.NewFromFloat(104.6), "USD"),
+						Quantity: decimal.NewFromInt(1),
+					},
+				},
+				"comp2": {
+					Prior: &cost.Component{
+						Error: fmt.Errorf("prior error"),
+					},
+				},
+			},
+		}
+
+		prior, err := rd.PriorCost()
+		require.NoError(t, err)
+		assertDecimalEqual(t, decimal.NewFromFloat(104.6), prior.Monthly())
+		assert.Equal(t, "USD", prior.Currency)
+	})
+	t.Run("PlannedWithError", func(t *testing.T) {
+		rd := &cost.ResourceDiff{
+			ComponentDiffs: map[string]*cost.ComponentDiff{
+				"comp1": {
+					Planned: &cost.Component{
+						Rate:     cost.NewMonthly(decimal.NewFromFloat(208.8), "USD"),
+						Quantity: decimal.NewFromInt(1),
+					},
+				},
+				"comp2": {
+					Planned: &cost.Component{
+						Error: fmt.Errorf("planned error"),
+					},
+				},
+			},
+		}
+
+		planned, err := rd.PlannedCost()
+		require.NoError(t, err)
+		assertDecimalEqual(t, decimal.NewFromFloat(208.8), planned.Decimal)
+		assert.Equal(t, "USD", planned.Currency)
+	})
+	t.Run("PriorCurrencyMismatch", func(t *testing.T) {
+		rd := &cost.ResourceDiff{
+			ComponentDiffs: map[string]*cost.ComponentDiff{
+				"comp1": {
+					Prior: &cost.Component{
+						Rate:     cost.NewMonthly(decimal.NewFromFloat(104.6), "USD"),
+						Quantity: decimal.NewFromInt(1),
+					},
+				},
+				"comp2": {
+					Prior: &cost.Component{
+						Rate:     cost.NewMonthly(decimal.NewFromFloat(10.5), "EUR"),
+						Quantity: decimal.NewFromInt(2),
+					},
+				},
+			},
+		}
+
+		_, err := rd.PriorCost()
+		require.Error(t, err)
+		assert.Equal(t, "failed calculating prior cost: currency mismatch: expected USD, got EUR", err.Error())
+	})
+	t.Run("PlannedCurrencyMismatch", func(t *testing.T) {
+		rd := &cost.ResourceDiff{
+			ComponentDiffs: map[string]*cost.ComponentDiff{
+				"comp1": {
+					Planned: &cost.Component{
+						Rate:     cost.NewMonthly(decimal.NewFromFloat(208.8), "USD"),
+						Quantity: decimal.NewFromInt(1),
+					},
+				},
+				"comp2": {
+					Planned: &cost.Component{
+						Rate:     cost.NewMonthly(decimal.NewFromFloat(20.7), "EUR"),
+						Quantity: decimal.NewFromInt(2),
+					},
+				},
+			},
+		}
+
+		_, err := rd.PlannedCost()
+		require.Error(t, err)
+		assert.Equal(t, "failed calculating planned cost: currency mismatch: expected USD, got EUR", err.Error())
+	})
+}
diff --git a/e2e/aws_estimation_test.go b/e2e/aws_estimation_test.go
index 75122cc..6d37e4b 100644
--- a/e2e/aws_estimation_test.go
+++ b/e2e/aws_estimation_test.go
@@ -161,11 +161,27 @@ func TestAWSEstimation(t *testing.T) {
 				assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(3.6), "USD"), compute.PriorCost())
 				assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(3.6), "USD"), compute.PlannedCost())
 
+				priorCost, err := diff.PriorCost()
+				require.NoError(t, err)
+				assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(91.2), "USD"), priorCost)
+
+				plannedCost, err := diff.PlannedCost()
+				require.NoError(t, err)
+				assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(901.5), "USD"), plannedCost)
+
 			case "aws_lb.example":
 				lb := diff.ComponentDiffs["Application Load Balancer"]
 				require.NotNil(t, lb)
 				assert.False(t, diff.Valid())
 				assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(0), ""), lb.Planned.Cost())
+
+				priorCost, err := diff.PriorCost()
+				require.NoError(t, err)
+				assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(0), ""), priorCost)
+
+				plannedCost, err := diff.PlannedCost()
+				require.NoError(t, err)
+				assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(0), ""), plannedCost)
 			}
 		}
 	})