diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..2aa0645 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,102 @@ +workflow: + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' + when: never + # NO DEPLOYMENT + - if: '$CI_COMMIT_TAG == null' + variables: + IMAGE_TAG: $CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA + CONTAINER_IMAGE: $CI_REGISTRY_IMAGE:$IMAGE_TAG + # STAGING/PRODUCTION - tags only + - if: '$CI_COMMIT_TAG =~ /(alpha)|(beta)|(rc)/' + variables: + IMAGE_TAG: $CI_COMMIT_TAG + CONTAINER_IMAGE: $CI_REGISTRY_IMAGE:$IMAGE_TAG + ENVIRONMENT: 'staging' + ENVIRONMENT_SHORT: 'stg' + - if: '$CI_COMMIT_TAG' + variables: + IMAGE_TAG: $CI_COMMIT_TAG + CONTAINER_IMAGE: $CI_REGISTRY_IMAGE:$IMAGE_TAG + ENVIRONMENT: 'production' + ENVIRONMENT_SHORT: 'prd' + +variables: + SRC_ROOT_PATH: ./app + +stages: + - lint + - build + - test + - release + +# ########################################### +# Lint +# ########################################### + +linter: + image: golangci/golangci-lint:v2.0.2-alpine + rules: + - changes: + - .gitlab-ci.yml + - app/* + - app/*/* + when: always + stage: lint + before_script: + - cd $SRC_ROOT_PATH + script: + - go mod tidy + - golangci-lint run --output.code-climate.path gl-code-quality-report.json --path-prefix /app/ + artifacts: + reports: + codequality: $SRC_ROOT_PATH/gl-code-quality-report.json + +build:application_test: + extends: .build:image + stage: build + variables: + BUILD_TARGET: dev + BUILD_IMAGE_DESTINATION: ${CONTAINER_IMAGE}_dev + BUILD_ARGS: "" + +# ########################################### +# Test +# ########################################### +go:tests: + image: ${CONTAINER_IMAGE}_dev + stage: test + before_script: + - cd $SRC_ROOT_PATH + - go mod download + - go install github.com/jstemmer/go-junit-report@latest + - go install github.com/boumenot/gocover-cobertura@latest + script: + - echo "Execute tests" + - go test ./... -v 2>&1 | go-junit-report > report-gotest.xml + - echo "Compute coverage" + - go test ./... -v -coverprofile=coverage.txt -covermode count + - gocover-cobertura < coverage.txt > report-coverage.xml + coverage: '/^coverage: \d+.\d+% of statements/' + artifacts: + reports: + junit: $SRC_ROOT_PATH/report-gotest.xml + coverage_report: + coverage_format: cobertura + path: $SRC_ROOT_PATH/report-coverage.xml + + +# ########################################### +# Build +# ########################################### + +build:application: + extends: .build:image + stage: release + rules: + - if: '$CI_COMMIT_TAG' + when: on_success + variables: + BUILD_TARGET: prod + BUILD_IMAGE_DESTINATION: ${CONTAINER_IMAGE} + BUILD_ARGS: "" diff --git a/app/.golangci.toml b/app/.golangci.toml new file mode 100644 index 0000000..79b7672 --- /dev/null +++ b/app/.golangci.toml @@ -0,0 +1,5 @@ +version = "2" + +[[linters.exclusions.rules]] +linters = [ "errcheck" ] +source = "^\\s*defer\\s+" diff --git a/app/Database/os.go b/app/Database/os.go index 2e0c629..0216bc6 100644 --- a/app/Database/os.go +++ b/app/Database/os.go @@ -85,7 +85,7 @@ func GetVersionsByDistributionList(db *sql.DB, d string) ([]null.String, error) } func checkIfOsExists(os OS, db *sql.DB) bool { - row := db.QueryRow("Select distribution, version from dashboard_os where distribution = ? and version = ?", os.Distribution, os.Version) + row := db.QueryRow("SELECT distribution, version FROM dashboard_os WHERE distribution = ? AND version = ?", os.Distribution, os.Version) err := row.Scan(&os.Distribution, &os.Version) return !errors.Is(err, sql.ErrNoRows) } diff --git a/app/Database/utils.go b/app/Database/utils.go index ff8de44..3397392 100644 --- a/app/Database/utils.go +++ b/app/Database/utils.go @@ -6,10 +6,9 @@ import ( "os" "github.com/go-sql-driver/mysql" - _ "github.com/go-sql-driver/mysql" ) -func GetDatabaseConnection() *sql.DB { +var GetDatabaseConnection = func() *sql.DB { cfg := mysql.Config{ User: os.Getenv("DATABASE_USER"), Passwd: os.Getenv("DATABASE_PASSWORD"), diff --git a/app/Http/os_test.go b/app/Http/os_test.go index cdd3be1..9e657aa 100644 --- a/app/Http/os_test.go +++ b/app/Http/os_test.go @@ -1,37 +1,230 @@ package http import ( + "database/sql" + "encoding/json" + "github.com/DATA-DOG/go-sqlmock" + "github.com/gorilla/mux" + "gopkg.in/guregu/null.v4" + db "infra-dashboard/Database" + "log" "net/http" "net/http/httptest" + "reflect" + "regexp" "testing" ) func TestGetOS(t *testing.T) { - // Create a request to pass to our handler. We don't have any query parameters for now, so we'll - // pass 'nil' as the third parameter. + // Créer une connexion à la base de données simulée + dbConn, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + defer dbConn.Close() + + // Simuler la réponse de la base de données + mockOSList := []db.OS{ + {ID: 1, Distribution: null.StringFrom("Ubuntu"), Version: null.StringFrom("22.04"), EndOfSupport: null.StringFrom("2026-04-01")}, + {ID: 2, Distribution: null.StringFrom("Debian"), Version: null.StringFrom("12"), EndOfSupport: null.StringFrom("2028-04-01")}, + } + + rows := sqlmock.NewRows([]string{"id", "distribution", "version", "end_of_support"}). + AddRow(1, "Ubuntu", "22.04", "2026-04-01"). + AddRow(2, "Debian", "12", "2028-04-01") + + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM dashboard_os")).WillReturnRows(rows) + + // Remplacer la fonction GetDatabaseConnection par une version mockée + originalGetDatabaseConnection := db.GetDatabaseConnection + db.GetDatabaseConnection = func() *sql.DB { + return dbConn + } + defer func() { db.GetDatabaseConnection = originalGetDatabaseConnection }() + + // Créer une requête HTTP req, err := http.NewRequest("GET", "/os", nil) if err != nil { t.Fatal(err) } - // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. + // Créer un ResponseRecorder pour enregistrer la réponse rr := httptest.NewRecorder() handler := http.HandlerFunc(GetOS) - // Our handlers satisfy http.Handler, so we can call their ServeHTTP method - // directly and pass in our Request and ResponseRecorder. + // Appeler la fonction GetOS handler.ServeHTTP(rr, req) - // Check the status code is what we expect. + // Vérifier le statut de la réponse if status := rr.Code; status != http.StatusOK { t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) } - // Check the response body is what we expect. - expected := `{"alive": true}` - if rr.Body.String() != expected { + // Vérifier le type de contenu de la réponse + expectedContentType := "application/json" + if ct := rr.Header().Get("Content-Type"); ct != expectedContentType { + t.Errorf("handler returned wrong content type: got %v want %v", + ct, expectedContentType) + } + + // Vérifier le corps de la réponse + var responseOSList []db.OS + err = json.NewDecoder(rr.Body).Decode(&responseOSList) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(responseOSList, mockOSList) { t.Errorf("handler returned unexpected body: got %v want %v", - rr.Body.String(), expected) + responseOSList, mockOSList) + } + + // Vérifier que toutes les attentes ont été satisfaites + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } +} + +func TestGetOSbyID(t *testing.T) { + // Créer une connexion à la base de données simulée + dbConn, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + defer dbConn.Close() + + // Simuler la réponse de la base de données + mockOS := db.OS{ + ID: 1, Distribution: null.StringFrom("Ubuntu"), Version: null.StringFrom("22.04"), EndOfSupport: null.StringFrom("2026-04-01"), + } + + rows := sqlmock.NewRows([]string{"id", "distribution", "version", "end_of_support"}). + AddRow(1, "Ubuntu", "22.04", "2026-04-01") + + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM dashboard_os where id = ?")).WithArgs(sqlmock.AnyArg()).WillReturnRows(rows) + + // Remplacer la fonction GetDatabaseConnection par une version mockée + originalGetDatabaseConnection := db.GetDatabaseConnection + db.GetDatabaseConnection = func() *sql.DB { + return dbConn + } + defer func() { db.GetDatabaseConnection = originalGetDatabaseConnection }() + + // Créer une requête HTTP + req, err := http.NewRequest("GET", "/os/1", nil) + if err != nil { + t.Fatal(err) + } + + // Ajouter les paramètres de la route + req = mux.SetURLVars(req, map[string]string{"id": "1"}) + + // Créer un ResponseRecorder pour enregistrer la réponse + rr := httptest.NewRecorder() + handler := http.HandlerFunc(GetOSbyID) + + // Appeler la fonction GetOS + handler.ServeHTTP(rr, req) + + // Vérifier le statut de la réponse + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", + status, http.StatusOK) + } + + // Vérifier le type de contenu de la réponse + expectedContentType := "application/json" + if ct := rr.Header().Get("Content-Type"); ct != expectedContentType { + t.Errorf("handler returned wrong content type: got %v want %v", + ct, expectedContentType) + } + + // Vérifier le corps de la réponse + var responseOS db.OS + err = json.NewDecoder(rr.Body).Decode(&responseOS) + if err != nil { + t.Fatal("Error decoding json request", err) + } + + if !reflect.DeepEqual(responseOS, mockOS) { + t.Errorf("handler returned unexpected body: got %v want %v", + responseOS, mockOS) + } + + // Vérifier que toutes les attentes ont été satisfaites + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } +} + +func TestGetDistributionList(t *testing.T) { + // Créer une connexion à la base de données simulée + dbConn, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + defer dbConn.Close() + + // Simuler la réponse de la base de données + mockDistributionList := []null.String{} + mockDistributionList = append(mockDistributionList, null.StringFrom("Ubuntu")) + mockDistributionList = append(mockDistributionList, null.StringFrom("Debian")) + + rows := sqlmock.NewRows([]string{"distribution"}). + AddRow("Ubuntu"). + AddRow("Debian") + + mock.ExpectQuery(regexp.QuoteMeta("SELECT DISTINCT distribution FROM dashboard_os")).WillReturnRows(rows) + + // Remplacer la fonction GetDatabaseConnection par une version mockée + originalGetDatabaseConnection := db.GetDatabaseConnection + db.GetDatabaseConnection = func() *sql.DB { + return dbConn + } + defer func() { db.GetDatabaseConnection = originalGetDatabaseConnection }() + + // Créer une requête HTTP + req, err := http.NewRequest("GET", "/os/distribution", nil) + if err != nil { + t.Fatal(err) + } + + // Créer un ResponseRecorder pour enregistrer la réponse + rr := httptest.NewRecorder() + handler := http.HandlerFunc(GetDistributionList) + + // Appeler la fonction GetOS + handler.ServeHTTP(rr, req) + + // Vérifier le statut de la réponse + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", + status, http.StatusOK) + } + + // Vérifier le type de contenu de la réponse + expectedContentType := "application/json" + if ct := rr.Header().Get("Content-Type"); ct != expectedContentType { + t.Errorf("handler returned wrong content type: got %v want %v", + ct, expectedContentType) + } + + // Vérifier le corps de la réponse + var responseOSList []null.String + err = json.NewDecoder(rr.Body).Decode(&responseOSList) + log.Println(responseOSList) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(responseOSList, mockDistributionList) { + t.Errorf("handler returned unexpected body: got %v want %v", + responseOSList, mockDistributionList) + } + + // Vérifier que toutes les attentes ont été satisfaites + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) } } diff --git a/app/Http/utils.go b/app/Http/utils.go index 8c47122..d6abfa3 100644 --- a/app/Http/utils.go +++ b/app/Http/utils.go @@ -12,7 +12,10 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) { t := time.Now() logRequest(t, r, 418) w.WriteHeader(http.StatusTeapot) - fmt.Fprint(w, "Bip Boop I'm a teapot") + _, err := fmt.Fprint(w, "Bip Boop I'm a teapot") + if err != nil { + log.Println("Something went wrong with the teapot") + } } func HealthHandler(w http.ResponseWriter, _ *http.Request) { diff --git a/app/Http/utils_test.go b/app/Http/utils_test.go new file mode 100644 index 0000000..2da12ce --- /dev/null +++ b/app/Http/utils_test.go @@ -0,0 +1,59 @@ +package http + +import ( + "io" + "net/http" + "net/http/httptest" + "testing" +) + +func TestHealthHandler(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/healthcheck", nil) + w := httptest.NewRecorder() + HealthHandler(w, req) + res := w.Result() + defer res.Body.Close() + + data, err := io.ReadAll(res.Body) + if err != nil { + t.Errorf("Expected error to be nil, got %v", err) + } + if res.StatusCode != http.StatusOK { + t.Errorf("Expected status code 200, got %v", res.StatusCode) + } + if string(data) != "OK" { + t.Errorf("Expected 'OK', got %v", string(data)) + } +} + +func TestRequestHandler(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/", nil) + w := httptest.NewRecorder() + RequestHandler(w, req) + res := w.Result() + defer res.Body.Close() + + _, err := io.ReadAll(res.Body) + if err != nil { + t.Errorf("Expected error to be nil, got %v", err) + } + if res.StatusCode != http.StatusTeapot { + t.Errorf("Expected status code 418, got %v", res.StatusCode) + } +} + +func TestNotFound(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/metrics", nil) + w := httptest.NewRecorder() + NotFound(w, req) + res := w.Result() + defer res.Body.Close() + + _, err := io.ReadAll(res.Body) + if err != nil { + t.Errorf("Expected error to be nil, got %v", err) + } + if res.StatusCode != http.StatusNotFound { + t.Errorf("Expected status code 404, got %v", res.StatusCode) + } +} diff --git a/app/go.mod b/app/go.mod index dc0d0f7..ef68d01 100644 --- a/app/go.mod +++ b/app/go.mod @@ -1,32 +1,12 @@ module infra-dashboard -go 1.23.1 - -toolchain go1.23.3 +go 1.24.2 require ( - github.com/alexflint/go-arg v1.5.1 + github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/go-sql-driver/mysql v1.8.1 github.com/gorilla/mux v1.8.1 - github.com/joho/godotenv v1.5.1 gopkg.in/guregu/null.v4 v4.0.0 ) -require ( - filippo.io/edwards25519 v1.1.0 // indirect - github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect - github.com/alexflint/go-scalar v1.2.0 // indirect - github.com/google/go-cmp v0.6.0 // indirect - golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 // indirect - golang.org/x/mod v0.22.0 // indirect - golang.org/x/sync v0.9.0 // indirect - golang.org/x/sys v0.27.0 // indirect - golang.org/x/telemetry v0.0.0-20241106142447-58a1122356f5 // indirect - golang.org/x/text v0.20.0 // indirect - golang.org/x/tools v0.27.1-0.20241219162658-575221bfbda3 // indirect - golang.org/x/tools/gopls v0.17.1 // indirect - golang.org/x/vuln v1.0.4 // indirect - honnef.co/go/tools v0.5.1 // indirect - mvdan.cc/gofumpt v0.7.0 // indirect - mvdan.cc/xurls/v2 v2.5.0 // indirect -) +require filippo.io/edwards25519 v1.1.0 // indirect diff --git a/app/go.sum b/app/go.sum index 7ba4041..b39f9ac 100644 --- a/app/go.sum +++ b/app/go.sum @@ -1,51 +1,11 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= -github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/alexflint/go-arg v1.5.1 h1:nBuWUCpuRy0snAG+uIJ6N0UvYxpxA0/ghA/AaHxlT8Y= -github.com/alexflint/go-arg v1.5.1/go.mod h1:A7vTJzvjoaSTypg4biM5uYNTkJ27SkNTArtYXnlqVO8= -github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw= -github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= +github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 h1:1P7xPZEwZMoBoz0Yze5Nx2/4pxj6nw9ZqHWXqP0iRgQ= -golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= -golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= -golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= -golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/telemetry v0.0.0-20241106142447-58a1122356f5 h1:TCDqnvbBsFapViksHcHySl/sW4+rTGNIAoJJesHRuMM= -golang.org/x/telemetry v0.0.0-20241106142447-58a1122356f5/go.mod h1:8nZWdGp9pq73ZI//QJyckMQab3yq7hoWi7SI0UIusVI= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= -golang.org/x/tools v0.27.1-0.20241219162658-575221bfbda3 h1:kgwdasJRsdDWYgWcEgMF424DiXwwXHSb3V8xVTi//i8= -golang.org/x/tools v0.27.1-0.20241219162658-575221bfbda3/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= -golang.org/x/tools/gopls v0.17.1 h1:Mt/DSfnnSe3dyf6MH/dZZ0iww+viHNhAFc4rEYDiOAw= -golang.org/x/tools/gopls v0.17.1/go.mod h1:niea3AFBDJrqLpvDQ8vjmtzjGcT44nAoYm/vd34SaH4= -golang.org/x/vuln v1.0.4 h1:SP0mPeg2PmGCu03V+61EcQiOjmpri2XijexKdzv8Z1I= -golang.org/x/vuln v1.0.4/go.mod h1:NbJdUQhX8jY++FtuhrXs2Eyx0yePo9pF7nPlIjo9aaQ= +github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= gopkg.in/guregu/null.v4 v4.0.0 h1:1Wm3S1WEA2I26Kq+6vcW+w0gcDo44YKYD7YIEJNHDjg= gopkg.in/guregu/null.v4 v4.0.0/go.mod h1:YoQhUrADuG3i9WqesrCmpNRwm1ypAgSHYqoOcTu/JrI= -gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= -gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.5.1 h1:4bH5o3b5ZULQ4UrBmP+63W9r7qIkqJClEA9ko5YKx+I= -honnef.co/go/tools v0.5.1/go.mod h1:e9irvo83WDG9/irijV44wr3tbhcFeRnfpVlRqVwpzMs= -mvdan.cc/gofumpt v0.7.0 h1:bg91ttqXmi9y2xawvkuMXyvAA/1ZGJqYAEGjXuP0JXU= -mvdan.cc/gofumpt v0.7.0/go.mod h1:txVFJy/Sc/mvaycET54pV8SW8gWxTlUuGHVEcncmNUo= -mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8= -mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE=