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