github.com/mineiros-io/terradoc@v0.0.9-0.20220711062319-018bd4ae81f5/internal/renderers/markdown/writer_test.go (about)

     1  package markdown
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"strings"
     8  	"testing"
     9  
    10  	"github.com/google/go-cmp/cmp"
    11  	"github.com/madlambda/spells/assert"
    12  	"github.com/mineiros-io/terradoc/internal/entities"
    13  	"github.com/mineiros-io/terradoc/internal/types"
    14  )
    15  
    16  const (
    17  	lineBreak = "\n"
    18  )
    19  
    20  func TestWriteSection(t *testing.T) {
    21  	for _, tt := range []struct {
    22  		desc    string
    23  		section entities.Section
    24  		want    mdSection
    25  	}{
    26  		{
    27  			desc: "with description and 4 levels",
    28  			section: entities.Section{
    29  				Level: 4,
    30  				Title: "I AM THE TITLE!",
    31  				Content: `Basic usage for granting an AWS Account with Account ID ` + "`123456789012`" + ` access to assume a role that grants
    32        full access to AWS Simple Storage Service (S3)
    33        ` + "```hcl" + `
    34        module "role-s3-full-access" {
    35          source  = "mineiros-io/iam-role/aws"
    36          version = "~> 0.6.0"
    37  
    38          name = "S3FullAccess"
    39  
    40          assume_role_principals = [
    41            {
    42              type        = "AWS"
    43              identifiers = ["arn:aws:iam::123456789012:root"]
    44            }
    45          ]
    46  
    47          policy_statements = [
    48            {
    49              sid = "FullS3Access"
    50  
    51              effect    = "Allow"
    52              actions   = ["s3:*"]
    53              resources = ["*"]
    54            }
    55          ]
    56        }
    57        ` + "```" + `
    58  `,
    59  			},
    60  			want: mdSection{
    61  				heading: "#### I AM THE TITLE!",
    62  				description: `Basic usage for granting an AWS Account with Account ID ` + "`123456789012`" + ` access to assume a role that grants
    63        full access to AWS Simple Storage Service (S3)
    64        ` + "```hcl" + `
    65        module "role-s3-full-access" {
    66          source  = "mineiros-io/iam-role/aws"
    67          version = "~> 0.6.0"
    68  
    69          name = "S3FullAccess"
    70  
    71          assume_role_principals = [
    72            {
    73              type        = "AWS"
    74              identifiers = ["arn:aws:iam::123456789012:root"]
    75            }
    76          ]
    77  
    78          policy_statements = [
    79            {
    80              sid = "FullS3Access"
    81  
    82              effect    = "Allow"
    83              actions   = ["s3:*"]
    84              resources = ["*"]
    85            }
    86          ]
    87        }
    88        ` + "```" + `
    89  `,
    90  			},
    91  		},
    92  		{
    93  			desc: "without description and 1 level",
    94  			section: entities.Section{
    95  				Level: 1,
    96  				Title: "section title",
    97  			},
    98  			want: mdSection{
    99  				heading: "# section title",
   100  			},
   101  		},
   102  	} {
   103  		t.Run(tt.desc, func(t *testing.T) {
   104  			buf := &bytes.Buffer{}
   105  
   106  			writer := newTestWriter(t, buf)
   107  
   108  			err := writer.writeSection(tt.section)
   109  			assert.NoError(t, err)
   110  
   111  			assertMarkdownHasSection(t, buf, tt.want)
   112  		})
   113  	}
   114  }
   115  
   116  func TestWriteVariable(t *testing.T) {
   117  	for _, tt := range []struct {
   118  		desc     string
   119  		variable entities.Variable
   120  		want     mdVariable
   121  	}{
   122  		{
   123  			desc: "a required string variable with description and default that forces recreation",
   124  			variable: entities.Variable{
   125  				Name: "string_variable",
   126  				Type: entities.Type{
   127  					TFType: types.TerraformString,
   128  				},
   129  				ForcesRecreation: true,
   130  				Required:         true,
   131  				Description:      "i am a variable",
   132  				Default:          []byte(`"default value"`),
   133  			},
   134  			want: mdVariable{
   135  				item:        "- [**`string_variable`**](#var-string_variable): *(**Required** `string`, Forces new resource)*<a name=\"var-string_variable\"></a>",
   136  				description: "i am a variable",
   137  				defaults:    "Default is `\"default value\"`.",
   138  			},
   139  		},
   140  		{
   141  			desc: "an optional number variable with defaults that forces recreation",
   142  			variable: entities.Variable{
   143  				Name: "number_variable",
   144  				Type: entities.Type{
   145  					TFType: types.TerraformNumber,
   146  				},
   147  				ForcesRecreation: true,
   148  				Required:         false,
   149  				Default:          []byte("123"),
   150  			},
   151  			want: mdVariable{
   152  				item:     "- [**`number_variable`**](#var-number_variable): *(Optional `number`, Forces new resource)*<a name=\"var-number_variable\"></a>",
   153  				defaults: "Default is `123`.",
   154  			},
   155  		},
   156  		{
   157  			desc: "a bool variable",
   158  			variable: entities.Variable{
   159  				Name: "bool_variable",
   160  				Type: entities.Type{
   161  					TFType: types.TerraformBool,
   162  				},
   163  				ForcesRecreation: false,
   164  				Required:         false,
   165  			},
   166  			want: mdVariable{
   167  				item: "- [**`bool_variable`**](#var-bool_variable): *(Optional `bool`)*<a name=\"var-bool_variable\"></a>",
   168  			},
   169  		},
   170  		{
   171  			desc: "an object variable with readme example",
   172  			variable: entities.Variable{
   173  				Name: "obj_variable",
   174  				Type: entities.Type{
   175  					TFType: types.TerraformObject,
   176  				},
   177  				ForcesRecreation: true,
   178  				Required:         true,
   179  				ReadmeExample: `obj_variable = {
   180    a = "foo"
   181  }
   182  `,
   183  			},
   184  			want: mdVariable{
   185  				item: "- [**`obj_variable`**](#var-obj_variable): *(**Required** `object`, Forces new resource)*<a name=\"var-obj_variable\"></a>",
   186  				readmeExample: `obj_variable = {
   187      a = "foo"
   188    }
   189  `,
   190  			},
   191  		},
   192  	} {
   193  		t.Run(tt.desc, func(t *testing.T) {
   194  			buf := &bytes.Buffer{}
   195  
   196  			writer := newTestWriter(t, buf)
   197  
   198  			err := writer.writeVariable(tt.variable)
   199  			assert.NoError(t, err)
   200  
   201  			assertMarkdownHasVariable(t, buf, tt.want)
   202  		})
   203  	}
   204  }
   205  
   206  func TestWriteAttribute(t *testing.T) {
   207  	for _, tt := range []struct {
   208  		desc string
   209  		attr entities.Attribute
   210  		want mdAttribute
   211  	}{
   212  		{
   213  			desc: "a required string attribute with description that forces recreation",
   214  			attr: entities.Attribute{
   215  				Level:       1,
   216  				Name:        "string_attribute",
   217  				Description: "i am this attribute's description",
   218  				Type: entities.Type{
   219  					TFType: types.TerraformString,
   220  				},
   221  				ForcesRecreation: true,
   222  				Required:         true,
   223  			},
   224  			want: mdAttribute{
   225  				item:        "  - [**`string_attribute`**](#attr-parent_var_name-string_attribute): *(**Required** `string`, Forces new resource)*<a name=\"attr-parent_var_name-string_attribute\"></a>",
   226  				description: "  i am this attribute's description",
   227  			},
   228  		},
   229  		{
   230  			desc: "an optional number attribute that forces recreations",
   231  			attr: entities.Attribute{
   232  				Level: 2,
   233  				Name:  "number_attribute",
   234  				Type: entities.Type{
   235  					TFType: types.TerraformNumber,
   236  				},
   237  				ForcesRecreation: true,
   238  				Required:         false,
   239  			},
   240  			want: mdAttribute{
   241  				item: "    - [**`number_attribute`**](#attr-parent_var_name-number_attribute): *(Optional `number`, Forces new resource)*<a name=\"attr-parent_var_name-number_attribute\"></a>",
   242  			},
   243  		},
   244  		{
   245  			desc: "a bool attribute",
   246  			attr: entities.Attribute{
   247  				Level: 0,
   248  				Name:  "bool_attribute",
   249  				Type: entities.Type{
   250  					TFType: types.TerraformBool,
   251  				},
   252  				ForcesRecreation: false,
   253  				Required:         false,
   254  			},
   255  			want: mdAttribute{
   256  				item: "- [**`bool_attribute`**](#attr-parent_var_name-bool_attribute): *(Optional `bool`)*<a name=\"attr-parent_var_name-bool_attribute\"></a>",
   257  			},
   258  		},
   259  		{
   260  			desc: "an attribute with defautlts",
   261  			attr: entities.Attribute{
   262  				Level: 1,
   263  				Name:  "i_have_a_default",
   264  				Type: entities.Type{
   265  					TFType: types.TerraformNumber,
   266  				},
   267  				Default: []byte("123"),
   268  			},
   269  			want: mdAttribute{
   270  				item:        "  - [**`i_have_a_default`**](#attr-parent_var_name-i_have_a_default): *(Optional `number`)*<a name=\"attr-parent_var_name-i_have_a_default\"></a>",
   271  				description: "  Default is `123`.",
   272  			},
   273  		},
   274  	} {
   275  		t.Run(tt.desc, func(t *testing.T) {
   276  			buf := &bytes.Buffer{}
   277  
   278  			writer := newTestWriter(t, buf)
   279  			err := writer.writeAttribute(tt.attr, "parent_var_name")
   280  			assert.NoError(t, err)
   281  
   282  			assertMarkdownHasAttribute(t, buf, tt.want)
   283  		})
   284  	}
   285  }
   286  
   287  func TestWriteAttributeWithNested(t *testing.T) {
   288  	t.Skip("write rendering tests for nested attributes once we know more about it")
   289  }
   290  
   291  func TestWriteOutput(t *testing.T) {
   292  	for _, tt := range []struct {
   293  		desc   string
   294  		output entities.Output
   295  		want   mdOutput
   296  	}{
   297  		{
   298  			desc: "",
   299  			output: entities.Output{
   300  				Name:        "string_output",
   301  				Description: "i am an output",
   302  				Type: entities.Type{
   303  					TFType: types.TerraformString,
   304  				},
   305  			},
   306  			want: mdOutput{
   307  				item:        "- [**`string_output`**](#output-string_output): *(`string`)*<a name=\"output-string_output\"></a>",
   308  				description: "i am an output",
   309  			},
   310  		},
   311  		{
   312  			desc: "an optional number output with defaults that forces recreation",
   313  			output: entities.Output{
   314  				Name: "number_output",
   315  				Type: entities.Type{
   316  					TFType: types.TerraformNumber,
   317  				},
   318  			},
   319  			want: mdOutput{
   320  				item: "- [**`number_output`**](#output-number_output): *(`number`)*<a name=\"output-number_output\"></a>",
   321  			},
   322  		},
   323  		{
   324  			desc: "a bool output",
   325  			output: entities.Output{
   326  				Name: "bool_output",
   327  				Type: entities.Type{
   328  					TFType: types.TerraformBool,
   329  				},
   330  			},
   331  			want: mdOutput{
   332  				item: "- [**`bool_output`**](#output-bool_output): *(`bool`)*<a name=\"output-bool_output\"></a>",
   333  			},
   334  		},
   335  		{
   336  			desc: "an object output with readme example",
   337  			output: entities.Output{
   338  				Name: "obj_output",
   339  				Type: entities.Type{
   340  					TFType: types.TerraformObject,
   341  					Label:  "some_object",
   342  				},
   343  			},
   344  			want: mdOutput{
   345  				item: "- [**`obj_output`**](#output-obj_output): *(`object(some_object)`)*<a name=\"output-obj_output\"></a>",
   346  			},
   347  		},
   348  	} {
   349  		t.Run(tt.desc, func(t *testing.T) {
   350  			buf := &bytes.Buffer{}
   351  
   352  			writer := newTestWriter(t, buf)
   353  
   354  			err := writer.writeOutput(tt.output)
   355  			assert.NoError(t, err)
   356  
   357  			assertMarkdownHasOutput(t, buf, tt.want)
   358  		})
   359  	}
   360  }
   361  
   362  // TODO: rewrite all? :D
   363  
   364  type mdSection struct {
   365  	heading     string
   366  	description string
   367  }
   368  
   369  type mdVariable struct {
   370  	item          string
   371  	description   string
   372  	defaults      string
   373  	readmeExample string
   374  }
   375  
   376  type mdAttribute struct {
   377  	item          string
   378  	description   string
   379  	defaults      string
   380  	readmeExample string
   381  }
   382  
   383  type mdOutput struct {
   384  	item        string
   385  	description string
   386  }
   387  
   388  func assertMarkdownHasSection(t *testing.T, buf *bytes.Buffer, md mdSection) {
   389  	t.Helper()
   390  
   391  	want := md.heading + lineBreak
   392  
   393  	if md.description != "" {
   394  		want += lineBreak + md.description + lineBreak
   395  	}
   396  
   397  	want += lineBreak
   398  
   399  	if diff := cmp.Diff(want, buf.String()); diff != "" {
   400  		t.Errorf("Expected section markdown to match (-want +got):\n%s", diff)
   401  	}
   402  }
   403  
   404  func assertMarkdownHasVariable(t *testing.T, buf *bytes.Buffer, md mdVariable) {
   405  	t.Helper()
   406  
   407  	want := md.item + lineBreak
   408  
   409  	if md.description != "" {
   410  		want += fmt.Sprintf("\n  %s\n", md.description)
   411  	}
   412  
   413  	if md.defaults != "" {
   414  		want += fmt.Sprintf("\n  %s\n", md.defaults)
   415  	}
   416  
   417  	if md.readmeExample != "" {
   418  		// TODO: what's a better way of checking that indentation is right?
   419  		want += fmt.Sprintf("\n  Example:\n\n  ```hcl\n  %s\n  ```\n", md.readmeExample)
   420  	}
   421  
   422  	want += "\n"
   423  
   424  	if diff := cmp.Diff(want, buf.String()); diff != "" {
   425  		t.Errorf("Expected variable markdown to match (-want +got):\n%s", diff)
   426  	}
   427  }
   428  
   429  func assertMarkdownHasAttribute(t *testing.T, buf *bytes.Buffer, md mdAttribute) {
   430  	t.Helper()
   431  
   432  	want := md.item + lineBreak
   433  
   434  	if md.description != "" {
   435  		want += fmt.Sprintf("\n  %s\n", md.description)
   436  	}
   437  
   438  	if md.defaults != "" {
   439  		want += fmt.Sprintf("\n  %s\n", md.defaults)
   440  	}
   441  
   442  	if md.readmeExample != "" {
   443  		want += fmt.Sprintf("\n  Example:\n\n  ```hcl\n  %s\n \n ```\n", md.readmeExample)
   444  	}
   445  
   446  	want += lineBreak
   447  
   448  	got := strings.TrimLeft(buf.String(), "\n")
   449  
   450  	if diff := cmp.Diff(want, got); diff != "" {
   451  		t.Errorf("Expected variable markdown to match (-want +got):\n%s", diff)
   452  	}
   453  }
   454  
   455  func assertMarkdownHasOutput(t *testing.T, buf *bytes.Buffer, md mdOutput) {
   456  	t.Helper()
   457  
   458  	want := md.item + lineBreak
   459  
   460  	if md.description != "" {
   461  		want += fmt.Sprintf("\n  %s\n", md.description)
   462  	}
   463  
   464  	want += "\n"
   465  
   466  	if diff := cmp.Diff(want, buf.String()); diff != "" {
   467  		t.Errorf("Expected output markdown to match (-want +got):\n%s", diff)
   468  	}
   469  }
   470  
   471  func newTestWriter(t *testing.T, buf io.Writer) *markdownWriter {
   472  	writer, err := newMarkdownWriter(buf)
   473  	assert.NoError(t, err)
   474  
   475  	return writer
   476  }