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 }