Skip to content

Commit

Permalink
Add test coverage (#78)
Browse files Browse the repository at this point in the history
* add test cases

* add test cases

* remove fmt stmts

* fix flaky tests and add more test cases
kumdeepakgit authored Oct 14, 2024

Verified

This commit was signed with the committer’s verified signature.
1 parent 789555f commit 3483af4
Showing 15 changed files with 904 additions and 4 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -7,3 +7,7 @@ tf-summarize
*.swp

example/tfplan

cover.out
cover.html
*.cov
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ require (
require (
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
25 changes: 25 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -5,6 +5,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
@@ -28,13 +30,36 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/zclconf/go-cty v1.14.1 h1:t9fyA35fwjjUMcmL5hLER+e/rEPqrbCK1/OSE4SI9KA=
github.com/zclconf/go-cty v1.14.1/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
47 changes: 47 additions & 0 deletions parser/json-parser_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package parser

import (
"encoding/json"
"testing"

tfjson "github.com/hashicorp/terraform-json"
"github.com/stretchr/testify/assert"
)

func TestJSONParser_Parse_Success(t *testing.T) {
mockPlan := tfjson.Plan{
FormatVersion: "0.1",
TerraformVersion: "1.0.0",
Variables: map[string]*tfjson.PlanVariable{"example": {}},
}
data, err := json.Marshal(mockPlan)
assert.NoError(t, err)

parser := NewJSONParser(data)
parsedPlan, err := parser.Parse()

assert.NoError(t, err)
assert.Equal(t, mockPlan.FormatVersion, parsedPlan.FormatVersion)
assert.Equal(t, mockPlan.TerraformVersion, parsedPlan.TerraformVersion)
assert.Equal(t, mockPlan.Variables, parsedPlan.Variables)
}

func TestJSONParser_Parse_InvalidJSON(t *testing.T) {
// invalid JSON data
invalidData := []byte(`{"invalid-json"}`)

parser := NewJSONParser(invalidData)
_, err := parser.Parse()

assert.Error(t, err)
assert.Contains(t, err.Error(), "error when parsing input")
}

func TestNewJSONParser(t *testing.T) {
data := []byte(`{"plan": "mock"}`)

parser := NewJSONParser(data)
jp, ok := parser.(JSONParser)
assert.True(t, ok, "expected a JSONParser instance")
assert.Equal(t, data, jp.data, "expected data to match the input")
}
60 changes: 60 additions & 0 deletions parser/parser_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package parser

import (
"testing"

"github.com/dineshba/tf-summarize/reader"
"github.com/stretchr/testify/assert"
)

// Test for JSON file input
func TestCreateParser_JSONFile(t *testing.T) {
data := []byte(`{"plan": "mock"}`)
fileName := "example.json"

p, err := CreateParser(data, fileName)

assert.NoError(t, err)

_, ok := p.(JSONParser)
assert.True(t, ok, "expected a JSON parser to be returned")
}

// Test for stdin input
func TestCreateParser_Stdin(t *testing.T) {
data := []byte(`{"plan": "mock"}`)
fileName := reader.StdinFileName

p, err := CreateParser(data, fileName)

assert.NoError(t, err)

_, ok := p.(JSONParser)
assert.True(t, ok, "expected a JSON parser for stdin input")
}

// Test for binary file input
func TestCreateParser_BinaryFile(t *testing.T) {
data := []byte{0x00, 0x01, 0x02} // Mock binary data
fileName := "example.binary"

p, err := CreateParser(data, fileName)

assert.NoError(t, err)

_, ok := p.(BinaryParser)
assert.True(t, ok, "expected a Binary parser to be returned")
}

// Test for non-JSON file name (like .txt or other extensions)
func TestCreateParser_InvalidFileName(t *testing.T) {
data := []byte(`irrelevant data`)
fileName := "example.txt"

p, err := CreateParser(data, fileName)

assert.NoError(t, err)

_, ok := p.(BinaryParser)
assert.True(t, ok, "expected a Binary parser for non-JSON file extension")
}
49 changes: 49 additions & 0 deletions testdata/mocks/mock_io_writer.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions writer/html.go
Original file line number Diff line number Diff line change
@@ -14,11 +14,13 @@ type HTMLWriter struct {
OutputChanges map[string][]string
}

var cfs = getFS()

// Write outputs the HTML summary to the io.Writer it's passed.
func (t HTMLWriter) Write(writer io.Writer) error {
templatesDir := "templates"
rcTmpl := "resourceChanges.html"
tmpl, err := template.New(rcTmpl).ParseFS(templates, path.Join(templatesDir, rcTmpl))
tmpl, err := template.New(rcTmpl).ParseFS(cfs, path.Join(templatesDir, rcTmpl))
if err != nil {
return err
}
@@ -33,7 +35,7 @@ func (t HTMLWriter) Write(writer io.Writer) error {
}

ocTmpl := "outputChanges.html"
outputTmpl, err := template.New(ocTmpl).ParseFS(templates, path.Join(templatesDir, ocTmpl))
outputTmpl, err := template.New(ocTmpl).ParseFS(cfs, path.Join(templatesDir, ocTmpl))
if err != nil {
return err
}
112 changes: 112 additions & 0 deletions writer/html_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package writer

import (
"bytes"
"testing"
"testing/fstest"

"github.com/dineshba/tf-summarize/terraformstate"
. "github.com/hashicorp/terraform-json"
)

func TestHTMLWriterWithMockFileSystem(t *testing.T) {
origFS := cfs
cfs = fstest.MapFS{
"templates/resourceChanges.html": &fstest.MapFile{
Data: []byte(`<table>
<tr>
<th>CHANGE</th>
<th>RESOURCE</th>
</tr>{{ range $change, $resources := .ResourceChanges }}{{ $length := len $resources }}{{ if gt $length 0 }}
<tr>
<td>{{ $change }}</td>
<td>
<ul>{{ range $i, $r := $resources }}
<li><code>{{ $r.Address }}</code></li>{{ end }}
</ul>
</td>
</tr>{{ end }}{{ end }}
</table>
`),
},
"templates/outputChanges.html": &fstest.MapFile{
Data: []byte(`<table>
<tr>
<th>CHANGE</th>
<th>OUTPUT</th>
</tr>{{ range $change, $outputs := .OutputChanges }}{{ $length := len $outputs }}{{ if gt $length 0 }}
<tr>
<td>{{ $change }}</td>
<td>
<ul>{{ range $i, $o := $outputs }}
<li><code>{{ $o }}</code></li>{{ end }}
</ul>
</td>
</tr>{{ end }}{{ end }}
</table>
`),
},
}
t.Cleanup(func() {
cfs = origFS
})

resourceChanges := map[string]terraformstate.ResourceChanges{
"module.test": {
{
Address: "aws_instance.example",
Name: "example",
Change: &Change{
Before: map[string]interface{}{"name": "old_instance"},
After: map[string]interface{}{"name": "new_instance"},
Actions: Actions{ActionCreate},
},
},
},
}
outputChanges := map[string][]string{
"output_key": {"output_value"},
}

htmlWriter := NewHTMLWriter(resourceChanges, outputChanges)
var buf bytes.Buffer

err := htmlWriter.Write(&buf)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}

expectedOutput := `<table>
<tr>
<th>CHANGE</th>
<th>RESOURCE</th>
</tr>
<tr>
<td>module.test</td>
<td>
<ul>
<li><code>aws_instance.example</code></li>
</ul>
</td>
</tr>
</table>
<table>
<tr>
<th>CHANGE</th>
<th>OUTPUT</th>
</tr>
<tr>
<td>output_key</td>
<td>
<ul>
<li><code>output_value</code></li>
</ul>
</td>
</tr>
</table>
`
if buf.String() != expectedOutput {
t.Errorf("expected %q, got %q", expectedOutput, buf.String())
}

}
129 changes: 129 additions & 0 deletions writer/json_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package writer

import (
"bytes"
"encoding/json"
"testing"

"github.com/dineshba/tf-summarize/terraformstate"
. "github.com/hashicorp/terraform-json"
"github.com/nsf/jsondiff"
)

// Struct to hold arguments and expected output for each test case
type testCase struct {
args terraformstate.ResourceChanges
expectedOutput map[string]interface{}
}

func TestJSONWriter(t *testing.T) {

// Define test cases
testCases := []testCase{
{
args: terraformstate.ResourceChanges{
{
Address: "module.test.azapi_resource.logical_network",
Type: "aws_instance",
Name: "example",
Change: &Change{
Before: map[string]interface{}{"name": "old_instance"},
After: map[string]interface{}{"name": "new_instance"},
Actions: Actions{ActionCreate},
},
},
},
expectedOutput: map[string]interface{}{
"module": map[string]interface{}{
"test": map[string]interface{}{
"azapi_resource": map[string]interface{}{
"logical_network": map[string]interface{}{
"(+)": map[string]interface{}{
"name": "new_instance",
},
},
},
},
},
},
},
{
args: terraformstate.ResourceChanges{
{
Address: "module.test.aws_s3_bucket.example",
Type: "aws_s3_bucket",
Name: "example",
Change: &Change{
Before: map[string]interface{}{"name": "old_bucket"},
After: map[string]interface{}{"name": "new_bucket"},
Actions: Actions{ActionUpdate},
},
},
},
expectedOutput: map[string]interface{}{
"module": map[string]interface{}{
"test": map[string]interface{}{
"aws_s3_bucket": map[string]interface{}{
"example": map[string]interface{}{
"(~)": map[string]interface{}{
"name": map[string]interface{}{
"changed": []string{
"old_bucket",
"new_bucket",
},
},
},
},
},
},
},
},
},
{
args: terraformstate.ResourceChanges{
{
Address: "module.test.aws_security_group.example",
Type: "aws_security_group",
Name: "example",
Change: &Change{
Before: map[string]interface{}{"name": "old_sg"},
After: map[string]interface{}{"name": "new_sg"},
Actions: Actions{ActionDelete},
},
},
},
expectedOutput: map[string]interface{}{
"module": map[string]interface{}{
"test": map[string]interface{}{
"aws_security_group": map[string]interface{}{
"example": map[string]interface{}{
"(-)": map[string]interface{}{
"name": "old_sg",
},
},
},
},
},
},
},
}

// Iterate through test cases
for i, tc := range testCases {
jsonWriter := NewJSONWriter(tc.args)
var buf bytes.Buffer
err := jsonWriter.Write(&buf)
if err != nil {
t.Fatalf("Unexpected error in test case %d: %v", i+1, err)
}
expectedJSON, err := json.Marshal(tc.expectedOutput)
if err != nil {
t.Fatalf("Error marshalling expected output in test case %d: %v", i+1, err)
}
opts := jsondiff.DefaultJSONOptions()
diff, str := jsondiff.Compare(expectedJSON, buf.Bytes(), &opts)
if diff != jsondiff.FullMatch {
t.Errorf("Output mismatch in test case %d: %s", i+1, str)
}
}
}
4 changes: 3 additions & 1 deletion writer/separate_tree.go
Original file line number Diff line number Diff line change
@@ -15,6 +15,8 @@ type SeparateTree struct {
drawable bool
}

var NewTreeWriterFunc = NewTreeWriter

func (s SeparateTree) Write(writer io.Writer) error {
var err error
for k, v := range s.changes {
@@ -23,7 +25,7 @@ func (s SeparateTree) Write(writer io.Writer) error {
if err != nil {
return fmt.Errorf("error writing to %s: %s", writer, err)
}
treeWriter := NewTreeWriter(v, s.drawable)
treeWriter := NewTreeWriterFunc(v, s.drawable)
err = treeWriter.Write(writer)
if err != nil {
return fmt.Errorf("error writing to %s: %s", writer, err)
168 changes: 168 additions & 0 deletions writer/separate_tree_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package writer

import (
"bytes"
"errors"
"io"
"testing"

"github.com/dineshba/tf-summarize/terraformstate"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"

"github.com/dineshba/tf-summarize/testdata/mocks"

. "github.com/hashicorp/terraform-json"
tfjson "github.com/hashicorp/terraform-json"
)

// Mock tree writer
type mockTreeWriter struct{}

func (m *mockTreeWriter) Write(writer io.Writer) error {
return nil
}

func NewMockTreeWriter(changes []*tfjson.ResourceChange, drawable bool) Writer {
return &mockTreeWriter{}
}

// Mock tree writer to simulate an error during treeWriter.Write
type mockTreeWriterWithError struct{}

func (m *mockTreeWriterWithError) Write(writer io.Writer) error {
return errors.New("tree writer error")
}

func NewMockTreeWriterWithError(changes []*tfjson.ResourceChange, drawable bool) Writer {
return &mockTreeWriterWithError{}
}

// Helper function to create changes
func createMockChanges() map[string]terraformstate.ResourceChanges {
return map[string]terraformstate.ResourceChanges{
"add": {
{
Address: "aws_instance.example1",
Change: &Change{Actions: Actions{ActionCreate}},
},
},
"delete": {
{
Address: "aws_instance.example2",
Change: &Change{Actions: Actions{ActionDelete}},
},
},
}
}

func TestSeparateTree_Write(t *testing.T) {
mockChanges := createMockChanges()

t.Run("Drawable True", func(t *testing.T) {
tw := NewSeparateTree(mockChanges, true)
var buf bytes.Buffer
err := tw.Write(&buf)

assert.NoError(t, err)

expectedAdd := `################### ADD ###################
╭─╮
│.│
╰┬╯
╭──────┴─────╮
│aws_instance│
╰──────┬─────╯
╭─────┴─────╮
│example1(+)│
╰───────────╯
`
expectedDelete := `################### DELETE ###################
╭─╮
│.│
╰┬╯
╭──────┴─────╮
│aws_instance│
╰──────┬─────╯
╭─────┴─────╮
│example2(-)│
╰───────────╯
`

actualOutput := removeANSI(buf.String())
assert.Contains(t, actualOutput, expectedAdd)
assert.Contains(t, actualOutput, expectedDelete)

})

t.Run("Drawable False", func(t *testing.T) {
tw := NewSeparateTree(mockChanges, false)
var buf bytes.Buffer
err := tw.Write(&buf)

assert.NoError(t, err)

expectedAdd := `################### ADD ###################
|---aws_instance
| |---example1(+)
`
expectedDelete := `################### DELETE ###################
|---aws_instance
| |---example2(-)
`

actualOutput := removeANSI(buf.String())
assert.Contains(t, actualOutput, expectedAdd)
assert.Contains(t, actualOutput, expectedDelete)
})

t.Run("Error Handling", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

s := NewSeparateTree(mockChanges, false)

t.Run("Write Error", func(t *testing.T) {
mockWriter := mocks.NewMockWriter(ctrl)
mockWriter.EXPECT().Write(gomock.Any()).Return(0, errors.New("write error")).Times(1)

err := s.Write(mockWriter)
assert.Error(t, err)
assert.Contains(t, err.Error(), "write error")
})

t.Run("Tree Writer Error", func(t *testing.T) {
originalFunc := NewTreeWriterFunc
defer func() { NewTreeWriterFunc = originalFunc }()

// Replace NewTreeWriter with the mock that returns an error
NewTreeWriterFunc = NewMockTreeWriterWithError

mockWriter := mocks.NewMockWriter(ctrl)
mockWriter.EXPECT().Write(gomock.Any()).Return(0, nil).AnyTimes()

err := s.Write(mockWriter)
assert.Error(t, err)
assert.Contains(t, err.Error(), "tree writer error")
})

t.Run("Final Newline Write Error", func(t *testing.T) {
originalFunc := NewTreeWriterFunc
defer func() { NewTreeWriterFunc = originalFunc }()

NewTreeWriterFunc = NewMockTreeWriter

mockWriter := mocks.NewMockWriter(ctrl)
mockWriter.EXPECT().Write(gomock.Any()).Return(0, nil).Times(1)
mockWriter.EXPECT().Write(gomock.Any()).Return(0, errors.New("write error")).Times(1)

err := s.Write(mockWriter)
assert.Error(t, err)
assert.Contains(t, err.Error(), "write error")
})
})
}
84 changes: 84 additions & 0 deletions writer/table_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package writer

import (
"bytes"
"testing"

"github.com/dineshba/tf-summarize/terraformstate"
"github.com/stretchr/testify/assert"
)

func TestTableWriter_Write_NoMarkdown(t *testing.T) {
changes := createMockChanges()

outputChanges := map[string][]string{
"update": {
"output.example",
},
}

tw := NewTableWriter(changes, outputChanges, false)
var output bytes.Buffer
err := tw.Write(&output)
assert.NoError(t, err)

expectedOutput := `+--------+-----------------------+
| CHANGE | RESOURCE |
+--------+-----------------------+
| add | aws_instance.example1 |
+--------+-----------------------+
| delete | aws_instance.example2 |
+--------+-----------------------+
+--------+----------------+
| CHANGE | OUTPUT |
+--------+----------------+
| update | output.example |
+--------+----------------+
`

assert.Equal(t, expectedOutput, output.String())
}

func TestTableWriter_Write_WithMarkdown(t *testing.T) {
changes := createMockChanges()

outputChanges := map[string][]string{
"update": {
"output.example",
},
}

tw := NewTableWriter(changes, outputChanges, true)
var output bytes.Buffer
err := tw.Write(&output)
assert.NoError(t, err)

expectedOutput := `| CHANGE | RESOURCE |
|--------|-------------------------|
| add | ` + "`aws_instance.example1`" + ` |
| delete | ` + "`aws_instance.example2`" + ` |
| CHANGE | OUTPUT |
|--------|------------------|
| update | ` + "`output.example`" + ` |
`

assert.Equal(t, expectedOutput, output.String())
}

func TestTableWriter_NoChanges(t *testing.T) {
changes := map[string]terraformstate.ResourceChanges{}
outputChanges := map[string][]string{}

tw := NewTableWriter(changes, outputChanges, false)
var output bytes.Buffer
err := tw.Write(&output)
assert.NoError(t, err)

expectedOutput := `+--------+----------+
| CHANGE | RESOURCE |
+--------+----------+
+--------+----------+
`
assert.Equal(t, expectedOutput, output.String())
}
115 changes: 115 additions & 0 deletions writer/tree_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package writer

import (
"bytes"
"errors"
"testing"

"github.com/dineshba/tf-summarize/terraformstate"
. "github.com/hashicorp/terraform-json"
tfjson "github.com/hashicorp/terraform-json"
"github.com/stretchr/testify/assert"
)

func TestTreeWriter_Write_DrawableTrue(t *testing.T) {

changes := terraformstate.ResourceChanges{
&tfjson.ResourceChange{Address: "module.test.azapi_resource.logical_network", Change: &Change{Actions: Actions{ActionNoop}}},
}

tw := NewTreeWriter(changes, true)

var buf bytes.Buffer
err := tw.Write(&buf)

assert.NoError(t, err)

expectedOutput := ` ╭─╮
│.│
╰┬╯
╭───┴──╮
│module│
╰───┬──╯
╭──┴─╮
│test│
╰──┬─╯
╭───────┴──────╮
│azapi_resource│
╰───────┬──────╯
╭───────┴───────╮
│logical_network│
╰───────────────╯
`

assert.Equal(t, expectedOutput, removeANSI(buf.String()))
}

func TestTreeWriter_Write_NonDrawable(t *testing.T) {

changes := terraformstate.ResourceChanges{
&tfjson.ResourceChange{Address: "module.test.azapi_resource.logical_network", Change: &Change{Actions: Actions{ActionNoop}}},
}

tw := NewTreeWriter(changes, false)

var buf bytes.Buffer
err := tw.Write(&buf)

assert.NoError(t, err)
assert.Contains(t, buf.String(), "|---module")
assert.Contains(t, buf.String(), "|---test")
assert.Contains(t, buf.String(), "|---azapi_resource")
assert.Contains(t, buf.String(), "|---logical_network")

expectedOutput := `|---module
| |---test
| | |---azapi_resource
| | | |---logical_network
`

assert.Equal(t, expectedOutput, removeANSI(buf.String()))
}

func TestTreeWriter_Write_NonDrawable_PrintTreeError(t *testing.T) {

changes := terraformstate.ResourceChanges{
&tfjson.ResourceChange{Address: "module.test.azapi_resource.logical_network", Change: &Change{Actions: Actions{ActionNoop}}},
}
tw := NewTreeWriter(changes, false)
faultyWriter := &errorWriter{}
err := tw.Write(faultyWriter)
assert.Error(t, err)
}

func TestTreeWriter_Write_PrintTreeError(t *testing.T) {
changes := terraformstate.ResourceChanges{
&tfjson.ResourceChange{Address: "module.test.azapi_resource.logical_network", Change: &Change{Actions: Actions{ActionNoop}}},
}
tw := NewTreeWriter(changes, true)
faultyWriter := &errorWriter{}
err := tw.Write(faultyWriter)
assert.Error(t, err)
assert.Contains(t, err.Error(), "simulated write error")
}

func TestTreeWriter_Write_EmptyChanges(t *testing.T) {
changes := terraformstate.ResourceChanges{} // Empty changes
tw := NewTreeWriter(changes, false)
var buf bytes.Buffer
err := tw.Write(&buf)

// Verify output and no errors (it should handle empty cases gracefully)
assert.NoError(t, err)
assert.Equal(t, "", buf.String())
}

// Custom faulty writer to simulate write errors
type errorWriter struct{}

func (ew *errorWriter) Write(p []byte) (n int, err error) {
return 0, errors.New("simulated write error")
}
16 changes: 15 additions & 1 deletion writer/util.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
package writer

import "embed"
import (
"embed"
"io/fs"
"regexp"
)

// Embed the templates directory in the compiled binary.
//
//go:embed templates
var templates embed.FS

var getFS = func() fs.FS {
return templates
}

func hasOutputChanges(opChanges map[string][]string) bool {
hasChanges := false

@@ -19,3 +27,9 @@ func hasOutputChanges(opChanges map[string][]string) bool {

return hasChanges
}

// Function to remove ANSI escape sequences
func removeANSI(input string) string {
re := regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`)
return re.ReplaceAllString(input, "")
}
88 changes: 88 additions & 0 deletions writer/util_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package writer

import (
"testing"
)

// Test for hasOutputChanges function
func TestHasOutputChanges(t *testing.T) {
tests := []struct {
name string
input map[string][]string
expected bool
}{
{
name: "No changes",
input: map[string][]string{
"change1": {},
"change2": {},
},
expected: false,
},
{
name: "Has changes",
input: map[string][]string{
"change1": {"added"},
"change2": {},
},
expected: true,
},
{
name: "All empty",
input: map[string][]string{
"change1": {},
"change2": {},
"change3": {},
},
expected: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := hasOutputChanges(tt.input)
if result != tt.expected {
t.Errorf("expected %v, got %v", tt.expected, result)
}
})
}
}

// Test for removeANSI function
func TestRemoveANSI(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "String without ANSI escape",
input: "This is a normal string",
expected: "This is a normal string",
},
{
name: "String with ANSI escape",
input: "\x1b[31mThis is red\x1b[0m",
expected: "This is red",
},
{
name: "String with multiple ANSI escapes",
input: "\x1b[31mThis is red\x1b[0m and \x1b[32mthis is green\x1b[0m",
expected: "This is red and this is green",
},
{
name: "Empty string",
input: "",
expected: "",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := removeANSI(tt.input)
if result != tt.expected {
t.Errorf("expected %v, got %v", tt.expected, result)
}
})
}
}

0 comments on commit 3483af4

Please sign in to comment.