refactor: use pinned golangci-lint Docker image for linting (#55)
All checks were successful
check / check (push) Successful in 5s
All checks were successful
check / check (push) Successful in 5s
Closes [issue #50](#50) ## Summary Refactors the Dockerfile to use a separate lint stage with a pinned golangci-lint Docker image, following the pattern used by [sneak/pixa](https://git.eeqj.de/sneak/pixa). This replaces the previous approach of installing golangci-lint via curl in the builder stage. ## Changes ### Dockerfile - **New `lint` stage** using `golangci/golangci-lint:v2.11.3` (Debian-based, pinned by sha256 digest) as a separate build stage - **Builder stage** depends on lint via `COPY --from=lint /src/go.sum /dev/null` — build won't proceed unless linting passes - **Go bumped** from 1.24 to 1.26.1 (`golang:1.26.1-bookworm`, pinned by sha256) - **golangci-lint bumped** from v1.64.8 to v2.11.3 - All three Docker images (golangci-lint, golang, alpine) pinned by sha256 digest - Debian-based golangci-lint image used (not Alpine) because mattn/go-sqlite3 CGO does not compile on musl (off64_t) ### Linter Config (.golangci.yml) - Migrated from v1 to v2 format (`version: "2"` added) - Removed linters no longer available in v2: `gofmt` (handled by `make fmt-check`), `gosimple` (merged into `staticcheck`), `typecheck` (always-on in v2) - Same set of linters enabled — no rules weakened ### Code Fixes (all lint issues from v2 upgrade) - Added package comments to all packages - Added doc comments to all exported types, functions, and methods - Fixed unchecked errors flagged by `errcheck` (sqlDB.Close, os.Setenv in tests, resp.Body.Close, fmt.Fprint) - Fixed unused parameters flagged by `revive` (renamed to `_`) - Fixed `gosec` G120 warnings: added `http.MaxBytesReader` before `r.ParseForm()` calls - Fixed `staticcheck` QF1012: replaced `WriteString(fmt.Sprintf(...))` with `fmt.Fprintf` - Fixed `staticcheck` QF1003: converted if/else chain to tagged switch - Renamed `DeliveryTask` → `Task` to avoid package stutter (`delivery.Task` instead of `delivery.DeliveryTask`) - Renamed shadowed builtin `max` parameter to `upperBound` in `cryptoRandInt` - Used `t.Setenv` instead of `os.Setenv` in tests (auto-restores) ### README.md - Updated version requirements: Go 1.26+, golangci-lint v2.11+ - Updated Dockerfile description in project structure ## Verification `docker build .` passes cleanly — formatting check, linting, all tests, and build all succeed. Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de> Reviewed-on: #55 Co-authored-by: clawbot <clawbot@noreply.example.org> Co-committed-by: clawbot <clawbot@noreply.example.org>
This commit was merged in pull request #55.
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
package delivery
|
||||
package delivery_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"sneak.berlin/go/webhooker/internal/delivery"
|
||||
)
|
||||
|
||||
func TestIsBlockedIP_PrivateRanges(t *testing.T) {
|
||||
@@ -16,56 +18,52 @@ func TestIsBlockedIP_PrivateRanges(t *testing.T) {
|
||||
ip string
|
||||
blocked bool
|
||||
}{
|
||||
// Loopback
|
||||
{"loopback 127.0.0.1", "127.0.0.1", true},
|
||||
{"loopback 127.0.0.2", "127.0.0.2", true},
|
||||
{"loopback 127.255.255.255", "127.255.255.255", true},
|
||||
|
||||
// RFC 1918 - Class A
|
||||
{"10.0.0.0", "10.0.0.0", true},
|
||||
{"10.0.0.1", "10.0.0.1", true},
|
||||
{"10.255.255.255", "10.255.255.255", true},
|
||||
|
||||
// RFC 1918 - Class B
|
||||
{"172.16.0.1", "172.16.0.1", true},
|
||||
{"172.31.255.255", "172.31.255.255", true},
|
||||
{"172.15.255.255", "172.15.255.255", false},
|
||||
{"172.32.0.0", "172.32.0.0", false},
|
||||
|
||||
// RFC 1918 - Class C
|
||||
{"192.168.0.1", "192.168.0.1", true},
|
||||
{"192.168.255.255", "192.168.255.255", true},
|
||||
|
||||
// Link-local / cloud metadata
|
||||
{"169.254.0.1", "169.254.0.1", true},
|
||||
{"169.254.169.254", "169.254.169.254", true},
|
||||
|
||||
// Public IPs (should NOT be blocked)
|
||||
{"8.8.8.8", "8.8.8.8", false},
|
||||
{"1.1.1.1", "1.1.1.1", false},
|
||||
{"93.184.216.34", "93.184.216.34", false},
|
||||
|
||||
// IPv6 loopback
|
||||
{"::1", "::1", true},
|
||||
|
||||
// IPv6 unique local
|
||||
{"fd00::1", "fd00::1", true},
|
||||
{"fc00::1", "fc00::1", true},
|
||||
|
||||
// IPv6 link-local
|
||||
{"fe80::1", "fe80::1", true},
|
||||
|
||||
// IPv6 public (should NOT be blocked)
|
||||
{"2607:f8b0:4004:800::200e", "2607:f8b0:4004:800::200e", false},
|
||||
{
|
||||
"2607:f8b0:4004:800::200e",
|
||||
"2607:f8b0:4004:800::200e",
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ip := net.ParseIP(tt.ip)
|
||||
require.NotNil(t, ip, "failed to parse IP %s", tt.ip)
|
||||
assert.Equal(t, tt.blocked, isBlockedIP(ip),
|
||||
"isBlockedIP(%s) = %v, want %v", tt.ip, isBlockedIP(ip), tt.blocked)
|
||||
|
||||
require.NotNil(t, ip,
|
||||
"failed to parse IP %s", tt.ip,
|
||||
)
|
||||
|
||||
assert.Equal(t,
|
||||
tt.blocked,
|
||||
delivery.ExportIsBlockedIP(ip),
|
||||
"isBlockedIP(%s) = %v, want %v",
|
||||
tt.ip,
|
||||
delivery.ExportIsBlockedIP(ip),
|
||||
tt.blocked,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -89,8 +87,14 @@ func TestValidateTargetURL_Blocked(t *testing.T) {
|
||||
for _, u := range blockedURLs {
|
||||
t.Run(u, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
err := ValidateTargetURL(u)
|
||||
assert.Error(t, err, "URL %s should be blocked", u)
|
||||
|
||||
err := delivery.ValidateTargetURL(
|
||||
context.Background(), u,
|
||||
)
|
||||
|
||||
assert.Error(t, err,
|
||||
"URL %s should be blocked", u,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -98,7 +102,6 @@ func TestValidateTargetURL_Blocked(t *testing.T) {
|
||||
func TestValidateTargetURL_Allowed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// These are public IPs and should be allowed
|
||||
allowedURLs := []string{
|
||||
"https://example.com/hook",
|
||||
"http://93.184.216.34/webhook",
|
||||
@@ -108,35 +111,62 @@ func TestValidateTargetURL_Allowed(t *testing.T) {
|
||||
for _, u := range allowedURLs {
|
||||
t.Run(u, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
err := ValidateTargetURL(u)
|
||||
assert.NoError(t, err, "URL %s should be allowed", u)
|
||||
|
||||
err := delivery.ValidateTargetURL(
|
||||
context.Background(), u,
|
||||
)
|
||||
|
||||
assert.NoError(t, err,
|
||||
"URL %s should be allowed", u,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateTargetURL_InvalidScheme(t *testing.T) {
|
||||
t.Parallel()
|
||||
err := ValidateTargetURL("ftp://example.com/hook")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "unsupported URL scheme")
|
||||
|
||||
err := delivery.ValidateTargetURL(
|
||||
context.Background(), "ftp://example.com/hook",
|
||||
)
|
||||
|
||||
require.Error(t, err)
|
||||
|
||||
assert.Contains(t, err.Error(),
|
||||
"unsupported URL scheme",
|
||||
)
|
||||
}
|
||||
|
||||
func TestValidateTargetURL_EmptyHost(t *testing.T) {
|
||||
t.Parallel()
|
||||
err := ValidateTargetURL("http:///path")
|
||||
|
||||
err := delivery.ValidateTargetURL(
|
||||
context.Background(), "http:///path",
|
||||
)
|
||||
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestValidateTargetURL_InvalidURL(t *testing.T) {
|
||||
t.Parallel()
|
||||
err := ValidateTargetURL("://invalid")
|
||||
|
||||
err := delivery.ValidateTargetURL(
|
||||
context.Background(), "://invalid",
|
||||
)
|
||||
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestBlockedNetworks_Initialized(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.NotEmpty(t, blockedNetworks, "blockedNetworks should be initialized")
|
||||
// Should have at least the main RFC 1918 + loopback + link-local ranges
|
||||
assert.GreaterOrEqual(t, len(blockedNetworks), 8,
|
||||
"should have at least 8 blocked network ranges")
|
||||
|
||||
nets := delivery.ExportBlockedNetworks()
|
||||
|
||||
assert.NotEmpty(t, nets,
|
||||
"blockedNetworks should be initialized",
|
||||
)
|
||||
|
||||
assert.GreaterOrEqual(t, len(nets), 8,
|
||||
"should have at least 8 blocked network ranges",
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user