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 }