diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml deleted file mode 100644 index 854f1f2..0000000 --- a/.github/workflows/audit.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Lint, test, vet - -on: - push: - pull_request: - -jobs: - audit: - name: Audit - timeout-minutes: 20 - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: 1.22 - - - name: Install dependencies - run: | - go mod download - go install golang.org/x/lint/golint@latest - - - name: Lint, test and vet - run: | - golint ./... - go test -v ./... - go vet ./... \ No newline at end of file diff --git a/.github/workflows/lint-test-vet.yml b/.github/workflows/lint-test-vet.yml new file mode 100644 index 0000000..4981f25 --- /dev/null +++ b/.github/workflows/lint-test-vet.yml @@ -0,0 +1,25 @@ +name: Lint, test, vet + +on: push + +jobs: + checks: + timeout-minutes: 5 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4.2.2 + + - uses: actions/setup-go@v5.1.0 + with: + go-version: 1.23.3 + + - name: Lint + uses: golangci/golangci-lint-action@v6.1.1 + with: + version: latest + + - name: Format check + run: go fmt ./... + + - name: Static analysis + run: go vet ./... diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..bd09eb1 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,17 @@ +linters: + enable: + - govet # correctness + - errcheck # error handling + - staticcheck # static analysis + - gosec # security + - revive # best practices + +linters-settings: + gosec: + excludes: + - G104 # disregard errors not requiring explicit handling + - G204 # allow subprocess launching with validated config inputs + +run: + timeout: 5m + tests: true \ No newline at end of file diff --git a/Makefile b/Makefile index 4276f80..f2b469d 100644 --- a/Makefile +++ b/Makefile @@ -21,6 +21,12 @@ live: air .PHONY: live +lint: + golangci-lint run + +lintfix: + golangci-lint run --fix + # ------------ # audit # ------------ diff --git a/cmd/server/main.go b/cmd/server/main.go index 5007b07..3e8a012 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -26,8 +26,6 @@ var ( buildTime string ) -var startTime = time.Now() - func main() { cfg := config.NewConfig(commitSha) diff --git a/docs/develop.md b/docs/develop.md index b24af16..0671e7d 100644 --- a/docs/develop.md +++ b/docs/develop.md @@ -1,16 +1,16 @@ # Development -Install Go 1.22.5: +Install Go 1.23.3: ```sh -brew install go@1.22.5 +brew install go@1.23.3 ``` Install Go tooling: ```sh go install gotest.tools/gotestsum@latest -go install golang.org/x/lint/golint@latest +go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest go install github.com/air-verse/air@latest go install -tags 'sqlite3' github.com/golang-migrate/migrate/v4/cmd/migrate@latest ``` @@ -27,7 +27,14 @@ Create an alias: echo "alias s.go='cd $(pwd)'" >> ~/.zshrc && source ~/.zshrc ``` -Refer to the [Makefile](../Makefile): +Create a DB and run migrations: + +```sh +make db/create +make db/mig/up +``` + +For more commands, refer to the [Makefile](../Makefile): ```sh make help diff --git a/go.mod b/go.mod index f8e33bc..58874fc 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/ivov/n8n-shortlink -go 1.22.5 +go 1.23.3 require ( github.com/felixge/httpsnoop v1.0.4 diff --git a/internal/api/api_test.go b/internal/api/api_test.go index 67e5f6e..874a09d 100644 --- a/internal/api/api_test.go +++ b/internal/api/api_test.go @@ -83,7 +83,7 @@ func TestAPI(t *testing.T) { } noFollowRedirectClient := http.Client{ - CheckRedirect: func(req *http.Request, via []*http.Request) error { + CheckRedirect: func(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse // do not follow redirect so we can inspect it }, } diff --git a/internal/api/handle_get_health.go b/internal/api/handle_get_health.go index 181308a..8d0bda1 100644 --- a/internal/api/handle_get_health.go +++ b/internal/api/handle_get_health.go @@ -6,7 +6,7 @@ import ( ) // HandleGetHealth returns the health status of the API. -func (api *API) HandleGetHealth(w http.ResponseWriter, r *http.Request) { +func (api *API) HandleGetHealth(w http.ResponseWriter, _ *http.Request) { health := fmt.Sprintf( `{"status": "ok", "environment": %q, "version": %q}`, api.Config.Env, @@ -14,5 +14,8 @@ func (api *API) HandleGetHealth(w http.ResponseWriter, r *http.Request) { ) w.Header().Set("Content-Type", "application/json") - w.Write([]byte(health)) + + if _, err := w.Write([]byte(health)); err != nil { + api.Logger.Error(err) + } } diff --git a/internal/api/handle_get_metrics.go b/internal/api/handle_get_metrics.go index 85b338b..1afaafb 100644 --- a/internal/api/handle_get_metrics.go +++ b/internal/api/handle_get_metrics.go @@ -47,12 +47,6 @@ var ( ) ) -var ( - lastTotalRequestsReceived float64 - lastTotalResponsesSent float64 - lastTotalProcessingTimeMs float64 -) - func init() { prometheus.MustRegister(totalRequestsReceived) prometheus.MustRegister(totalResponsesSent) diff --git a/internal/api/handle_get_protected_{slug}.go b/internal/api/handle_get_protected_{slug}.go index dd3f16f..dd0b165 100644 --- a/internal/api/handle_get_protected_{slug}.go +++ b/internal/api/handle_get_protected_{slug}.go @@ -22,20 +22,20 @@ func (api *API) HandleGetProtectedSlug(w http.ResponseWriter, r *http.Request, s } if !strings.HasPrefix(authHeader, "Basic ") { - api.Unauthorized(errors.ErrAuthHeaderMalformed, "MALFORMED_AUTHORIZATION_HEADER", w) + api.Unauthorized(errors.ErrAuthHeaderMalformed, w) return } encodedPassword := strings.TrimPrefix(authHeader, "Basic ") decodedBytes, err := base64.StdEncoding.DecodeString(encodedPassword) if err != nil { - api.Unauthorized(errors.ErrAuthHeaderMalformed, "MALFORMED_AUTHORIZATION_HEADER", w) + api.Unauthorized(errors.ErrAuthHeaderMalformed, w) return } decodedPassword := string(decodedBytes) if !api.ShortlinkService.VerifyPassword(shortlink.Password, decodedPassword) { - api.Unauthorized(errors.ErrPasswordInvalid, "INVALID_PASSWORD", w) + api.Unauthorized(errors.ErrPasswordInvalid, w) return } @@ -49,10 +49,15 @@ func (api *API) HandleGetProtectedSlug(w http.ResponseWriter, r *http.Request, s switch shortlink.Kind { case "workflow": w.Header().Set("Content-Type", "application/json") - w.Write([]byte(shortlink.Content)) + if _, err := w.Write([]byte(shortlink.Content)); err != nil { + api.Logger.Error(err) + } case "url": w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{"url": shortlink.Content}) + if err := json.NewEncoder(w).Encode(map[string]string{"url": shortlink.Content}); err != nil { + api.Logger.Error(err) + api.InternalServerError(err, w) + } default: api.BadRequest(errors.ErrKindUnsupported, w) } diff --git a/internal/api/handle_get_{slug}.go b/internal/api/handle_get_{slug}.go index ddf71b6..65b6203 100644 --- a/internal/api/handle_get_{slug}.go +++ b/internal/api/handle_get_{slug}.go @@ -61,7 +61,9 @@ func (api *API) HandleGetSlug(w http.ResponseWriter, r *http.Request) { } w.Header().Set("Content-Type", "application/json") - w.Write([]byte(shortlink.Content)) + if _, err := w.Write([]byte(shortlink.Content)); err != nil { + api.Logger.Error(err) + } case "url": http.Redirect(w, r, shortlink.Content, http.StatusMovedPermanently) default: diff --git a/internal/api/responses.go b/internal/api/responses.go index 57e0b27..19d35eb 100644 --- a/internal/api/responses.go +++ b/internal/api/responses.go @@ -17,7 +17,9 @@ func (api *API) jsonResponse(w http.ResponseWriter, status int, payload interfac w.WriteHeader(status) w.Header().Set("Content-Type", "application/json") - w.Write(json) + if _, err := w.Write(json); err != nil { + api.Logger.Error(err) + } } // CreatedSuccesfully responds with a 201. @@ -88,7 +90,7 @@ func (api *API) RateLimitExceeded(w http.ResponseWriter, ip string) { } // Unauthorized responds with a 401. -func (api *API) Unauthorized(err error, code string, w http.ResponseWriter) { +func (api *API) Unauthorized(err error, w http.ResponseWriter) { payload := ErrorResponse{ Error: ErrorField{ Message: "Missing valid authentication credentials.",