github.com/Redstoneguy129/cli@v0.0.0-20230211220159-15dca4e91917/internal/db/lint/lint_test.go (about)

     1  package lint
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"net/http"
     8  	"testing"
     9  
    10  	"github.com/Redstoneguy129/cli/internal/testing/apitest"
    11  	"github.com/Redstoneguy129/cli/internal/testing/pgtest"
    12  	"github.com/Redstoneguy129/cli/internal/utils"
    13  	"github.com/docker/docker/api/types"
    14  	"github.com/docker/docker/client"
    15  	"github.com/jackc/pgerrcode"
    16  	"github.com/spf13/afero"
    17  	"github.com/stretchr/testify/assert"
    18  	"github.com/stretchr/testify/require"
    19  	"gopkg.in/h2non/gock.v1"
    20  )
    21  
    22  func TestLintCommand(t *testing.T) {
    23  	const version = "1.41"
    24  
    25  	// Setup in-memory fs
    26  	fsys := afero.NewMemMapFs()
    27  	require.NoError(t, utils.WriteConfig(fsys, false))
    28  	// Setup mock docker
    29  	require.NoError(t, client.WithHTTPClient(http.DefaultClient)(utils.Docker))
    30  	defer gock.OffAll()
    31  	gock.New("http:///var/run/docker.sock").
    32  		Head("/_ping").
    33  		Reply(http.StatusOK).
    34  		SetHeader("API-Version", version).
    35  		SetHeader("OSType", "linux")
    36  	gock.New("http:///var/run/docker.sock").
    37  		Get("/v" + version + "/containers").
    38  		Reply(200).
    39  		JSON(types.ContainerJSON{})
    40  	// Setup db response
    41  	expected := Result{
    42  		Function: "22751",
    43  		Issues: []Issue{{
    44  			Level:   AllowedLevels[1],
    45  			Message: `record "r" has no field "c"`,
    46  			Statement: &Statement{
    47  				LineNumber: "6",
    48  				Text:       "RAISE",
    49  			},
    50  			Context:  `SQL expression "r.c"`,
    51  			SQLState: pgerrcode.UndefinedColumn,
    52  		}},
    53  	}
    54  	data, err := json.Marshal(expected)
    55  	require.NoError(t, err)
    56  	// Setup mock postgres
    57  	conn := pgtest.NewConn()
    58  	defer conn.Close(t)
    59  	conn.Query("begin").Reply("BEGIN").
    60  		Query(ENABLE_PGSQL_CHECK).
    61  		Reply("CREATE EXTENSION").
    62  		Query(checkSchemaScript, "public").
    63  		Reply("SELECT 1", []interface{}{"f1", string(data)}).
    64  		Query("rollback").Reply("ROLLBACK")
    65  	// Run test
    66  	assert.NoError(t, Run(context.Background(), []string{"public"}, "warning", fsys, conn.Intercept))
    67  	// Validate api
    68  	assert.Empty(t, apitest.ListUnmatchedRequests())
    69  }
    70  
    71  func TestLintDatabase(t *testing.T) {
    72  	t.Run("parses lint results", func(t *testing.T) {
    73  		expected := []Result{{
    74  			Function: "public.f1",
    75  			Issues: []Issue{{
    76  				Level:   AllowedLevels[1],
    77  				Message: `record "r" has no field "c"`,
    78  				Statement: &Statement{
    79  					LineNumber: "6",
    80  					Text:       "RAISE",
    81  				},
    82  				Context:  `SQL expression "r.c"`,
    83  				SQLState: pgerrcode.UndefinedColumn,
    84  			}, {
    85  				Level:    "warning extra",
    86  				Message:  `never read variable "entity"`,
    87  				SQLState: pgerrcode.SuccessfulCompletion,
    88  			}},
    89  		}, {
    90  			Function: "public.f2",
    91  			Issues: []Issue{{
    92  				Level:   AllowedLevels[1],
    93  				Message: `relation "t2" does not exist`,
    94  				Statement: &Statement{
    95  					LineNumber: "4",
    96  					Text:       "FOR over SELECT rows",
    97  				},
    98  				Query: &Query{
    99  					Position: "15",
   100  					Text:     "SELECT * FROM t2",
   101  				},
   102  				SQLState: pgerrcode.UndefinedTable,
   103  			}},
   104  		}}
   105  		r1, err := json.Marshal(expected[0])
   106  		require.NoError(t, err)
   107  		r2, err := json.Marshal(expected[1])
   108  		require.NoError(t, err)
   109  		// Setup mock postgres
   110  		conn := pgtest.NewConn()
   111  		defer conn.Close(t)
   112  		conn.Query("begin").Reply("BEGIN").
   113  			Query(ENABLE_PGSQL_CHECK).
   114  			Reply("CREATE EXTENSION").
   115  			Query(checkSchemaScript, "public").
   116  			Reply("SELECT 2",
   117  				[]interface{}{"f1", string(r1)},
   118  				[]interface{}{"f2", string(r2)},
   119  			).
   120  			Query("rollback").Reply("ROLLBACK")
   121  		// Connect to mock
   122  		ctx := context.Background()
   123  		mock, err := utils.ConnectLocalPostgres(ctx, utils.Config.Hostname, 5432, "postgres", conn.Intercept)
   124  		require.NoError(t, err)
   125  		defer mock.Close(ctx)
   126  		// Run test
   127  		result, err := LintDatabase(ctx, mock, []string{"public"})
   128  		assert.NoError(t, err)
   129  		// Validate result
   130  		assert.ElementsMatch(t, expected, result)
   131  	})
   132  
   133  	t.Run("supports multiple schema", func(t *testing.T) {
   134  		expected := []Result{{
   135  			Function: "public.where_clause",
   136  			Issues: []Issue{{
   137  				Level:   AllowedLevels[0],
   138  				Message: "target type is different type than source type",
   139  				Statement: &Statement{
   140  					LineNumber: "32",
   141  					Text:       "statement block",
   142  				},
   143  				Hint:     "The input expression type does not have an assignment cast to the target type.",
   144  				Detail:   `cast "text" value to "text[]" type`,
   145  				Context:  `during statement block local variable "clause_arr" initialization on line 3`,
   146  				SQLState: pgerrcode.DatatypeMismatch,
   147  			}},
   148  		}, {
   149  			Function: "private.f2",
   150  			Issues:   []Issue{},
   151  		}}
   152  		r1, err := json.Marshal(expected[0])
   153  		require.NoError(t, err)
   154  		r2, err := json.Marshal(expected[1])
   155  		require.NoError(t, err)
   156  		// Setup mock postgres
   157  		conn := pgtest.NewConn()
   158  		defer conn.Close(t)
   159  		conn.Query("begin").Reply("BEGIN").
   160  			Query(ENABLE_PGSQL_CHECK).
   161  			Reply("CREATE EXTENSION").
   162  			Query(checkSchemaScript, "public").
   163  			Reply("SELECT 1", []interface{}{"where_clause", string(r1)}).
   164  			Query(checkSchemaScript, "private").
   165  			Reply("SELECT 1", []interface{}{"f2", string(r2)}).
   166  			Query("rollback").Reply("ROLLBACK")
   167  		// Connect to mock
   168  		ctx := context.Background()
   169  		mock, err := utils.ConnectLocalPostgres(ctx, utils.Config.Hostname, 5432, "postgres", conn.Intercept)
   170  		require.NoError(t, err)
   171  		defer mock.Close(ctx)
   172  		// Run test
   173  		result, err := LintDatabase(ctx, mock, []string{"public", "private"})
   174  		assert.NoError(t, err)
   175  		// Validate result
   176  		assert.ElementsMatch(t, expected, result)
   177  	})
   178  
   179  	t.Run("throws error on missing extension", func(t *testing.T) {
   180  		// Setup mock postgres
   181  		conn := pgtest.NewConn()
   182  		defer conn.Close(t)
   183  		conn.Query("begin").Reply("BEGIN").
   184  			Query(ENABLE_PGSQL_CHECK).
   185  			ReplyError(pgerrcode.UndefinedFile, `could not open extension control file "/usr/share/postgresql/14/extension/plpgsql_check.control": No such file or directory"`).
   186  			Query("rollback").Reply("ROLLBACK")
   187  		// Connect to mock
   188  		ctx := context.Background()
   189  		mock, err := utils.ConnectLocalPostgres(ctx, utils.Config.Hostname, 5432, "postgres", conn.Intercept)
   190  		require.NoError(t, err)
   191  		defer mock.Close(ctx)
   192  		// Run test
   193  		_, err = LintDatabase(ctx, mock, []string{"public"})
   194  		assert.Error(t, err)
   195  	})
   196  
   197  	t.Run("throws error on malformed json", func(t *testing.T) {
   198  		// Setup mock postgres
   199  		conn := pgtest.NewConn()
   200  		defer conn.Close(t)
   201  		conn.Query("begin").Reply("BEGIN").
   202  			Query(ENABLE_PGSQL_CHECK).
   203  			Reply("CREATE EXTENSION").
   204  			Query(checkSchemaScript, "public").
   205  			Reply("SELECT 1", []interface{}{"f1", "malformed"}).
   206  			Query("rollback").Reply("ROLLBACK")
   207  		// Connect to mock
   208  		ctx := context.Background()
   209  		mock, err := utils.ConnectLocalPostgres(ctx, utils.Config.Hostname, 5432, "postgres", conn.Intercept)
   210  		require.NoError(t, err)
   211  		defer mock.Close(ctx)
   212  		// Run test
   213  		_, err = LintDatabase(ctx, mock, []string{"public"})
   214  		assert.Error(t, err)
   215  	})
   216  }
   217  
   218  func TestPrintResult(t *testing.T) {
   219  	result := []Result{{
   220  		Function: "public.f1",
   221  		Issues: []Issue{{
   222  			Level:   "warning",
   223  			Message: "test 1a",
   224  		}, {
   225  			Level:   "error",
   226  			Message: "test 1b",
   227  		}},
   228  	}, {
   229  		Function: "private.f2",
   230  		Issues: []Issue{{
   231  			Level:   "warning extra",
   232  			Message: "test 2",
   233  		}},
   234  	}}
   235  
   236  	t.Run("filters warning level", func(t *testing.T) {
   237  		// Run test
   238  		var out bytes.Buffer
   239  		assert.NoError(t, printResultJSON(result, toEnum("warning"), &out))
   240  		// Validate output
   241  		var actual []Result
   242  		assert.NoError(t, json.Unmarshal(out.Bytes(), &actual))
   243  		assert.ElementsMatch(t, result, actual)
   244  	})
   245  
   246  	t.Run("filters error level", func(t *testing.T) {
   247  		// Run test
   248  		var out bytes.Buffer
   249  		assert.NoError(t, printResultJSON(result, toEnum("error"), &out))
   250  		// Validate output
   251  		var actual []Result
   252  		assert.NoError(t, json.Unmarshal(out.Bytes(), &actual))
   253  		assert.ElementsMatch(t, []Result{{
   254  			Function: result[0].Function,
   255  			Issues:   []Issue{result[0].Issues[1]},
   256  		}}, actual)
   257  	})
   258  }