package lint import ( "bytes" "context" "encoding/json" "net/http" "testing" "github.com/docker/docker/api/types" "github.com/h2non/gock" "github.com/jackc/pgconn" "github.com/jackc/pgerrcode" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/supabase/cli/internal/testing/apitest" "github.com/supabase/cli/internal/utils" "github.com/supabase/cli/pkg/pgtest" ) var dbConfig = pgconn.Config{ Host: "127.0.0.1", Port: 5432, User: "admin", Password: "password", Database: "postgres", } func TestLintCommand(t *testing.T) { utils.Config.Hostname = "127.0.0.1" utils.Config.Db.Port = 5432 // Setup in-memory fs fsys := afero.NewMemMapFs() // Setup mock docker require.NoError(t, apitest.MockDocker(utils.Docker)) defer gock.OffAll() gock.New(utils.Docker.DaemonHost()). Get("/v" + utils.Docker.ClientVersion() + "/containers"). Reply(http.StatusOK). JSON(types.ContainerJSON{}) // Setup db response expected := Result{ Function: "22751", Issues: []Issue{{ Level: AllowedLevels[1], Message: `record "r" has no field "c"`, Statement: &Statement{ LineNumber: "6", Text: "RAISE", }, Context: `SQL expression "r.c"`, SQLState: pgerrcode.UndefinedColumn, }}, } data, err := json.Marshal(expected) require.NoError(t, err) // Setup mock postgres conn := pgtest.NewConn() defer conn.Close(t) conn.Query("begin").Reply("BEGIN"). Query(ENABLE_PGSQL_CHECK). Reply("CREATE EXTENSION"). Query(checkSchemaScript, "public"). Reply("SELECT 1", []interface{}{"f1", string(data)}). Query("rollback").Reply("ROLLBACK") // Run test err = Run(context.Background(), []string{"public"}, "warning", "none", dbConfig, fsys, conn.Intercept) // Check error assert.NoError(t, err) assert.Empty(t, apitest.ListUnmatchedRequests()) } func TestLintDatabase(t *testing.T) { t.Run("parses lint results", func(t *testing.T) { expected := []Result{{ Function: "public.f1", Issues: []Issue{{ Level: AllowedLevels[1], Message: `record "r" has no field "c"`, Statement: &Statement{ LineNumber: "6", Text: "RAISE", }, Context: `SQL expression "r.c"`, SQLState: pgerrcode.UndefinedColumn, }, { Level: "warning extra", Message: `never read variable "entity"`, SQLState: pgerrcode.SuccessfulCompletion, }}, }, { Function: "public.f2", Issues: []Issue{{ Level: AllowedLevels[1], Message: `relation "t2" does not exist`, Statement: &Statement{ LineNumber: "4", Text: "FOR over SELECT rows", }, Query: &Query{ Position: "15", Text: "SELECT * FROM t2", }, SQLState: pgerrcode.UndefinedTable, }}, }} r1, err := json.Marshal(expected[0]) require.NoError(t, err) r2, err := json.Marshal(expected[1]) require.NoError(t, err) // Setup mock postgres conn := pgtest.NewConn() defer conn.Close(t) conn.Query("begin").Reply("BEGIN"). Query(ENABLE_PGSQL_CHECK). Reply("CREATE EXTENSION"). Query(checkSchemaScript, "public"). Reply("SELECT 2", []interface{}{"f1", string(r1)}, []interface{}{"f2", string(r2)}, ). Query("rollback").Reply("ROLLBACK") // Run test result, err := LintDatabase(context.Background(), conn.MockClient(t), []string{"public"}) assert.NoError(t, err) // Validate result assert.ElementsMatch(t, expected, result) }) t.Run("supports multiple schema", func(t *testing.T) { expected := []Result{{ Function: "public.where_clause", Issues: []Issue{{ Level: AllowedLevels[0], Message: "target type is different type than source type", Statement: &Statement{ LineNumber: "32", Text: "statement block", }, Hint: "The input expression type does not have an assignment cast to the target type.", Detail: `cast "text" value to "text[]" type`, Context: `during statement block local variable "clause_arr" initialization on line 3`, SQLState: pgerrcode.DatatypeMismatch, }}, }, { Function: "private.f2", Issues: []Issue{}, }} r1, err := json.Marshal(expected[0]) require.NoError(t, err) r2, err := json.Marshal(expected[1]) require.NoError(t, err) // Setup mock postgres conn := pgtest.NewConn() defer conn.Close(t) conn.Query("begin").Reply("BEGIN"). Query(ENABLE_PGSQL_CHECK). Reply("CREATE EXTENSION"). Query(checkSchemaScript, "public"). Reply("SELECT 1", []interface{}{"where_clause", string(r1)}). Query(checkSchemaScript, "private"). Reply("SELECT 1", []interface{}{"f2", string(r2)}). Query("rollback").Reply("ROLLBACK") // Run test result, err := LintDatabase(context.Background(), conn.MockClient(t), []string{"public", "private"}) // Check error assert.NoError(t, err) assert.ElementsMatch(t, expected, result) }) t.Run("throws error on missing extension", func(t *testing.T) { // Setup mock postgres conn := pgtest.NewConn() defer conn.Close(t) conn.Query("begin").Reply("BEGIN"). Query(ENABLE_PGSQL_CHECK). ReplyError(pgerrcode.UndefinedFile, `could not open extension control file "/usr/share/postgresql/14/extension/plpgsql_check.control": No such file or directory"`). Query("rollback").Reply("ROLLBACK") // Run test _, err := LintDatabase(context.Background(), conn.MockClient(t), []string{"public"}) // Check error assert.Error(t, err) }) t.Run("throws error on malformed json", func(t *testing.T) { // Setup mock postgres conn := pgtest.NewConn() defer conn.Close(t) conn.Query("begin").Reply("BEGIN"). Query(ENABLE_PGSQL_CHECK). Reply("CREATE EXTENSION"). Query(checkSchemaScript, "public"). Reply("SELECT 1", []interface{}{"f1", "malformed"}). Query("rollback").Reply("ROLLBACK") // Run test _, err := LintDatabase(context.Background(), conn.MockClient(t), []string{"public"}) // Check error assert.Error(t, err) }) } func TestPrintResult(t *testing.T) { result := []Result{{ Function: "public.f1", Issues: []Issue{{ Level: "warning", Message: "test 1a", }, { Level: "error", Message: "test 1b", }}, }, { Function: "private.f2", Issues: []Issue{{ Level: "warning extra", Message: "test 2", }}, }} t.Run("filters warning level", func(t *testing.T) { // Run test var out bytes.Buffer filtered := filterResult(result, toEnum("warning")) assert.NoError(t, printResultJSON(filtered, &out)) // Validate output var actual []Result assert.NoError(t, json.Unmarshal(out.Bytes(), &actual)) assert.ElementsMatch(t, result, actual) }) t.Run("filters error level", func(t *testing.T) { // Run test var out bytes.Buffer filtered := filterResult(result, toEnum("error")) assert.NoError(t, printResultJSON(filtered, &out)) // Validate output var actual []Result assert.NoError(t, json.Unmarshal(out.Bytes(), &actual)) assert.ElementsMatch(t, []Result{{ Function: result[0].Function, Issues: []Issue{result[0].Issues[1]}, }}, actual) }) t.Run("exits with non-zero status on warning", func(t *testing.T) { // Setup in-memory fs fsys := afero.NewMemMapFs() // Setup mock postgres conn := pgtest.NewConn() defer conn.Close(t) conn.Query("begin").Reply("BEGIN"). Query(ENABLE_PGSQL_CHECK). Reply("CREATE EXTENSION"). Query(checkSchemaScript, "public"). Reply("SELECT 1", []interface{}{"f1", `{"function":"22751","issues":[{"level":"warning","message":"test warning"}]}`}). Query("rollback").Reply("ROLLBACK") // Run test err := Run(context.Background(), []string{"public"}, "warning", "warning", dbConfig, fsys, conn.Intercept) // Check error assert.ErrorContains(t, err, "fail-on is set to warning, non-zero exit") }) t.Run("exits with non-zero status on error", func(t *testing.T) { // Setup in-memory fs fsys := afero.NewMemMapFs() // Setup mock postgres conn := pgtest.NewConn() defer conn.Close(t) conn.Query("begin").Reply("BEGIN"). Query(ENABLE_PGSQL_CHECK). Reply("CREATE EXTENSION"). Query(checkSchemaScript, "public"). Reply("SELECT 1", []interface{}{"f1", `{"function":"22751","issues":[{"level":"error","message":"test error"}]}`}). Query("rollback").Reply("ROLLBACK") // Run test err := Run(context.Background(), []string{"public"}, "warning", "error", dbConfig, fsys, conn.Intercept) // Check error assert.ErrorContains(t, err, "fail-on is set to error, non-zero exit") }) t.Run("does not exit with non-zero status when fail-on is none", func(t *testing.T) { // Setup in-memory fs fsys := afero.NewMemMapFs() // Setup mock postgres conn := pgtest.NewConn() defer conn.Close(t) conn.Query("begin").Reply("BEGIN"). Query(ENABLE_PGSQL_CHECK). Reply("CREATE EXTENSION"). Query(checkSchemaScript, "public"). Reply("SELECT 1", []interface{}{"f1", `{"function":"22751","issues":[{"level":"error","message":"test error"}]}`}). Query("rollback").Reply("ROLLBACK") // Run test err := Run(context.Background(), []string{"public"}, "warning", "none", dbConfig, fsys, conn.Intercept) // Check error assert.NoError(t, err) }) }