Compare commits
1
Commits
next
..
74004178ce
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74004178ce |
+3
-8
@@ -72,13 +72,8 @@
|
|||||||
**/*.sublime-*
|
**/*.sublime-*
|
||||||
|
|
||||||
# This repository's own host-built artifacts: the binary `make build`
|
# This repository's own host-built artifacts: the binary `make build`
|
||||||
# writes, and the Alpine.js file `make assets` extracts from 3p/ (the
|
# writes, the Alpine.js file `make assets` extracts from 3p/ (the build
|
||||||
# build extracts its own).
|
# extracts its own), and the databases of a `DATA_DIR=./data make dev` run.
|
||||||
/bin
|
/bin
|
||||||
/static/js/alpine.min.js
|
/static/js/alpine.min.js
|
||||||
|
/data
|
||||||
# SQLite databases, which hold the session key and webhook payloads, at
|
|
||||||
# any depth.
|
|
||||||
**/*.db
|
|
||||||
**/*.sqlite
|
|
||||||
**/*.sqlite3
|
|
||||||
|
|||||||
@@ -10,6 +10,3 @@ insert_final_newline = true
|
|||||||
|
|
||||||
[Makefile]
|
[Makefile]
|
||||||
indent_style = tab
|
indent_style = tab
|
||||||
|
|
||||||
[*.go]
|
|
||||||
indent_style = tab
|
|
||||||
|
|||||||
-36
@@ -45,39 +45,3 @@ node_modules/
|
|||||||
[iI][dD]_[eE][cC][dD][sS][aA]_[sS][kK]
|
[iI][dD]_[eE][cC][dD][sS][aA]_[sS][kK]
|
||||||
[iI][dD]_[eE][dD]25519
|
[iI][dD]_[eE][dD]25519
|
||||||
[iI][dD]_[eE][dD]25519_[sS][kK]
|
[iI][dD]_[eE][dD]25519_[sS][kK]
|
||||||
|
|
||||||
# This repository's own entries, after the shared content above.
|
|
||||||
|
|
||||||
# Binaries
|
|
||||||
*.exe
|
|
||||||
*.dll
|
|
||||||
*.so
|
|
||||||
*.dylib
|
|
||||||
bin/
|
|
||||||
/webhooker
|
|
||||||
|
|
||||||
# Test binary, built with `go test -c`
|
|
||||||
*.test
|
|
||||||
|
|
||||||
# Output of the go coverage tool
|
|
||||||
*.out
|
|
||||||
|
|
||||||
# Go vendor directory
|
|
||||||
vendor/
|
|
||||||
|
|
||||||
# Data directory (SQLite databases)
|
|
||||||
data/
|
|
||||||
*.db
|
|
||||||
*.sqlite
|
|
||||||
*.sqlite3
|
|
||||||
|
|
||||||
# Log files
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# Temporary files
|
|
||||||
tmp/
|
|
||||||
temp/
|
|
||||||
|
|
||||||
# Alpine.js, extracted by `make assets` from its tarball in 3p/, which is
|
|
||||||
# what is committed.
|
|
||||||
/static/js/alpine.min.js
|
|
||||||
|
|||||||
@@ -61,12 +61,6 @@ linters:
|
|||||||
desc: >-
|
desc: >-
|
||||||
Test-support code belongs in test files and in packages whose
|
Test-support code belongs in test files and in packages whose
|
||||||
directory name ends in test, not in the shipped binary.
|
directory name ends in test, not in the shipped binary.
|
||||||
- pkg: sneak.berlin/go/webhooker/internal/config/configtest
|
|
||||||
desc: test support; a file that is not a test must not import it
|
|
||||||
- pkg: sneak.berlin/go/webhooker/internal/database/databasetest
|
|
||||||
desc: test support; a file that is not a test must not import it
|
|
||||||
- pkg: sneak.berlin/go/webhooker/internal/middleware/middlewaretest
|
|
||||||
desc: test support; a file that is not a test must not import it
|
|
||||||
# Only decisions already recorded in the Go package defaults are
|
# Only decisions already recorded in the Go package defaults are
|
||||||
# listed here. Every entry matches the module path exactly.
|
# listed here. Every entry matches the module path exactly.
|
||||||
gomodguard_v2:
|
gomodguard_v2:
|
||||||
|
|||||||
+1
-8
@@ -147,16 +147,9 @@ RUN script/assets
|
|||||||
# shown in full, so there is nothing to rerun. The step fails after the rerun
|
# shown in full, so there is nothing to rerun. The step fails after the rerun
|
||||||
# whatever its result: the first run already showed the suite is broken.
|
# whatever its result: the first run already showed the suite is broken.
|
||||||
#
|
#
|
||||||
# TMPDIR, where the tests keep their SQLite databases, is a tmpfs: SQLite
|
|
||||||
# waits for the disk at every commit, and on a busy host that waiting was
|
|
||||||
# about 40% of the slowest package's run time. GOTMPDIR keeps go's own
|
|
||||||
# build files, the test binaries among them, on disk.
|
|
||||||
#
|
|
||||||
# bash with pipefail, so that the first run's status is go test's, not tee's.
|
# bash with pipefail, so that the first run's status is go test's, not tee's.
|
||||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||||
RUN --mount=type=tmpfs,target=/tmp/tests,size=512m \
|
RUN go test -race -cover -p 4 -parallel 8 -timeout 90s ./... 2>&1 | tee /tmp/go-test.log && exit 0; \
|
||||||
export TMPDIR=/tmp/tests GOTMPDIR=/tmp; \
|
|
||||||
go test -race -cover -p 4 -parallel 8 -timeout 90s ./... 2>&1 | tee /tmp/go-test.log && exit 0; \
|
|
||||||
tests="$(awk '/^--- FAIL: / { print $3 }' /tmp/go-test.log | paste -s -d '|' -)"; \
|
tests="$(awk '/^--- FAIL: / { print $3 }' /tmp/go-test.log | paste -s -d '|' -)"; \
|
||||||
packages="$(awk '/^FAIL\t/ { print $2 }' /tmp/go-test.log)"; \
|
packages="$(awk '/^FAIL\t/ { print $2 }' /tmp/go-test.log)"; \
|
||||||
if [ -n "$tests" ]; then \
|
if [ -n "$tests" ]; then \
|
||||||
|
|||||||
@@ -43,9 +43,8 @@ make check
|
|||||||
|
|
||||||
# Run the server from the clone. DATA_DIR defaults to
|
# Run the server from the clone. DATA_DIR defaults to
|
||||||
# /var/lib/webhooker in every environment, so set it (in .env or the
|
# /var/lib/webhooker in every environment, so set it (in .env or the
|
||||||
# shell) to a writable directory outside the clone: the databases hold
|
# shell) to a writable directory.
|
||||||
# the session key.
|
DATA_DIR=./data make dev
|
||||||
DATA_DIR=../webhooker-data make dev
|
|
||||||
|
|
||||||
# Build Docker image
|
# Build Docker image
|
||||||
make docker
|
make docker
|
||||||
@@ -2984,7 +2983,7 @@ webhooker/
|
|||||||
│ │ └── resetpw.go # `webhooker resetpw`: set an account's password, stopped deployments only
|
│ │ └── resetpw.go # `webhooker resetpw`: set an account's password, stopped deployments only
|
||||||
│ ├── config/
|
│ ├── config/
|
||||||
│ │ ├── config.go # Configuration loading from environment variables
|
│ │ ├── config.go # Configuration loading from environment variables
|
||||||
│ │ └── configtest/ # Test support: ClearEnv, an empty environment for one test
|
│ │ └── testing.go # ClearEnvForTest: an empty environment for one test
|
||||||
│ ├── database/
|
│ ├── database/
|
||||||
│ │ ├── base_model.go # BaseModel with UUID primary keys
|
│ │ ├── base_model.go # BaseModel with UUID primary keys
|
||||||
│ │ ├── database.go # GORM connection, migrations, admin seed
|
│ │ ├── database.go # GORM connection, migrations, admin seed
|
||||||
@@ -3001,8 +3000,8 @@ webhooker/
|
|||||||
│ │ ├── model_apikey.go # APIKey entity
|
│ │ ├── model_apikey.go # APIKey entity
|
||||||
│ │ ├── password.go # Argon2id hashing and verification
|
│ │ ├── password.go # Argon2id hashing and verification
|
||||||
│ │ ├── retention.go # Retention reaper (per-webhook event expiry)
|
│ │ ├── retention.go # Retention reaper (per-webhook event expiry)
|
||||||
│ │ ├── webhook_db_manager.go # Per-webhook DB lifecycle manager
|
│ │ ├── testing.go # NewTestDatabase: wrapper for tests, no fx lifecycle
|
||||||
│ │ └── databasetest/ # Test support: a WebhookDBManager for tests in other packages
|
│ │ └── webhook_db_manager.go # Per-webhook DB lifecycle manager
|
||||||
│ ├── datadir/
|
│ ├── datadir/
|
||||||
│ │ └── lock.go # Exclusive advisory lock on DATA_DIR (one instance)
|
│ │ └── lock.go # Exclusive advisory lock on DATA_DIR (one instance)
|
||||||
│ ├── globals/
|
│ ├── globals/
|
||||||
@@ -3063,7 +3062,7 @@ webhooker/
|
|||||||
│ │ ├── csrf.go # CSRF protection middleware (gorilla/csrf)
|
│ │ ├── csrf.go # CSRF protection middleware (gorilla/csrf)
|
||||||
│ │ ├── ratelimit.go # Per-IP rate limiting middleware (go-chi/httprate)
|
│ │ ├── ratelimit.go # Per-IP rate limiting middleware (go-chi/httprate)
|
||||||
│ │ ├── loginguard.go # Login failure counters and the Argon2id verification semaphore
|
│ │ ├── loginguard.go # Login failure counters and the Argon2id verification semaphore
|
||||||
│ │ └── middlewaretest/ # Test support: a Middleware for tests in other packages
|
│ │ └── testing.go # NewForTest: Middleware without the fx lifecycle
|
||||||
│ ├── reqtls/
|
│ ├── reqtls/
|
||||||
│ │ └── reqtls.go # IsTLS: the one TLS predicate, r.TLS or X-Forwarded-Proto
|
│ │ └── reqtls.go # IsTLS: the one TLS predicate, r.TLS or X-Forwarded-Proto
|
||||||
│ ├── server/
|
│ ├── server/
|
||||||
@@ -3071,7 +3070,8 @@ webhooker/
|
|||||||
│ │ ├── http.go # HTTP server setup with timeouts
|
│ │ ├── http.go # HTTP server setup with timeouts
|
||||||
│ │ └── routes.go # All route definitions
|
│ │ └── routes.go # All route definitions
|
||||||
│ ├── session/
|
│ ├── session/
|
||||||
│ │ └── session.go # Cookie-based session management
|
│ │ ├── session.go # Cookie-based session management
|
||||||
|
│ │ └── testing.go # NewForTest: Session without the fx lifecycle
|
||||||
│ └── versionscript/
|
│ └── versionscript/
|
||||||
│ └── doc.go # Tests for script/version and the build files that use it
|
│ └── doc.go # Tests for script/version and the build files that use it
|
||||||
├── static/
|
├── static/
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -188,9 +187,6 @@ func newApp() *fx.App {
|
|||||||
fx.Provide(
|
fx.Provide(
|
||||||
globals.New,
|
globals.New,
|
||||||
logger.New,
|
logger.New,
|
||||||
// The plain logger the session, the middleware and the
|
|
||||||
// webhook database manager take.
|
|
||||||
func(l *logger.Logger) *slog.Logger { return l.Get() },
|
|
||||||
config.New,
|
config.New,
|
||||||
database.New,
|
database.New,
|
||||||
database.NewWebhookDBManager,
|
database.NewWebhookDBManager,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"sneak.berlin/go/webhooker/internal/config/configtest"
|
"sneak.berlin/go/webhooker/internal/config"
|
||||||
"sneak.berlin/go/webhooker/internal/datadir"
|
"sneak.berlin/go/webhooker/internal/datadir"
|
||||||
"sneak.berlin/go/webhooker/internal/resetpw"
|
"sneak.berlin/go/webhooker/internal/resetpw"
|
||||||
"sneak.berlin/go/webhooker/internal/server"
|
"sneak.berlin/go/webhooker/internal/server"
|
||||||
@@ -37,7 +37,7 @@ const dockerStopGrace = 10 * time.Second
|
|||||||
// fx.New applies options before it executes invokes, so the timeout
|
// fx.New applies options before it executes invokes, so the timeout
|
||||||
// is set whether or not the graph itself can be constructed here.
|
// is set whether or not the graph itself can be constructed here.
|
||||||
func TestNewApp_StopTimeout(t *testing.T) {
|
func TestNewApp_StopTimeout(t *testing.T) {
|
||||||
configtest.ClearEnv(t)
|
config.ClearEnvForTest(t)
|
||||||
t.Setenv("DATA_DIR", t.TempDir())
|
t.Setenv("DATA_DIR", t.TempDir())
|
||||||
|
|
||||||
got := newApp().StopTimeout()
|
got := newApp().StopTimeout()
|
||||||
@@ -75,7 +75,7 @@ func freePort(t *testing.T) int {
|
|||||||
// anything is built, and the run of logger.New, which happens before
|
// anything is built, and the run of logger.New, which happens before
|
||||||
// the configuration sets the level.
|
// the configuration sets the level.
|
||||||
func TestNewApp_SendsFxEventsToTheLogger(t *testing.T) {
|
func TestNewApp_SendsFxEventsToTheLogger(t *testing.T) {
|
||||||
configtest.ClearEnv(t)
|
config.ClearEnvForTest(t)
|
||||||
t.Setenv("DATA_DIR", t.TempDir())
|
t.Setenv("DATA_DIR", t.TempDir())
|
||||||
t.Setenv("PORT", strconv.Itoa(freePort(t)))
|
t.Setenv("PORT", strconv.Itoa(freePort(t)))
|
||||||
t.Setenv("DEBUG", "true")
|
t.Setenv("DEBUG", "true")
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ require (
|
|||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
go.uber.org/fx v1.24.0
|
go.uber.org/fx v1.24.0
|
||||||
golang.org/x/crypto v0.38.0
|
golang.org/x/crypto v0.38.0
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
gorm.io/driver/sqlite v1.5.4
|
gorm.io/driver/sqlite v1.5.4
|
||||||
gorm.io/gorm v1.25.5
|
gorm.io/gorm v1.25.5
|
||||||
modernc.org/sqlite v1.28.0
|
modernc.org/sqlite v1.28.0
|
||||||
@@ -58,7 +59,6 @@ require (
|
|||||||
golang.org/x/text v0.25.0 // indirect
|
golang.org/x/text v0.25.0 // indirect
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||||
google.golang.org/protobuf v1.31.0 // indirect
|
google.golang.org/protobuf v1.31.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
lukechampine.com/uint128 v1.2.0 // indirect
|
lukechampine.com/uint128 v1.2.0 // indirect
|
||||||
modernc.org/cc/v3 v3.40.0 // indirect
|
modernc.org/cc/v3 v3.40.0 // indirect
|
||||||
modernc.org/ccgo/v3 v3.16.13 // indirect
|
modernc.org/ccgo/v3 v3.16.13 // indirect
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"go.uber.org/fx"
|
"go.uber.org/fx"
|
||||||
"go.uber.org/fx/fxtest"
|
"go.uber.org/fx/fxtest"
|
||||||
"sneak.berlin/go/webhooker/internal/config"
|
"sneak.berlin/go/webhooker/internal/config"
|
||||||
"sneak.berlin/go/webhooker/internal/config/configtest"
|
|
||||||
"sneak.berlin/go/webhooker/internal/globals"
|
"sneak.berlin/go/webhooker/internal/globals"
|
||||||
"sneak.berlin/go/webhooker/internal/logger"
|
"sneak.berlin/go/webhooker/internal/logger"
|
||||||
)
|
)
|
||||||
@@ -71,7 +70,7 @@ func TestEnvironmentConfig(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
// Cannot use t.Parallel() here because t.Setenv
|
// Cannot use t.Parallel() here because t.Setenv
|
||||||
// is incompatible with parallel subtests.
|
// is incompatible with parallel subtests.
|
||||||
configtest.ClearEnv(t)
|
config.ClearEnvForTest(t)
|
||||||
|
|
||||||
if tt.envValue != "" {
|
if tt.envValue != "" {
|
||||||
t.Setenv(
|
t.Setenv(
|
||||||
@@ -197,7 +196,7 @@ func TestRetentionSweepInterval(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
// Cannot use t.Parallel() here because t.Setenv
|
// Cannot use t.Parallel() here because t.Setenv
|
||||||
// is incompatible with parallel subtests.
|
// is incompatible with parallel subtests.
|
||||||
configtest.ClearEnv(t)
|
config.ClearEnvForTest(t)
|
||||||
t.Setenv("WEBHOOKER_ENVIRONMENT", "dev")
|
t.Setenv("WEBHOOKER_ENVIRONMENT", "dev")
|
||||||
|
|
||||||
if tt.set {
|
if tt.set {
|
||||||
@@ -336,7 +335,7 @@ func TestSessionIdleTimeout(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
// Cannot use t.Parallel() here because t.Setenv
|
// Cannot use t.Parallel() here because t.Setenv
|
||||||
// is incompatible with parallel subtests.
|
// is incompatible with parallel subtests.
|
||||||
configtest.ClearEnv(t)
|
config.ClearEnvForTest(t)
|
||||||
t.Setenv("WEBHOOKER_ENVIRONMENT", "dev")
|
t.Setenv("WEBHOOKER_ENVIRONMENT", "dev")
|
||||||
|
|
||||||
if tt.set {
|
if tt.set {
|
||||||
@@ -389,7 +388,7 @@ func TestDefaultDataDir(t *testing.T) {
|
|||||||
t.Run("env="+name, func(t *testing.T) {
|
t.Run("env="+name, func(t *testing.T) {
|
||||||
// Cannot use t.Parallel() here because t.Setenv
|
// Cannot use t.Parallel() here because t.Setenv
|
||||||
// is incompatible with parallel subtests.
|
// is incompatible with parallel subtests.
|
||||||
configtest.ClearEnv(t)
|
config.ClearEnvForTest(t)
|
||||||
|
|
||||||
if env != "" {
|
if env != "" {
|
||||||
t.Setenv("WEBHOOKER_ENVIRONMENT", env)
|
t.Setenv("WEBHOOKER_ENVIRONMENT", env)
|
||||||
@@ -434,7 +433,7 @@ func TestDataDirHelper(t *testing.T) {
|
|||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
// Cannot use t.Parallel() here because t.Setenv
|
// Cannot use t.Parallel() here because t.Setenv
|
||||||
// is incompatible with parallel subtests.
|
// is incompatible with parallel subtests.
|
||||||
configtest.ClearEnv(t)
|
config.ClearEnvForTest(t)
|
||||||
|
|
||||||
if set != "" {
|
if set != "" {
|
||||||
t.Setenv("DATA_DIR", set)
|
t.Setenv("DATA_DIR", set)
|
||||||
@@ -499,7 +498,7 @@ func TestReceiverRateLimit(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
// Cannot use t.Parallel() here because t.Setenv
|
// Cannot use t.Parallel() here because t.Setenv
|
||||||
// is incompatible with parallel subtests.
|
// is incompatible with parallel subtests.
|
||||||
configtest.ClearEnv(t)
|
config.ClearEnvForTest(t)
|
||||||
t.Setenv("WEBHOOKER_ENVIRONMENT", "dev")
|
t.Setenv("WEBHOOKER_ENVIRONMENT", "dev")
|
||||||
|
|
||||||
if tt.set {
|
if tt.set {
|
||||||
@@ -615,7 +614,7 @@ func TestTrustedProxies(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
// Cannot use t.Parallel() here because t.Setenv
|
// Cannot use t.Parallel() here because t.Setenv
|
||||||
// is incompatible with parallel subtests.
|
// is incompatible with parallel subtests.
|
||||||
configtest.ClearEnv(t)
|
config.ClearEnvForTest(t)
|
||||||
t.Setenv("WEBHOOKER_ENVIRONMENT", "dev")
|
t.Setenv("WEBHOOKER_ENVIRONMENT", "dev")
|
||||||
|
|
||||||
if tt.set {
|
if tt.set {
|
||||||
@@ -726,7 +725,7 @@ func TestAllowedEgressCIDRs(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
// Cannot use t.Parallel() here because t.Setenv
|
// Cannot use t.Parallel() here because t.Setenv
|
||||||
// is incompatible with parallel subtests.
|
// is incompatible with parallel subtests.
|
||||||
configtest.ClearEnv(t)
|
config.ClearEnvForTest(t)
|
||||||
t.Setenv("WEBHOOKER_ENVIRONMENT", "dev")
|
t.Setenv("WEBHOOKER_ENVIRONMENT", "dev")
|
||||||
|
|
||||||
if tt.set {
|
if tt.set {
|
||||||
@@ -798,7 +797,7 @@ func TestEgressAllowlistWarning(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
// Cannot use t.Parallel() here because t.Setenv
|
// Cannot use t.Parallel() here because t.Setenv
|
||||||
// is incompatible with parallel subtests.
|
// is incompatible with parallel subtests.
|
||||||
configtest.ClearEnv(t)
|
config.ClearEnvForTest(t)
|
||||||
t.Setenv("WEBHOOKER_ENVIRONMENT", config.EnvironmentDev)
|
t.Setenv("WEBHOOKER_ENVIRONMENT", config.EnvironmentDev)
|
||||||
|
|
||||||
if tt.allowed != "" {
|
if tt.allowed != "" {
|
||||||
@@ -934,7 +933,7 @@ func TestMetricsAuthConfig(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
// Cannot use t.Parallel() here because t.Setenv
|
// Cannot use t.Parallel() here because t.Setenv
|
||||||
// is incompatible with parallel subtests.
|
// is incompatible with parallel subtests.
|
||||||
configtest.ClearEnv(t)
|
config.ClearEnvForTest(t)
|
||||||
|
|
||||||
if tt.username.set {
|
if tt.username.set {
|
||||||
t.Setenv("METRICS_USERNAME", tt.username.value)
|
t.Setenv("METRICS_USERNAME", tt.username.value)
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"sneak.berlin/go/webhooker/internal/config"
|
"sneak.berlin/go/webhooker/internal/config"
|
||||||
"sneak.berlin/go/webhooker/internal/config/configtest"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// dotEnvKey is a throwaway variable name the .env tests write and
|
// dotEnvKey is a throwaway variable name the .env tests write and
|
||||||
@@ -40,9 +39,9 @@ func writeDotEnv(t *testing.T, contents string) string {
|
|||||||
// normally rather than be refused for a file it was never meant to
|
// normally rather than be refused for a file it was never meant to
|
||||||
// have.
|
// have.
|
||||||
//
|
//
|
||||||
//nolint:paralleltest // ClearEnv uses t.Setenv.
|
//nolint:paralleltest // ClearEnvForTest uses t.Setenv.
|
||||||
func TestLoadDotEnv_MissingFileIsFine(t *testing.T) {
|
func TestLoadDotEnv_MissingFileIsFine(t *testing.T) {
|
||||||
configtest.ClearEnv(t)
|
config.ClearEnvForTest(t)
|
||||||
|
|
||||||
absent := filepath.Join(t.TempDir(), config.DotEnvPath)
|
absent := filepath.Join(t.TempDir(), config.DotEnvPath)
|
||||||
require.NoError(t, config.LoadDotEnvFileForTest(absent))
|
require.NoError(t, config.LoadDotEnvFileForTest(absent))
|
||||||
@@ -55,9 +54,9 @@ func TestLoadDotEnv_MissingFileIsFine(t *testing.T) {
|
|||||||
// reaches the environment, which is the whole reason the file is read
|
// reaches the environment, which is the whole reason the file is read
|
||||||
// at all.
|
// at all.
|
||||||
//
|
//
|
||||||
//nolint:paralleltest // ClearEnv uses t.Setenv.
|
//nolint:paralleltest // ClearEnvForTest uses t.Setenv.
|
||||||
func TestLoadDotEnv_AppliesValues(t *testing.T) {
|
func TestLoadDotEnv_AppliesValues(t *testing.T) {
|
||||||
configtest.ClearEnv(t)
|
config.ClearEnvForTest(t)
|
||||||
|
|
||||||
path := writeDotEnv(t, "# a comment\n"+dotEnvKey+"=from-dot-env\n")
|
path := writeDotEnv(t, "# a comment\n"+dotEnvKey+"=from-dot-env\n")
|
||||||
|
|
||||||
@@ -83,9 +82,9 @@ func TestLoadDotEnv_RealEnvironmentWins(t *testing.T) {
|
|||||||
// reverts to its default; the process used to start that way with no
|
// reverts to its default; the process used to start that way with no
|
||||||
// log line naming the file at all.
|
// log line naming the file at all.
|
||||||
//
|
//
|
||||||
//nolint:paralleltest // ClearEnv uses t.Setenv.
|
//nolint:paralleltest // ClearEnvForTest uses t.Setenv.
|
||||||
func TestLoadDotEnv_MalformedFileAborts(t *testing.T) {
|
func TestLoadDotEnv_MalformedFileAborts(t *testing.T) {
|
||||||
configtest.ClearEnv(t)
|
config.ClearEnvForTest(t)
|
||||||
|
|
||||||
path := writeDotEnv(
|
path := writeDotEnv(
|
||||||
t, malformedDotEnv+dotEnvKey+"=from-dot-env\n",
|
t, malformedDotEnv+dotEnvKey+"=from-dot-env\n",
|
||||||
@@ -133,7 +132,7 @@ func TestLoadDotEnv_UnreadableFileAborts(t *testing.T) {
|
|||||||
//
|
//
|
||||||
//nolint:paralleltest // t.Chdir moves the whole process.
|
//nolint:paralleltest // t.Chdir moves the whole process.
|
||||||
func TestLoadDotEnv_ReadsTheWorkingDirectory(t *testing.T) {
|
func TestLoadDotEnv_ReadsTheWorkingDirectory(t *testing.T) {
|
||||||
configtest.ClearEnv(t)
|
config.ClearEnvForTest(t)
|
||||||
|
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
require.NoError(t, os.WriteFile(
|
require.NoError(t, os.WriteFile(
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"go.uber.org/fx"
|
"go.uber.org/fx"
|
||||||
"sneak.berlin/go/webhooker/internal/config"
|
"sneak.berlin/go/webhooker/internal/config"
|
||||||
"sneak.berlin/go/webhooker/internal/config/configtest"
|
|
||||||
"sneak.berlin/go/webhooker/internal/globals"
|
"sneak.berlin/go/webhooker/internal/globals"
|
||||||
"sneak.berlin/go/webhooker/internal/logger"
|
"sneak.berlin/go/webhooker/internal/logger"
|
||||||
)
|
)
|
||||||
@@ -121,7 +120,7 @@ func TestEnvBool(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
// Cannot use t.Parallel() here because t.Setenv
|
// Cannot use t.Parallel() here because t.Setenv
|
||||||
// is incompatible with parallel subtests.
|
// is incompatible with parallel subtests.
|
||||||
configtest.ClearEnv(t)
|
config.ClearEnvForTest(t)
|
||||||
|
|
||||||
if tt.set {
|
if tt.set {
|
||||||
t.Setenv(testEnvKey, tt.value)
|
t.Setenv(testEnvKey, tt.value)
|
||||||
@@ -170,7 +169,7 @@ func runEnvIntCases(
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
// Cannot use t.Parallel() here because t.Setenv
|
// Cannot use t.Parallel() here because t.Setenv
|
||||||
// is incompatible with parallel subtests.
|
// is incompatible with parallel subtests.
|
||||||
configtest.ClearEnv(t)
|
config.ClearEnvForTest(t)
|
||||||
|
|
||||||
if tt.set {
|
if tt.set {
|
||||||
t.Setenv(testEnvKey, tt.value)
|
t.Setenv(testEnvKey, tt.value)
|
||||||
@@ -311,7 +310,7 @@ func TestEnvBindAddress(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
// Cannot use t.Parallel() here because t.Setenv
|
// Cannot use t.Parallel() here because t.Setenv
|
||||||
// is incompatible with parallel subtests.
|
// is incompatible with parallel subtests.
|
||||||
configtest.ClearEnv(t)
|
config.ClearEnvForTest(t)
|
||||||
|
|
||||||
if tt.set {
|
if tt.set {
|
||||||
t.Setenv(testEnvKey, tt.value)
|
t.Setenv(testEnvKey, tt.value)
|
||||||
@@ -477,7 +476,7 @@ func TestNewRejectsBadEnvValues(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
// Cannot use t.Parallel() here because t.Setenv
|
// Cannot use t.Parallel() here because t.Setenv
|
||||||
// is incompatible with parallel subtests.
|
// is incompatible with parallel subtests.
|
||||||
configtest.ClearEnv(t)
|
config.ClearEnvForTest(t)
|
||||||
t.Setenv("WEBHOOKER_ENVIRONMENT", "dev")
|
t.Setenv("WEBHOOKER_ENVIRONMENT", "dev")
|
||||||
t.Setenv(tt.key, tt.value)
|
t.Setenv(tt.key, tt.value)
|
||||||
|
|
||||||
@@ -639,7 +638,7 @@ func sentryEnvValueCases() []badEnvValueCase {
|
|||||||
// break the legitimate unset case: absent variables still get their
|
// break the legitimate unset case: absent variables still get their
|
||||||
// documented defaults.
|
// documented defaults.
|
||||||
func TestNewUsesDefaultsWhenUnset(t *testing.T) {
|
func TestNewUsesDefaultsWhenUnset(t *testing.T) {
|
||||||
configtest.ClearEnv(t)
|
config.ClearEnvForTest(t)
|
||||||
t.Setenv("WEBHOOKER_ENVIRONMENT", "dev")
|
t.Setenv("WEBHOOKER_ENVIRONMENT", "dev")
|
||||||
|
|
||||||
cfg, err := buildConfig(t)
|
cfg, err := buildConfig(t)
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"sneak.berlin/go/webhooker/internal/config"
|
"sneak.berlin/go/webhooker/internal/config"
|
||||||
"sneak.berlin/go/webhooker/internal/config/configtest"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// envKeySentryDSN is the variable envSentryDSN reads in production.
|
// envKeySentryDSN is the variable envSentryDSN reads in production.
|
||||||
@@ -101,7 +100,7 @@ func TestEnvSentryDSN(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
// Cannot use t.Parallel() here because t.Setenv
|
// Cannot use t.Parallel() here because t.Setenv
|
||||||
// is incompatible with parallel subtests.
|
// is incompatible with parallel subtests.
|
||||||
configtest.ClearEnv(t)
|
config.ClearEnvForTest(t)
|
||||||
|
|
||||||
if tt.set {
|
if tt.set {
|
||||||
t.Setenv(envKeySentryDSN, tt.value)
|
t.Setenv(envKeySentryDSN, tt.value)
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
// Package configtest holds test support for code that reads the
|
package config
|
||||||
// process environment.
|
|
||||||
package configtest
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
@@ -8,12 +6,12 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ClearEnv unsets every variable in the process environment
|
// ClearEnvForTest unsets every variable in the process environment
|
||||||
// for the rest of the test, so a test sees only the variables it sets
|
// for the rest of the test, so a test sees only the variables it sets
|
||||||
// itself, not whatever the developer's shell exports. When the test
|
// itself, not whatever the developer's shell exports. When the test
|
||||||
// ends it leaves the environment exactly as it found it: each variable
|
// ends it leaves the environment exactly as it found it: each variable
|
||||||
// it unset is put back, and any variable added since is removed.
|
// it unset is put back, and any variable added since is removed.
|
||||||
func ClearEnv(t *testing.T) {
|
func ClearEnvForTest(t *testing.T) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
present := make(map[string]bool)
|
present := make(map[string]bool)
|
||||||
@@ -7,22 +7,21 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"sneak.berlin/go/webhooker/internal/config"
|
"sneak.berlin/go/webhooker/internal/config"
|
||||||
"sneak.berlin/go/webhooker/internal/config/configtest"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestClearEnv_RemovesAddedVariables pins that a variable set
|
// TestClearEnvForTest_RemovesAddedVariables pins that a variable set
|
||||||
// after the clear other than through t.Setenv, as a test's .env file
|
// after the clear other than through t.Setenv, as a test's .env file
|
||||||
// sets one, is gone once the test ends, so it cannot reach the tests
|
// sets one, is gone once the test ends, so it cannot reach the tests
|
||||||
// that run after it.
|
// that run after it.
|
||||||
//
|
//
|
||||||
//nolint:paralleltest // ClearEnv uses t.Setenv.
|
//nolint:paralleltest // ClearEnvForTest uses t.Setenv.
|
||||||
func TestClearEnv_RemovesAddedVariables(t *testing.T) {
|
func TestClearEnvForTest_RemovesAddedVariables(t *testing.T) {
|
||||||
// The outer clear keeps a value of the key exported in the shell
|
// The outer clear keeps a value of the key exported in the shell
|
||||||
// from making it a variable the inner clear has to put back.
|
// from making it a variable the inner clear has to put back.
|
||||||
configtest.ClearEnv(t)
|
config.ClearEnvForTest(t)
|
||||||
|
|
||||||
t.Run("loads a .env file after the clear", func(t *testing.T) {
|
t.Run("loads a .env file after the clear", func(t *testing.T) {
|
||||||
configtest.ClearEnv(t)
|
config.ClearEnvForTest(t)
|
||||||
|
|
||||||
path := writeDotEnv(t, dotEnvKey+"=from-dot-env\n")
|
path := writeDotEnv(t, dotEnvKey+"=from-dot-env\n")
|
||||||
require.NoError(t, config.LoadDotEnvFileForTest(path))
|
require.NoError(t, config.LoadDotEnvFileForTest(path))
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
// Package databasetest builds a WebhookDBManager for tests in other
|
|
||||||
// packages.
|
|
||||||
package databasetest
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log/slog"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"go.uber.org/fx/fxtest"
|
|
||||||
"sneak.berlin/go/webhooker/internal/config"
|
|
||||||
"sneak.berlin/go/webhooker/internal/database"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewWebhookDBManager creates a WebhookDBManager backed by the given
|
|
||||||
// data directory, logging at DEBUG to standard error.
|
|
||||||
func NewWebhookDBManager(
|
|
||||||
t *testing.T, dataDir string,
|
|
||||||
) *database.WebhookDBManager {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
return NewWebhookDBManagerWithLogger(
|
|
||||||
t,
|
|
||||||
dataDir,
|
|
||||||
slog.New(slog.NewTextHandler(
|
|
||||||
os.Stderr,
|
|
||||||
&slog.HandlerOptions{Level: slog.LevelDebug},
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewWebhookDBManagerWithLogger is NewWebhookDBManager with the
|
|
||||||
// logger supplied by the caller. The per-webhook databases this manager
|
|
||||||
// opens hand that logger to gormlog, so a test that needs to see the SQL
|
|
||||||
// the service emits can capture it.
|
|
||||||
//
|
|
||||||
// It is built through database.NewWebhookDBManager on a lifecycle that
|
|
||||||
// is never started, so nothing closes its databases but the caller.
|
|
||||||
func NewWebhookDBManagerWithLogger(
|
|
||||||
t *testing.T, dataDir string, log *slog.Logger,
|
|
||||||
) *database.WebhookDBManager {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
mgr, err := database.NewWebhookDBManager(
|
|
||||||
fxtest.NewLifecycle(t),
|
|
||||||
database.WebhookDBManagerParams{
|
|
||||||
Config: &config.Config{DataDir: dataDir},
|
|
||||||
Logger: log,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
return mgr
|
|
||||||
}
|
|
||||||
@@ -13,7 +13,6 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
_ "modernc.org/sqlite"
|
_ "modernc.org/sqlite"
|
||||||
"sneak.berlin/go/webhooker/internal/database"
|
"sneak.berlin/go/webhooker/internal/database"
|
||||||
"sneak.berlin/go/webhooker/internal/database/databasetest"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// testDataDirPerm is the mode the test data directory is created
|
// testDataDirPerm is the mode the test data directory is created
|
||||||
@@ -134,7 +133,7 @@ func TestOpenPurgesLeakedTargetRows(t *testing.T) {
|
|||||||
// Create the file the way the application does, so the targets
|
// Create the file the way the application does, so the targets
|
||||||
// table has exactly the shape AutoMigrate gives it, then write
|
// table has exactly the shape AutoMigrate gives it, then write
|
||||||
// a leaked row into it the way the association upsert did.
|
// a leaked row into it the way the association upsert did.
|
||||||
initial := databasetest.NewWebhookDBManager(t, dataDir)
|
initial := database.NewTestWebhookDBManager(dataDir)
|
||||||
|
|
||||||
_, err := initial.GetDB(webhookID)
|
_, err := initial.GetDB(webhookID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -157,7 +156,7 @@ func TestOpenPurgesLeakedTargetRows(t *testing.T) {
|
|||||||
clearEventDBSweptMarker(t, seed)
|
clearEventDBSweptMarker(t, seed)
|
||||||
require.NoError(t, seed.Close())
|
require.NoError(t, seed.Close())
|
||||||
|
|
||||||
mgr := databasetest.NewWebhookDBManager(t, dataDir)
|
mgr := database.NewTestWebhookDBManager(dataDir)
|
||||||
|
|
||||||
_, err = mgr.GetDB(webhookID)
|
_, err = mgr.GetDB(webhookID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -173,7 +172,7 @@ func TestOpenPurgesLeakedTargetRows(t *testing.T) {
|
|||||||
|
|
||||||
// Idempotent: a second open leaves it at zero and does not
|
// Idempotent: a second open leaves it at zero and does not
|
||||||
// error.
|
// error.
|
||||||
again := databasetest.NewWebhookDBManager(t, dataDir)
|
again := database.NewTestWebhookDBManager(dataDir)
|
||||||
|
|
||||||
_, err = again.GetDB(webhookID)
|
_, err = again.GetDB(webhookID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -196,7 +195,7 @@ func TestOpenPurgeRemovesCredentialBytes(t *testing.T) {
|
|||||||
webhookID := uuid.New().String()
|
webhookID := uuid.New().String()
|
||||||
credential := "T00000000/B00000000/" + uuid.New().String()
|
credential := "T00000000/B00000000/" + uuid.New().String()
|
||||||
|
|
||||||
initial := databasetest.NewWebhookDBManager(t, dataDir)
|
initial := database.NewTestWebhookDBManager(dataDir)
|
||||||
|
|
||||||
_, err := initial.GetDB(webhookID)
|
_, err := initial.GetDB(webhookID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -231,7 +230,7 @@ func TestOpenPurgeRemovesCredentialBytes(t *testing.T) {
|
|||||||
"seeded credential is not in the file, so this test proves nothing",
|
"seeded credential is not in the file, so this test proves nothing",
|
||||||
)
|
)
|
||||||
|
|
||||||
mgr := databasetest.NewWebhookDBManager(t, dataDir)
|
mgr := database.NewTestWebhookDBManager(dataDir)
|
||||||
|
|
||||||
_, err = mgr.GetDB(webhookID)
|
_, err = mgr.GetDB(webhookID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -259,7 +258,7 @@ func TestOpenRevacuumsAfterIncompleteSweep(t *testing.T) {
|
|||||||
webhookID := uuid.New().String()
|
webhookID := uuid.New().String()
|
||||||
credential := "T00000000/B00000000/" + uuid.New().String()
|
credential := "T00000000/B00000000/" + uuid.New().String()
|
||||||
|
|
||||||
initial := databasetest.NewWebhookDBManager(t, dataDir)
|
initial := database.NewTestWebhookDBManager(dataDir)
|
||||||
|
|
||||||
_, err := initial.GetDB(webhookID)
|
_, err := initial.GetDB(webhookID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -299,7 +298,7 @@ func TestOpenRevacuumsAfterIncompleteSweep(t *testing.T) {
|
|||||||
"test proves nothing",
|
"test proves nothing",
|
||||||
)
|
)
|
||||||
|
|
||||||
mgr := databasetest.NewWebhookDBManager(t, dataDir)
|
mgr := database.NewTestWebhookDBManager(dataDir)
|
||||||
|
|
||||||
_, err = mgr.GetDB(webhookID)
|
_, err = mgr.GetDB(webhookID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -326,7 +325,7 @@ func TestOpenSkipsSweptDatabase(t *testing.T) {
|
|||||||
dataDir := eventDBDataDir(t)
|
dataDir := eventDBDataDir(t)
|
||||||
webhookID := uuid.New().String()
|
webhookID := uuid.New().String()
|
||||||
|
|
||||||
mgr := databasetest.NewWebhookDBManager(t, dataDir)
|
mgr := database.NewTestWebhookDBManager(dataDir)
|
||||||
|
|
||||||
_, err := mgr.GetDB(webhookID)
|
_, err := mgr.GetDB(webhookID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -348,7 +347,7 @@ func TestOpenSkipsSweptDatabase(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, marked.Close())
|
require.NoError(t, marked.Close())
|
||||||
|
|
||||||
again := databasetest.NewWebhookDBManager(t, dataDir)
|
again := database.NewTestWebhookDBManager(dataDir)
|
||||||
|
|
||||||
_, err = again.GetDB(webhookID)
|
_, err = again.GetDB(webhookID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -379,7 +378,7 @@ func TestOpenSucceedsWithoutTargetsTable(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, seed.Close())
|
require.NoError(t, seed.Close())
|
||||||
|
|
||||||
mgr := databasetest.NewWebhookDBManager(t, dataDir)
|
mgr := database.NewTestWebhookDBManager(dataDir)
|
||||||
|
|
||||||
db, err := mgr.GetDB(webhookID)
|
db, err := mgr.GetDB(webhookID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -397,7 +396,7 @@ func TestEventDBCreateOmitsAssociations(t *testing.T) {
|
|||||||
dataDir := eventDBDataDir(t)
|
dataDir := eventDBDataDir(t)
|
||||||
webhookID := uuid.New().String()
|
webhookID := uuid.New().String()
|
||||||
|
|
||||||
mgr := databasetest.NewWebhookDBManager(t, dataDir)
|
mgr := database.NewTestWebhookDBManager(dataDir)
|
||||||
|
|
||||||
db, err := mgr.GetDB(webhookID)
|
db, err := mgr.GetDB(webhookID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func setupRetentionTest(t *testing.T) *retentionTestEnv {
|
|||||||
|
|
||||||
mgr, err := database.NewWebhookDBManager(
|
mgr, err := database.NewWebhookDBManager(
|
||||||
lc,
|
lc,
|
||||||
database.WebhookDBManagerParams{Config: cfg, Logger: l.Get()},
|
database.WebhookDBManagerParams{Config: cfg, Logger: l},
|
||||||
)
|
)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewTestDatabase creates a Database wrapper around a pre-opened *gorm.DB.
|
||||||
|
// Intended for use in tests that need a *database.Database without the
|
||||||
|
// full fx lifecycle. The caller is responsible for closing the underlying
|
||||||
|
// sql.DB connection.
|
||||||
|
func NewTestDatabase(db *gorm.DB) *Database {
|
||||||
|
return &Database{
|
||||||
|
db: db,
|
||||||
|
log: slog.New(slog.NewTextHandler(
|
||||||
|
os.Stderr,
|
||||||
|
&slog.HandlerOptions{Level: slog.LevelDebug},
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTestWebhookDBManager creates a WebhookDBManager backed by the given
|
||||||
|
// data directory. Intended for use in tests without the fx lifecycle.
|
||||||
|
func NewTestWebhookDBManager(dataDir string) *WebhookDBManager {
|
||||||
|
return NewTestWebhookDBManagerWithLogger(
|
||||||
|
dataDir,
|
||||||
|
slog.New(slog.NewTextHandler(
|
||||||
|
os.Stderr,
|
||||||
|
&slog.HandlerOptions{Level: slog.LevelDebug},
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTestWebhookDBManagerWithLogger is NewTestWebhookDBManager with the
|
||||||
|
// logger supplied by the caller. The per-webhook databases this manager
|
||||||
|
// opens hand that logger to gormlog, so a test that needs to see the SQL
|
||||||
|
// the service emits can capture it.
|
||||||
|
func NewTestWebhookDBManagerWithLogger(
|
||||||
|
dataDir string, log *slog.Logger,
|
||||||
|
) *WebhookDBManager {
|
||||||
|
return &WebhookDBManager{
|
||||||
|
dataDir: dataDir,
|
||||||
|
log: log,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"sneak.berlin/go/webhooker/internal/config"
|
"sneak.berlin/go/webhooker/internal/config"
|
||||||
"sneak.berlin/go/webhooker/internal/datadir"
|
"sneak.berlin/go/webhooker/internal/datadir"
|
||||||
"sneak.berlin/go/webhooker/internal/gormlog"
|
"sneak.berlin/go/webhooker/internal/gormlog"
|
||||||
|
"sneak.berlin/go/webhooker/internal/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WebhookDBManagerParams holds the fx dependencies for
|
// WebhookDBManagerParams holds the fx dependencies for
|
||||||
@@ -23,7 +24,7 @@ type WebhookDBManagerParams struct {
|
|||||||
fx.In
|
fx.In
|
||||||
|
|
||||||
Config *config.Config
|
Config *config.Config
|
||||||
Logger *slog.Logger
|
Logger *logger.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// errInvalidCachedDBType indicates a type assertion failure
|
// errInvalidCachedDBType indicates a type assertion failure
|
||||||
@@ -69,7 +70,7 @@ func NewWebhookDBManager(
|
|||||||
) (*WebhookDBManager, error) {
|
) (*WebhookDBManager, error) {
|
||||||
m := &WebhookDBManager{
|
m := &WebhookDBManager{
|
||||||
dataDir: params.Config.DataDir,
|
dataDir: params.Config.DataDir,
|
||||||
log: params.Logger,
|
log: params.Logger.Get(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create data directory if it doesn't exist. datadir.DirPerm is the
|
// Create data directory if it doesn't exist. datadir.DirPerm is the
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"sneak.berlin/go/webhooker/internal/config"
|
"sneak.berlin/go/webhooker/internal/config"
|
||||||
"sneak.berlin/go/webhooker/internal/database"
|
"sneak.berlin/go/webhooker/internal/database"
|
||||||
"sneak.berlin/go/webhooker/internal/database/databasetest"
|
|
||||||
"sneak.berlin/go/webhooker/internal/globals"
|
"sneak.berlin/go/webhooker/internal/globals"
|
||||||
"sneak.berlin/go/webhooker/internal/logger"
|
"sneak.berlin/go/webhooker/internal/logger"
|
||||||
)
|
)
|
||||||
@@ -51,7 +50,7 @@ func setupTestWebhookDBManager(
|
|||||||
lc,
|
lc,
|
||||||
database.WebhookDBManagerParams{
|
database.WebhookDBManagerParams{
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
Logger: l.Get(),
|
Logger: l,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -118,8 +117,7 @@ func TestWebhookDBManager_ConcurrentFirstTouchOpensOnce(t *testing.T) {
|
|||||||
|
|
||||||
var logs bytes.Buffer
|
var logs bytes.Buffer
|
||||||
|
|
||||||
mgr := databasetest.NewWebhookDBManagerWithLogger(
|
mgr := database.NewTestWebhookDBManagerWithLogger(
|
||||||
t,
|
|
||||||
t.TempDir(),
|
t.TempDir(),
|
||||||
slog.New(slog.NewTextHandler(&logs, nil)),
|
slog.New(slog.NewTextHandler(&logs, nil)),
|
||||||
)
|
)
|
||||||
@@ -309,8 +307,7 @@ func TestWebhookDBManager_LostDatabaseIsLogged(t *testing.T) {
|
|||||||
|
|
||||||
var logs bytes.Buffer
|
var logs bytes.Buffer
|
||||||
|
|
||||||
mgr := databasetest.NewWebhookDBManagerWithLogger(
|
mgr := database.NewTestWebhookDBManagerWithLogger(
|
||||||
t,
|
|
||||||
t.TempDir(),
|
t.TempDir(),
|
||||||
slog.New(slog.NewTextHandler(&logs, nil)),
|
slog.New(slog.NewTextHandler(&logs, nil)),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import (
|
|||||||
"gorm.io/gorm/clause"
|
"gorm.io/gorm/clause"
|
||||||
_ "modernc.org/sqlite" // Pure Go SQLite driver.
|
_ "modernc.org/sqlite" // Pure Go SQLite driver.
|
||||||
"sneak.berlin/go/webhooker/internal/database"
|
"sneak.berlin/go/webhooker/internal/database"
|
||||||
"sneak.berlin/go/webhooker/internal/database/databasetest"
|
|
||||||
"sneak.berlin/go/webhooker/internal/delivery"
|
"sneak.berlin/go/webhooker/internal/delivery"
|
||||||
"sneak.berlin/go/webhooker/internal/gormlog"
|
"sneak.berlin/go/webhooker/internal/gormlog"
|
||||||
)
|
)
|
||||||
@@ -60,14 +59,29 @@ func setupArchiveTest(t *testing.T) *archiveEnv {
|
|||||||
dataDir := t.TempDir()
|
dataDir := t.TempDir()
|
||||||
log := archiveTestLogger()
|
log := archiveTestLogger()
|
||||||
|
|
||||||
mainDB, err := database.Open(dataDir, slog.New(slog.DiscardHandler))
|
sqlDB, err := sql.Open(
|
||||||
|
"sqlite",
|
||||||
|
fmt.Sprintf(
|
||||||
|
"file:%s?mode=rwc",
|
||||||
|
filepath.Join(dataDir, "main.db"),
|
||||||
|
),
|
||||||
|
)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
t.Cleanup(func() { _ = mainDB.Close() })
|
t.Cleanup(func() { _ = sqlDB.Close() })
|
||||||
|
|
||||||
|
gdb, err := gorm.Open(
|
||||||
|
sqlite.Dialector{Conn: sqlDB},
|
||||||
|
&gorm.Config{Logger: gormlog.New(slog.New(slog.DiscardHandler))},
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
mainDB := database.NewTestDatabase(gdb)
|
||||||
|
require.NoError(t, mainDB.Migrate())
|
||||||
|
|
||||||
eng := delivery.NewTestEngineWithDB(
|
eng := delivery.NewTestEngineWithDB(
|
||||||
mainDB,
|
mainDB,
|
||||||
databasetest.NewWebhookDBManager(t, dataDir),
|
database.NewTestWebhookDBManager(dataDir),
|
||||||
log,
|
log,
|
||||||
&http.Client{Timeout: 5 * time.Second},
|
&http.Client{Timeout: 5 * time.Second},
|
||||||
1,
|
1,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -18,11 +19,12 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
_ "modernc.org/sqlite"
|
_ "modernc.org/sqlite"
|
||||||
"sneak.berlin/go/webhooker/internal/database"
|
"sneak.berlin/go/webhooker/internal/database"
|
||||||
"sneak.berlin/go/webhooker/internal/database/databasetest"
|
|
||||||
"sneak.berlin/go/webhooker/internal/delivery"
|
"sneak.berlin/go/webhooker/internal/delivery"
|
||||||
|
"sneak.berlin/go/webhooker/internal/gormlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// iSetup holds common integration test dependencies.
|
// iSetup holds common integration test dependencies.
|
||||||
@@ -43,12 +45,12 @@ func newISetup(t *testing.T) iSetup {
|
|||||||
wDB := iSeedWebhookDB(t, dbMgr, wID)
|
wDB := iSeedWebhookDB(t, dbMgr, wID)
|
||||||
|
|
||||||
return iSetup{
|
return iSetup{
|
||||||
MainDB: mainDB.DB(),
|
MainDB: mainDB,
|
||||||
DBMgr: dbMgr,
|
DBMgr: dbMgr,
|
||||||
WebhookID: wID,
|
WebhookID: wID,
|
||||||
WebhookDB: wDB,
|
WebhookDB: wDB,
|
||||||
Engine: delivery.NewTestEngineWithDB(
|
Engine: delivery.NewTestEngineWithDB(
|
||||||
mainDB,
|
database.NewTestDatabase(mainDB),
|
||||||
dbMgr,
|
dbMgr,
|
||||||
slog.New(slog.NewTextHandler(
|
slog.New(slog.NewTextHandler(
|
||||||
os.Stderr,
|
os.Stderr,
|
||||||
@@ -62,16 +64,35 @@ func newISetup(t *testing.T) iSetup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// iMainDB opens a main database through database.Open, the way the
|
func iMainDB(t *testing.T) *gorm.DB {
|
||||||
// service opens it, so these tests cannot pass against journal and
|
|
||||||
// locking settings production does not use.
|
|
||||||
func iMainDB(t *testing.T) *database.Database {
|
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
db, err := database.Open(t.TempDir(), slog.New(slog.DiscardHandler))
|
dbPath := filepath.Join(
|
||||||
|
t.TempDir(), "main-test.db",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Opened the way the service opens the main database, so these
|
||||||
|
// tests cannot pass against journal and locking settings
|
||||||
|
// production does not use.
|
||||||
|
sqlDB, err := database.OpenSQLite(
|
||||||
|
dbPath, database.SQLiteModeCreate,
|
||||||
|
)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
t.Cleanup(func() { _ = db.Close() })
|
t.Cleanup(func() { _ = sqlDB.Close() })
|
||||||
|
|
||||||
|
db, err := gorm.Open(
|
||||||
|
sqlite.Dialector{Conn: sqlDB},
|
||||||
|
&gorm.Config{Logger: gormlog.New(slog.New(slog.DiscardHandler))},
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NoError(t, db.AutoMigrate(
|
||||||
|
&database.Webhook{},
|
||||||
|
&database.Target{},
|
||||||
|
&database.User{},
|
||||||
|
&database.Setting{},
|
||||||
|
))
|
||||||
|
|
||||||
return db
|
return db
|
||||||
}
|
}
|
||||||
@@ -81,7 +102,7 @@ func iDBManager(
|
|||||||
) *database.WebhookDBManager {
|
) *database.WebhookDBManager {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
return databasetest.NewWebhookDBManager(t, t.TempDir())
|
return database.NewTestWebhookDBManager(t.TempDir())
|
||||||
}
|
}
|
||||||
|
|
||||||
func iSeedWebhookDB(
|
func iSeedWebhookDB(
|
||||||
@@ -1133,17 +1154,17 @@ func TestRecoverInFlight_ReportsAMissingWebhookDatabase(t *testing.T) {
|
|||||||
|
|
||||||
mainDB := iMainDB(t)
|
mainDB := iMainDB(t)
|
||||||
webhookID := uuid.New().String()
|
webhookID := uuid.New().String()
|
||||||
iCreateWebhook(t, mainDB.DB(), webhookID, "lost-database")
|
iCreateWebhook(t, mainDB, webhookID, "lost-database")
|
||||||
|
|
||||||
var logs bytes.Buffer
|
var logs bytes.Buffer
|
||||||
|
|
||||||
dbMgr := databasetest.NewWebhookDBManagerWithLogger(
|
dbMgr := database.NewTestWebhookDBManagerWithLogger(
|
||||||
t, t.TempDir(), slog.New(slog.NewTextHandler(&logs, nil)),
|
t.TempDir(), slog.New(slog.NewTextHandler(&logs, nil)),
|
||||||
)
|
)
|
||||||
t.Cleanup(func() { _ = dbMgr.CloseAll() })
|
t.Cleanup(func() { _ = dbMgr.CloseAll() })
|
||||||
|
|
||||||
engine := delivery.NewTestEngineWithDB(
|
engine := delivery.NewTestEngineWithDB(
|
||||||
mainDB, dbMgr,
|
database.NewTestDatabase(mainDB), dbMgr,
|
||||||
slog.New(slog.DiscardHandler),
|
slog.New(slog.DiscardHandler),
|
||||||
&http.Client{Timeout: 5 * time.Second}, 1,
|
&http.Client{Timeout: 5 * time.Second}, 1,
|
||||||
)
|
)
|
||||||
@@ -1167,14 +1188,14 @@ func TestRecoverInFlight_SkipsAWebhookDeletedAfterTheListIsRead(
|
|||||||
|
|
||||||
mainDB := iMainDB(t)
|
mainDB := iMainDB(t)
|
||||||
webhookID := uuid.New().String()
|
webhookID := uuid.New().String()
|
||||||
iCreateWebhook(t, mainDB.DB(), webhookID, "deleted-during-recovery")
|
iCreateWebhook(t, mainDB, webhookID, "deleted-during-recovery")
|
||||||
|
|
||||||
// The first query to return is recovery's read of the list of
|
// The first query to return is recovery's read of the list of
|
||||||
// webhooks. Deleting the webhook right after it puts the delete
|
// webhooks. Deleting the webhook right after it puts the delete
|
||||||
// between that read and the opening of the webhook's database.
|
// between that read and the opening of the webhook's database.
|
||||||
deleted := false
|
deleted := false
|
||||||
|
|
||||||
require.NoError(t, mainDB.DB().Callback().Query().After("gorm:query").
|
require.NoError(t, mainDB.Callback().Query().After("gorm:query").
|
||||||
Register("delete-after-list", func(*gorm.DB) {
|
Register("delete-after-list", func(*gorm.DB) {
|
||||||
if deleted {
|
if deleted {
|
||||||
return
|
return
|
||||||
@@ -1182,16 +1203,16 @@ func TestRecoverInFlight_SkipsAWebhookDeletedAfterTheListIsRead(
|
|||||||
|
|
||||||
deleted = true
|
deleted = true
|
||||||
|
|
||||||
require.NoError(t, mainDB.DB().Delete(
|
require.NoError(t, mainDB.Delete(
|
||||||
&database.Webhook{}, "id = ?", webhookID,
|
&database.Webhook{}, "id = ?", webhookID,
|
||||||
).Error)
|
).Error)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
dbMgr := databasetest.NewWebhookDBManager(t, t.TempDir())
|
dbMgr := database.NewTestWebhookDBManager(t.TempDir())
|
||||||
t.Cleanup(func() { _ = dbMgr.CloseAll() })
|
t.Cleanup(func() { _ = dbMgr.CloseAll() })
|
||||||
|
|
||||||
engine := delivery.NewTestEngineWithDB(
|
engine := delivery.NewTestEngineWithDB(
|
||||||
mainDB, dbMgr,
|
database.NewTestDatabase(mainDB), dbMgr,
|
||||||
slog.New(slog.DiscardHandler),
|
slog.New(slog.DiscardHandler),
|
||||||
&http.Client{Timeout: 5 * time.Second}, 1,
|
&http.Client{Timeout: 5 * time.Second}, 1,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -48,9 +48,8 @@ func fSweepSetup(
|
|||||||
//
|
//
|
||||||
// Every caller drives the dispatch paths synchronously and has already
|
// Every caller drives the dispatch paths synchronously and has already
|
||||||
// waited for them to return, so anything they queued is in the channel
|
// waited for them to return, so anything they queued is in the channel
|
||||||
// by now, and nothing is waited for. A timer here would race the queued
|
// by now. The short grace covers nothing but scheduler jitter, and is
|
||||||
// tasks: on a busy host it can be due by the time select looks, and
|
// kept small because one of these tests runs the drain forty times.
|
||||||
// select picks at random among the cases that are ready.
|
|
||||||
func fDrain(e *delivery.Engine) []delivery.Task {
|
func fDrain(e *delivery.Engine) []delivery.Task {
|
||||||
var out []delivery.Task
|
var out []delivery.Task
|
||||||
|
|
||||||
@@ -60,7 +59,7 @@ func fDrain(e *delivery.Engine) []delivery.Task {
|
|||||||
out = append(out, task)
|
out = append(out, task)
|
||||||
case task := <-e.ExportRetryCh():
|
case task := <-e.ExportRetryCh():
|
||||||
out = append(out, task)
|
out = append(out, task)
|
||||||
default:
|
case <-time.After(25 * time.Millisecond):
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -13,9 +14,11 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
"sneak.berlin/go/webhooker/internal/database"
|
"sneak.berlin/go/webhooker/internal/database"
|
||||||
"sneak.berlin/go/webhooker/internal/database/databasetest"
|
|
||||||
"sneak.berlin/go/webhooker/internal/delivery"
|
"sneak.berlin/go/webhooker/internal/delivery"
|
||||||
|
"sneak.berlin/go/webhooker/internal/gormlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// qdAggregateMarker identifies the queue-depth aggregate in the
|
// qdAggregateMarker identifies the queue-depth aggregate in the
|
||||||
@@ -46,13 +49,27 @@ func (q *qdSyncBuf) String() string {
|
|||||||
|
|
||||||
// qdMainDB opens a main database whose GORM logger is the service's
|
// qdMainDB opens a main database whose GORM logger is the service's
|
||||||
// adapter, writing through log.
|
// adapter, writing through log.
|
||||||
func qdMainDB(t *testing.T, log *slog.Logger) *database.Database {
|
func qdMainDB(t *testing.T, log *slog.Logger) *gorm.DB {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
db, err := database.Open(t.TempDir(), log)
|
sqlDB, err := database.OpenSQLite(
|
||||||
|
filepath.Join(t.TempDir(), "main-gormlog.db"),
|
||||||
|
database.SQLiteModeCreate,
|
||||||
|
)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
t.Cleanup(func() { _ = db.Close() })
|
t.Cleanup(func() { _ = sqlDB.Close() })
|
||||||
|
|
||||||
|
db, err := gorm.Open(
|
||||||
|
sqlite.Dialector{Conn: sqlDB},
|
||||||
|
&gorm.Config{Logger: gormlog.New(log)},
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NoError(t, db.AutoMigrate(
|
||||||
|
&database.Webhook{},
|
||||||
|
&database.Target{},
|
||||||
|
))
|
||||||
|
|
||||||
return db
|
return db
|
||||||
}
|
}
|
||||||
@@ -89,18 +106,18 @@ func TestQueueDepthSample_LogsNoBoundValue(t *testing.T) {
|
|||||||
))
|
))
|
||||||
|
|
||||||
mainDB := qdMainDB(t, log)
|
mainDB := qdMainDB(t, log)
|
||||||
dbMgr := databasetest.NewWebhookDBManagerWithLogger(
|
dbMgr := database.NewTestWebhookDBManagerWithLogger(
|
||||||
t, t.TempDir(), log,
|
t.TempDir(), log,
|
||||||
)
|
)
|
||||||
|
|
||||||
webhookID := uuid.New().String()
|
webhookID := uuid.New().String()
|
||||||
webhookDB := iSeedWebhookDB(t, dbMgr, webhookID)
|
webhookDB := iSeedWebhookDB(t, dbMgr, webhookID)
|
||||||
|
|
||||||
iCreateWebhook(t, mainDB.DB(), webhookID, "queue-depth-gormlog")
|
iCreateWebhook(t, mainDB, webhookID, "queue-depth-gormlog")
|
||||||
|
|
||||||
targetID := uuid.New().String()
|
targetID := uuid.New().String()
|
||||||
|
|
||||||
iCreateTarget(t, mainDB.DB(), targetID, webhookID,
|
iCreateTarget(t, mainDB, targetID, webhookID,
|
||||||
"queue-depth-gormlog-target", database.TargetTypeHTTP,
|
"queue-depth-gormlog-target", database.TargetTypeHTTP,
|
||||||
iHTTPConfig("https://example.com/hook"), 3,
|
iHTTPConfig("https://example.com/hook"), 3,
|
||||||
)
|
)
|
||||||
@@ -119,7 +136,7 @@ func TestQueueDepthSample_LogsNoBoundValue(t *testing.T) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
engine := delivery.NewTestEngineWithDB(
|
engine := delivery.NewTestEngineWithDB(
|
||||||
mainDB,
|
database.NewTestDatabase(mainDB),
|
||||||
dbMgr,
|
dbMgr,
|
||||||
log,
|
log,
|
||||||
&http.Client{Timeout: 5 * time.Second},
|
&http.Client{Timeout: 5 * time.Second},
|
||||||
|
|||||||
@@ -460,13 +460,8 @@ func TestArchiveExport_OneFileOpenAtATime(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// heapPeak is an io.Writer that discards what it is given and records
|
// heapPeak is an io.Writer that discards what it is given and records
|
||||||
// the largest heap it saw at a write. It collects garbage twice before
|
// the largest heap it saw at a write. It collects garbage before each
|
||||||
// each reading, so the heap it reads is what is still held. Once is not
|
// reading, so the heap it reads is what is still held.
|
||||||
// enough: the libraries the export calls (regexp, under GORM's table
|
|
||||||
// names, and encoding/json among them) cache buffers in a sync.Pool,
|
|
||||||
// which keeps them through one collection, so after one the reading
|
|
||||||
// counts however many happen to be cached. That varies from run to run
|
|
||||||
// by about as much as the limit in TestArchiveExport_Streams.
|
|
||||||
type heapPeak struct {
|
type heapPeak struct {
|
||||||
max uint64
|
max uint64
|
||||||
}
|
}
|
||||||
@@ -474,7 +469,6 @@ type heapPeak struct {
|
|||||||
func (p *heapPeak) Write(b []byte) (int, error) {
|
func (p *heapPeak) Write(b []byte) (int, error) {
|
||||||
var m runtime.MemStats
|
var m runtime.MemStats
|
||||||
|
|
||||||
runtime.GC()
|
|
||||||
runtime.GC()
|
runtime.GC()
|
||||||
runtime.ReadMemStats(&m)
|
runtime.ReadMemStats(&m)
|
||||||
p.max = max(p.max, m.HeapAlloc)
|
p.max = max(p.max, m.HeapAlloc)
|
||||||
@@ -504,8 +498,6 @@ func exportHeapGrowth(t *testing.T, rows, bodySize int) uint64 {
|
|||||||
|
|
||||||
export := listExport(t, path)
|
export := listExport(t, path)
|
||||||
|
|
||||||
// Twice, for the reason heapPeak gives.
|
|
||||||
runtime.GC()
|
|
||||||
runtime.GC()
|
runtime.GC()
|
||||||
|
|
||||||
var start runtime.MemStats
|
var start runtime.MemStats
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"sneak.berlin/go/webhooker/internal/database"
|
"sneak.berlin/go/webhooker/internal/database"
|
||||||
"sneak.berlin/go/webhooker/internal/database/databasetest"
|
|
||||||
"sneak.berlin/go/webhooker/internal/delivery"
|
"sneak.berlin/go/webhooker/internal/delivery"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -336,7 +335,7 @@ func TestArchivePathAt(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
dataDir := t.TempDir()
|
dataDir := t.TempDir()
|
||||||
dbMgr := databasetest.NewWebhookDBManager(t, dataDir)
|
dbMgr := database.NewTestWebhookDBManager(dataDir)
|
||||||
webhook := &database.Webhook{
|
webhook := &database.Webhook{
|
||||||
BaseModel: database.BaseModel{ID: "wh-id"}, Name: "Orders",
|
BaseModel: database.BaseModel{ID: "wh-id"}, Name: "Orders",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package gormlog_test
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"log/slog"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -14,7 +13,6 @@ import (
|
|||||||
"go.uber.org/fx/fxtest"
|
"go.uber.org/fx/fxtest"
|
||||||
_ "modernc.org/sqlite" // Pure Go SQLite driver.
|
_ "modernc.org/sqlite" // Pure Go SQLite driver.
|
||||||
"sneak.berlin/go/webhooker/internal/config"
|
"sneak.berlin/go/webhooker/internal/config"
|
||||||
"sneak.berlin/go/webhooker/internal/config/configtest"
|
|
||||||
"sneak.berlin/go/webhooker/internal/database"
|
"sneak.berlin/go/webhooker/internal/database"
|
||||||
"sneak.berlin/go/webhooker/internal/globals"
|
"sneak.berlin/go/webhooker/internal/globals"
|
||||||
"sneak.berlin/go/webhooker/internal/logger"
|
"sneak.berlin/go/webhooker/internal/logger"
|
||||||
@@ -130,7 +128,7 @@ func readFirstBootSecrets(
|
|||||||
func bootAtDebug(t *testing.T, dataDir string) string {
|
func bootAtDebug(t *testing.T, dataDir string) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
configtest.ClearEnv(t)
|
config.ClearEnvForTest(t)
|
||||||
t.Setenv("DEBUG", "true")
|
t.Setenv("DEBUG", "true")
|
||||||
t.Setenv("DATA_DIR", dataDir)
|
t.Setenv("DATA_DIR", dataDir)
|
||||||
|
|
||||||
@@ -147,7 +145,6 @@ func bootAtDebug(t *testing.T, dataDir string) string {
|
|||||||
fx.Provide(
|
fx.Provide(
|
||||||
globals.New,
|
globals.New,
|
||||||
logger.New,
|
logger.New,
|
||||||
func(l *logger.Logger) *slog.Logger { return l.Get() },
|
|
||||||
config.New,
|
config.New,
|
||||||
database.New,
|
database.New,
|
||||||
session.New,
|
session.New,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"log/slog"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -250,7 +249,6 @@ func newTestAppWithConfig(
|
|||||||
fx.Provide(
|
fx.Provide(
|
||||||
globals.New,
|
globals.New,
|
||||||
logger.New,
|
logger.New,
|
||||||
func(l *logger.Logger) *slog.Logger { return l.Get() },
|
|
||||||
func() *config.Config { return cfg },
|
func() *config.Config { return cfg },
|
||||||
database.New,
|
database.New,
|
||||||
database.NewWebhookDBManager,
|
database.NewWebhookDBManager,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
"sneak.berlin/go/webhooker/internal/database"
|
"sneak.berlin/go/webhooker/internal/database"
|
||||||
"sneak.berlin/go/webhooker/internal/handlers"
|
"sneak.berlin/go/webhooker/internal/handlers"
|
||||||
"sneak.berlin/go/webhooker/internal/logger"
|
"sneak.berlin/go/webhooker/internal/logger"
|
||||||
"sneak.berlin/go/webhooker/internal/middleware/middlewaretest"
|
"sneak.berlin/go/webhooker/internal/middleware"
|
||||||
"sneak.berlin/go/webhooker/internal/session"
|
"sneak.berlin/go/webhooker/internal/session"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -135,7 +135,7 @@ func TestUserRoute_Unauthenticated_RedirectedByMiddleware(t *testing.T) {
|
|||||||
|
|
||||||
t.Cleanup(app.RequireStop)
|
t.Cleanup(app.RequireStop)
|
||||||
|
|
||||||
mw := middlewaretest.New(t, log.Get(), cfg, sess)
|
mw := middleware.NewForTest(log.Get(), cfg, sess)
|
||||||
|
|
||||||
var handlerReached bool
|
var handlerReached bool
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"sneak.berlin/go/webhooker/internal/config"
|
"sneak.berlin/go/webhooker/internal/config"
|
||||||
"sneak.berlin/go/webhooker/internal/database"
|
"sneak.berlin/go/webhooker/internal/database"
|
||||||
"sneak.berlin/go/webhooker/internal/middleware/middlewaretest"
|
"sneak.berlin/go/webhooker/internal/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
// targetSecretSegments are the path segments of an incoming-webhook
|
// targetSecretSegments are the path segments of an incoming-webhook
|
||||||
@@ -65,8 +65,7 @@ func postTargetCreate(
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
logBuf := new(bytes.Buffer)
|
logBuf := new(bytes.Buffer)
|
||||||
mw := middlewaretest.New(
|
mw := middleware.NewForTest(
|
||||||
t,
|
|
||||||
slog.New(slog.NewJSONHandler(
|
slog.New(slog.NewJSONHandler(
|
||||||
logBuf, &slog.HandlerOptions{Level: slog.LevelInfo},
|
logBuf, &slog.HandlerOptions{Level: slog.LevelInfo},
|
||||||
)),
|
)),
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import (
|
|||||||
"sneak.berlin/go/webhooker/internal/config"
|
"sneak.berlin/go/webhooker/internal/config"
|
||||||
"sneak.berlin/go/webhooker/internal/database"
|
"sneak.berlin/go/webhooker/internal/database"
|
||||||
"sneak.berlin/go/webhooker/internal/delivery"
|
"sneak.berlin/go/webhooker/internal/delivery"
|
||||||
"sneak.berlin/go/webhooker/internal/middleware/middlewaretest"
|
"sneak.berlin/go/webhooker/internal/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
// errClientGone is the write failure of a client that has gone away.
|
// errClientGone is the write failure of a client that has gone away.
|
||||||
@@ -310,8 +310,7 @@ func limitedServer(
|
|||||||
const sendBuffer = 4 << 10
|
const sendBuffer = 4 << 10
|
||||||
|
|
||||||
logBuf := new(bytes.Buffer)
|
logBuf := new(bytes.Buffer)
|
||||||
mw := middlewaretest.New(
|
mw := middleware.NewForTest(
|
||||||
t,
|
|
||||||
slog.New(slog.NewJSONHandler(logBuf, nil)),
|
slog.New(slog.NewJSONHandler(logBuf, nil)),
|
||||||
&config.Config{Environment: config.EnvironmentDev},
|
&config.Config{Environment: config.EnvironmentDev},
|
||||||
nil,
|
nil,
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"sneak.berlin/go/webhooker/internal/config"
|
"sneak.berlin/go/webhooker/internal/config"
|
||||||
"sneak.berlin/go/webhooker/internal/middleware"
|
"sneak.berlin/go/webhooker/internal/middleware"
|
||||||
"sneak.berlin/go/webhooker/internal/middleware/middlewaretest"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// floodRequests is the number of distinct invented paths each flood
|
// floodRequests is the number of distinct invented paths each flood
|
||||||
@@ -84,7 +83,7 @@ func capturingMiddleware(t *testing.T) (*middleware.Middleware, *bytes.Buffer) {
|
|||||||
TrustedProxies: trustedProxies("192.0.2.1/32"),
|
TrustedProxies: trustedProxies("192.0.2.1/32"),
|
||||||
}
|
}
|
||||||
|
|
||||||
return middlewaretest.New(t, log, cfg, nil), buf
|
return middleware.NewForTest(log, cfg, nil), buf
|
||||||
}
|
}
|
||||||
|
|
||||||
// capturingTextMiddleware is capturingMiddleware for the other handler
|
// capturingTextMiddleware is capturingMiddleware for the other handler
|
||||||
@@ -108,7 +107,7 @@ func capturingTextMiddleware(
|
|||||||
TrustedProxies: trustedProxies("192.0.2.1/32"),
|
TrustedProxies: trustedProxies("192.0.2.1/32"),
|
||||||
}
|
}
|
||||||
|
|
||||||
return middlewaretest.New(t, log, cfg, nil), buf
|
return middleware.NewForTest(log, cfg, nil), buf
|
||||||
}
|
}
|
||||||
|
|
||||||
// accessLogRouter mirrors the production route shapes that an
|
// accessLogRouter mirrors the production route shapes that an
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"sneak.berlin/go/webhooker/internal/config"
|
"sneak.berlin/go/webhooker/internal/config"
|
||||||
"sneak.berlin/go/webhooker/internal/middleware"
|
"sneak.berlin/go/webhooker/internal/middleware"
|
||||||
"sneak.berlin/go/webhooker/internal/middleware/middlewaretest"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -134,8 +133,8 @@ func clientLogLines(
|
|||||||
TrustedProxies: trustedProxies(trustedProxyCIDR),
|
TrustedProxies: trustedProxies(trustedProxyCIDR),
|
||||||
}
|
}
|
||||||
|
|
||||||
m := middlewaretest.New(
|
m := middleware.NewForTest(
|
||||||
t, log, cfg, newTestSessionManager(t, cfg),
|
log, cfg, newTestSessionManager(cfg, log, nil),
|
||||||
)
|
)
|
||||||
handler := m.Logging()(site.build(m))
|
handler := m.Logging()(site.build(m))
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"sneak.berlin/go/webhooker/internal/config"
|
"sneak.berlin/go/webhooker/internal/config"
|
||||||
"sneak.berlin/go/webhooker/internal/middleware"
|
"sneak.berlin/go/webhooker/internal/middleware"
|
||||||
"sneak.berlin/go/webhooker/internal/middleware/middlewaretest"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// bodyLimitBytes is the MaxBodySize cap these tests install. Any
|
// bodyLimitBytes is the MaxBodySize cap these tests install. Any
|
||||||
@@ -156,9 +155,9 @@ func capturingBoundMiddleware(
|
|||||||
ReceiverRateLimit: receiverLimitPerMinute,
|
ReceiverRateLimit: receiverLimitPerMinute,
|
||||||
}
|
}
|
||||||
|
|
||||||
sess := newTestSessionManager(t, cfg)
|
sess := newTestSessionManager(cfg, log, nil)
|
||||||
|
|
||||||
return middlewaretest.New(t, log, cfg, sess), buf
|
return middleware.NewForTest(log, cfg, sess), buf
|
||||||
}
|
}
|
||||||
|
|
||||||
// unreachable is a next-handler that fails the test if the middleware
|
// unreachable is a next-handler that fails the test if the middleware
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ var _ httpmetrics.Recorder = boundedLabelRecorder{}
|
|||||||
|
|
||||||
// Metrics returns middleware that records Prometheus HTTP metrics
|
// Metrics returns middleware that records Prometheus HTTP metrics
|
||||||
// with the Middleware's one recorder, which New builds on the registry
|
// with the Middleware's one recorder, which New builds on the registry
|
||||||
// it is given: in the application, the one the /metrics route serves.
|
// the /metrics route serves and NewForTest on a registry of its own.
|
||||||
// Every call reuses that recorder, so any number of routers can
|
// Every call reuses that recorder, so any number of routers can
|
||||||
// install it.
|
// install it.
|
||||||
func (s *Middleware) Metrics() func(http.Handler) http.Handler {
|
func (s *Middleware) Metrics() func(http.Handler) http.Handler {
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"sneak.berlin/go/webhooker/internal/config"
|
"sneak.berlin/go/webhooker/internal/config"
|
||||||
"sneak.berlin/go/webhooker/internal/middleware"
|
"sneak.berlin/go/webhooker/internal/middleware"
|
||||||
"sneak.berlin/go/webhooker/internal/middleware/middlewaretest"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -71,8 +70,8 @@ func metricsTestRouter(
|
|||||||
Environment: "prod",
|
Environment: "prod",
|
||||||
ReceiverRateLimit: receiverLimit,
|
ReceiverRateLimit: receiverLimit,
|
||||||
}
|
}
|
||||||
m := middlewaretest.New(
|
m := middleware.NewForTest(
|
||||||
t, log, cfg, newTestSessionManager(t, cfg),
|
log, cfg, newTestSessionManager(cfg, log, nil),
|
||||||
)
|
)
|
||||||
|
|
||||||
reg := prometheus.NewRegistry()
|
reg := prometheus.NewRegistry()
|
||||||
@@ -456,11 +455,11 @@ func TestMetrics_StatusAndSizeStillRecorded(t *testing.T) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestMetrics_WorksOnMiddlewaretestNew pins that a Middleware built
|
// TestMetrics_WorksOnNewForTestMiddleware pins that a Middleware built
|
||||||
// by middlewaretest.New has a recorder of its own: its Metrics()
|
// by NewForTest has a recorder of its own: its Metrics() serves a
|
||||||
// serves a request instead of panicking, and a second one does not
|
// request instead of panicking, and a second one does not collide
|
||||||
// collide with the first.
|
// with the first.
|
||||||
func TestMetrics_WorksOnMiddlewaretestNew(t *testing.T) {
|
func TestMetrics_WorksOnNewForTestMiddleware(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
log := slog.New(slog.DiscardHandler)
|
log := slog.New(slog.DiscardHandler)
|
||||||
@@ -470,7 +469,7 @@ func TestMetrics_WorksOnMiddlewaretestNew(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
for range 2 {
|
for range 2 {
|
||||||
h := middlewaretest.New(t, log, cfg, nil).Metrics()(ok)
|
h := middleware.NewForTest(log, cfg, nil).Metrics()(ok)
|
||||||
|
|
||||||
req := httptest.NewRequestWithContext(
|
req := httptest.NewRequestWithContext(
|
||||||
t.Context(), http.MethodGet, okRoute, nil,
|
t.Context(), http.MethodGet, okRoute, nil,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
"sneak.berlin/go/webhooker/internal/config"
|
"sneak.berlin/go/webhooker/internal/config"
|
||||||
"sneak.berlin/go/webhooker/internal/globals"
|
"sneak.berlin/go/webhooker/internal/globals"
|
||||||
"sneak.berlin/go/webhooker/internal/logfield"
|
"sneak.berlin/go/webhooker/internal/logfield"
|
||||||
|
"sneak.berlin/go/webhooker/internal/logger"
|
||||||
"sneak.berlin/go/webhooker/internal/session"
|
"sneak.berlin/go/webhooker/internal/session"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -154,7 +155,7 @@ const (
|
|||||||
type MiddlewareParams struct {
|
type MiddlewareParams struct {
|
||||||
fx.In
|
fx.In
|
||||||
|
|
||||||
Logger *slog.Logger
|
Logger *logger.Logger
|
||||||
Globals *globals.Globals
|
Globals *globals.Globals
|
||||||
Config *config.Config
|
Config *config.Config
|
||||||
Session *session.Session
|
Session *session.Session
|
||||||
@@ -168,10 +169,12 @@ type Middleware struct {
|
|||||||
params *MiddlewareParams
|
params *MiddlewareParams
|
||||||
session *session.Session
|
session *session.Session
|
||||||
|
|
||||||
// metricsRecorder records the inbound HTTP metrics on
|
// metricsRecorder records the inbound HTTP metrics. New builds
|
||||||
// params.Registry. It is built once per Middleware and Metrics
|
// it on the registry /metrics serves, NewForTest on a registry
|
||||||
// reuses it, because building it registers its collectors, and a
|
// of its own. Either way it is built once per Middleware and
|
||||||
// second registration on the same registry panics.
|
// Metrics reuses it, because building it registers its
|
||||||
|
// collectors, and a second registration on the same registry
|
||||||
|
// panics.
|
||||||
metricsRecorder httpmetrics.Recorder
|
metricsRecorder httpmetrics.Recorder
|
||||||
|
|
||||||
// loginGuard counts failed credential verifications and bounds
|
// loginGuard counts failed credential verifications and bounds
|
||||||
@@ -190,7 +193,7 @@ func New(
|
|||||||
) (*Middleware, error) {
|
) (*Middleware, error) {
|
||||||
s := new(Middleware)
|
s := new(Middleware)
|
||||||
s.params = ¶ms
|
s.params = ¶ms
|
||||||
s.log = params.Logger
|
s.log = params.Logger.Get()
|
||||||
s.session = params.Session
|
s.session = params.Session
|
||||||
s.metricsRecorder = prommetrics.NewRecorder(
|
s.metricsRecorder = prommetrics.NewRecorder(
|
||||||
prommetrics.Config{Registry: params.Registry},
|
prommetrics.Config{Registry: params.Registry},
|
||||||
|
|||||||
@@ -14,34 +14,36 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"go.uber.org/fx/fxtest"
|
|
||||||
"sneak.berlin/go/webhooker/internal/config"
|
"sneak.berlin/go/webhooker/internal/config"
|
||||||
"sneak.berlin/go/webhooker/internal/database"
|
|
||||||
"sneak.berlin/go/webhooker/internal/middleware"
|
"sneak.berlin/go/webhooker/internal/middleware"
|
||||||
"sneak.berlin/go/webhooker/internal/middleware/middlewaretest"
|
|
||||||
"sneak.berlin/go/webhooker/internal/session"
|
"sneak.berlin/go/webhooker/internal/session"
|
||||||
)
|
)
|
||||||
|
|
||||||
const testKeySize = 32
|
const testKeySize = 32
|
||||||
|
|
||||||
// testMiddleware creates a Middleware with minimal dependencies
|
// testMiddleware creates a Middleware with minimal dependencies
|
||||||
// for testing. It uses a real session.Session.
|
// for testing. It uses a real session.Session backed by an
|
||||||
|
// in-memory cookie store.
|
||||||
func testMiddleware(
|
func testMiddleware(
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
env string,
|
env string,
|
||||||
) (*middleware.Middleware, *session.Session) {
|
) (*middleware.Middleware, *session.Session) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
return testMiddlewareWithIdleTimeout(t, env, 0)
|
m, s, _ := testMiddlewareWithSessionClock(t, env, 0, nil)
|
||||||
|
|
||||||
|
return m, s
|
||||||
}
|
}
|
||||||
|
|
||||||
// testMiddlewareWithIdleTimeout is testMiddleware with a
|
// testMiddlewareWithSessionClock is testMiddleware with a
|
||||||
// configurable session idle timeout, for the session-expiry tests.
|
// configurable session idle timeout and a manually advanced clock,
|
||||||
func testMiddlewareWithIdleTimeout(
|
// for the session-expiry tests. A nil clock uses the real one.
|
||||||
|
func testMiddlewareWithSessionClock(
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
env string,
|
env string,
|
||||||
idleTimeout time.Duration,
|
idleTimeout time.Duration,
|
||||||
) (*middleware.Middleware, *session.Session) {
|
clock *fakeClock,
|
||||||
|
) (*middleware.Middleware, *session.Session, *fakeClock) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
log := slog.New(slog.NewTextHandler(
|
log := slog.New(slog.NewTextHandler(
|
||||||
@@ -54,44 +56,59 @@ func testMiddlewareWithIdleTimeout(
|
|||||||
SessionIdleTimeout: idleTimeout,
|
SessionIdleTimeout: idleTimeout,
|
||||||
}
|
}
|
||||||
|
|
||||||
sessManager := newTestSessionManager(t, cfg)
|
sessManager := newTestSessionManager(cfg, log, clock)
|
||||||
|
|
||||||
m := middlewaretest.New(t, log, cfg, sessManager)
|
m := middleware.NewForTest(log, cfg, sessManager)
|
||||||
|
|
||||||
return m, sessManager
|
return m, sessManager, clock
|
||||||
}
|
}
|
||||||
|
|
||||||
// newTestSessionManager builds the real session.Session the
|
// newTestSessionManager builds the real session.Session the
|
||||||
// middleware tests run against, through session.New, with its key
|
// middleware tests run against: an in-memory cookie store with a
|
||||||
// in a main database of its own.
|
// known key, and optionally a manually advanced clock.
|
||||||
func newTestSessionManager(
|
func newTestSessionManager(
|
||||||
t *testing.T,
|
|
||||||
cfg *config.Config,
|
cfg *config.Config,
|
||||||
|
log *slog.Logger,
|
||||||
|
clock *fakeClock,
|
||||||
) *session.Session {
|
) *session.Session {
|
||||||
t.Helper()
|
key := make([]byte, testKeySize)
|
||||||
|
|
||||||
discard := slog.New(slog.DiscardHandler)
|
for i := range key {
|
||||||
|
key[i] = byte(i)
|
||||||
|
}
|
||||||
|
|
||||||
db, err := database.Open(t.TempDir(), discard)
|
store := session.NewStore(key)
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
t.Cleanup(func() { _ = db.Close() })
|
var now func() time.Time
|
||||||
|
|
||||||
lc := fxtest.NewLifecycle(t)
|
if clock != nil {
|
||||||
|
now = clock.Now
|
||||||
|
}
|
||||||
|
|
||||||
sessManager, err := session.New(lc, session.Params{
|
return session.NewForTest(store, cfg, log, key, now)
|
||||||
Config: cfg,
|
}
|
||||||
Database: db,
|
|
||||||
Logger: discard,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// The start hook reads the key from db and builds the cookie
|
// fakeClock is a manually advanced clock, so session expiry can be
|
||||||
// store.
|
// tested without sleeping.
|
||||||
lc.RequireStart()
|
type fakeClock struct {
|
||||||
t.Cleanup(lc.RequireStop)
|
t time.Time
|
||||||
|
}
|
||||||
|
|
||||||
return sessManager
|
func (c *fakeClock) Now() time.Time {
|
||||||
|
return c.t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakeClock) Advance(d time.Duration) {
|
||||||
|
c.t = c.t.Add(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
// newFakeClock returns a clock started at a fixed instant.
|
||||||
|
func newFakeClock() *fakeClock {
|
||||||
|
return &fakeClock{
|
||||||
|
t: time.Date(
|
||||||
|
2026, time.January, 2, 3, 4, 5, 0, time.UTC,
|
||||||
|
),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Logging Middleware Tests ---
|
// --- Logging Middleware Tests ---
|
||||||
@@ -566,40 +583,6 @@ func sessionCookies(
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// aged re-issues the session cookie in cookies with both of its
|
|
||||||
// timestamps moved back by d: the cookie as it stands once d has
|
|
||||||
// passed, so session expiry can be tested without sleeping.
|
|
||||||
func aged(
|
|
||||||
t *testing.T,
|
|
||||||
sessManager *session.Session,
|
|
||||||
cookies []*http.Cookie,
|
|
||||||
d time.Duration,
|
|
||||||
) []*http.Cookie {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
req := httptest.NewRequestWithContext(
|
|
||||||
context.Background(), http.MethodGet, "/", nil)
|
|
||||||
|
|
||||||
for _, c := range cookies {
|
|
||||||
req.AddCookie(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
sess, err := sessManager.Get(req)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
for _, key := range []string{session.CreatedAtKey, session.LastSeenKey} {
|
|
||||||
at, ok := sess.Values[key].(int64)
|
|
||||||
require.True(t, ok, "the session has no %s", key)
|
|
||||||
|
|
||||||
sess.Values[key] = at - int64(d/time.Second)
|
|
||||||
}
|
|
||||||
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
require.NoError(t, sessManager.Save(req, w, sess))
|
|
||||||
|
|
||||||
return sessionCookies(w)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRequireAuth_IdleExpiredSession_RedirectsToLogin(
|
func TestRequireAuth_IdleExpiredSession_RedirectsToLogin(
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
) {
|
) {
|
||||||
@@ -607,11 +590,13 @@ func TestRequireAuth_IdleExpiredSession_RedirectsToLogin(
|
|||||||
|
|
||||||
idle := time.Hour
|
idle := time.Hour
|
||||||
|
|
||||||
m, sessManager := testMiddlewareWithIdleTimeout(
|
m, sessManager, clock := testMiddlewareWithSessionClock(
|
||||||
t, config.EnvironmentDev, idle,
|
t, config.EnvironmentDev, idle, newFakeClock(),
|
||||||
)
|
)
|
||||||
|
|
||||||
cookies := aged(t, sessManager, loginCookies(t, sessManager), idle)
|
cookies := loginCookies(t, sessManager)
|
||||||
|
|
||||||
|
clock.Advance(idle)
|
||||||
|
|
||||||
called, w := runAuthed(t, m, cookies)
|
called, w := runAuthed(t, m, cookies)
|
||||||
|
|
||||||
@@ -636,12 +621,14 @@ func TestRequireAuth_RefreshesIdleDeadlineOnActivity(
|
|||||||
|
|
||||||
idle := time.Hour
|
idle := time.Hour
|
||||||
|
|
||||||
m, sessManager := testMiddlewareWithIdleTimeout(
|
m, sessManager, clock := testMiddlewareWithSessionClock(
|
||||||
t, config.EnvironmentDev, idle,
|
t, config.EnvironmentDev, idle, newFakeClock(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
cookies := loginCookies(t, sessManager)
|
||||||
|
|
||||||
// Activity halfway through the idle window.
|
// Activity halfway through the idle window.
|
||||||
cookies := aged(t, sessManager, loginCookies(t, sessManager), idle/2)
|
clock.Advance(idle / 2)
|
||||||
|
|
||||||
called, w := runAuthed(t, m, cookies)
|
called, w := runAuthed(t, m, cookies)
|
||||||
require.True(t, called, "handler should run while valid")
|
require.True(t, called, "handler should run while valid")
|
||||||
@@ -653,22 +640,16 @@ func TestRequireAuth_RefreshesIdleDeadlineOnActivity(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Past the original deadline. The refreshed cookie is still
|
// Past the original deadline. The refreshed cookie is still
|
||||||
// good; the original one is not. A minute short of the idle
|
// good; the original one is not.
|
||||||
// window leaves room for the real clock, which the session
|
clock.Advance(idle - time.Second)
|
||||||
// reads, to tick on while the test runs.
|
|
||||||
later := idle - time.Minute
|
|
||||||
|
|
||||||
calledRefreshed, _ := runAuthed(
|
calledRefreshed, _ := runAuthed(t, m, refreshed)
|
||||||
t, m, aged(t, sessManager, refreshed, later),
|
|
||||||
)
|
|
||||||
assert.True(
|
assert.True(
|
||||||
t, calledRefreshed,
|
t, calledRefreshed,
|
||||||
"refreshed session should outlive the original deadline",
|
"refreshed session should outlive the original deadline",
|
||||||
)
|
)
|
||||||
|
|
||||||
calledStale, staleW := runAuthed(
|
calledStale, staleW := runAuthed(t, m, cookies)
|
||||||
t, m, aged(t, sessManager, cookies, later),
|
|
||||||
)
|
|
||||||
assert.False(
|
assert.False(
|
||||||
t, calledStale,
|
t, calledStale,
|
||||||
"the pre-refresh cookie carries the old idle deadline",
|
"the pre-refresh cookie carries the old idle deadline",
|
||||||
@@ -681,8 +662,8 @@ func TestRequireAuth_UnauthenticatedRequestDoesNotRefresh(
|
|||||||
) {
|
) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
m, sessManager := testMiddlewareWithIdleTimeout(
|
m, sessManager, _ := testMiddlewareWithSessionClock(
|
||||||
t, config.EnvironmentDev, time.Hour,
|
t, config.EnvironmentDev, time.Hour, newFakeClock(),
|
||||||
)
|
)
|
||||||
|
|
||||||
// A session cookie that exists but was never authenticated.
|
// A session cookie that exists but was never authenticated.
|
||||||
@@ -943,9 +924,12 @@ func metricsAuthMiddleware(
|
|||||||
MetricsPassword: "secret",
|
MetricsPassword: "secret",
|
||||||
}
|
}
|
||||||
|
|
||||||
return middlewaretest.New(
|
key := make([]byte, testKeySize)
|
||||||
t, log, cfg, newTestSessionManager(t, cfg),
|
store := session.NewStore(key)
|
||||||
)
|
|
||||||
|
sessManager := session.NewForTest(store, cfg, log, key, nil)
|
||||||
|
|
||||||
|
return middleware.NewForTest(log, cfg, sessManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
// runMetricsAuthRequest sends a GET /metrics request with the
|
// runMetricsAuthRequest sends a GET /metrics request with the
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
// Package middlewaretest builds a Middleware for tests in other
|
|
||||||
// packages.
|
|
||||||
package middlewaretest
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log/slog"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"go.uber.org/fx/fxtest"
|
|
||||||
"sneak.berlin/go/webhooker/internal/config"
|
|
||||||
"sneak.berlin/go/webhooker/internal/middleware"
|
|
||||||
"sneak.berlin/go/webhooker/internal/session"
|
|
||||||
)
|
|
||||||
|
|
||||||
// New builds a Middleware through middleware.New, on a
|
|
||||||
// lifecycle that is never started.
|
|
||||||
//
|
|
||||||
// Its metrics recorder writes to a fresh registry of its own, so
|
|
||||||
// Metrics() works on it and two of them never collide.
|
|
||||||
func New(
|
|
||||||
t *testing.T,
|
|
||||||
log *slog.Logger,
|
|
||||||
cfg *config.Config,
|
|
||||||
sess *session.Session,
|
|
||||||
) *middleware.Middleware {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
m, err := middleware.New(
|
|
||||||
fxtest.NewLifecycle(t),
|
|
||||||
middleware.MiddlewareParams{
|
|
||||||
Logger: log,
|
|
||||||
Config: cfg,
|
|
||||||
Session: sess,
|
|
||||||
Registry: prometheus.NewRegistry(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
@@ -18,7 +18,6 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"sneak.berlin/go/webhooker/internal/config"
|
"sneak.berlin/go/webhooker/internal/config"
|
||||||
"sneak.berlin/go/webhooker/internal/middleware"
|
"sneak.berlin/go/webhooker/internal/middleware"
|
||||||
"sneak.berlin/go/webhooker/internal/middleware/middlewaretest"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPostRateLimit_AllowsGET(t *testing.T) {
|
func TestPostRateLimit_AllowsGET(t *testing.T) {
|
||||||
@@ -199,7 +198,7 @@ func rateLimitMiddleware(
|
|||||||
&slog.HandlerOptions{Level: slog.LevelDebug},
|
&slog.HandlerOptions{Level: slog.LevelDebug},
|
||||||
))
|
))
|
||||||
|
|
||||||
return middlewaretest.New(t, log, cfg, nil)
|
return middleware.NewForTest(log, cfg, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// trustedProxies parses CIDR strings for a test Config.
|
// trustedProxies parses CIDR strings for a test Config.
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
prommetrics "github.com/slok/go-http-metrics/metrics/prometheus"
|
||||||
|
"sneak.berlin/go/webhooker/internal/config"
|
||||||
|
"sneak.berlin/go/webhooker/internal/session"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewForTest creates a Middleware with the minimum dependencies
|
||||||
|
// needed for testing. This bypasses the fx lifecycle.
|
||||||
|
//
|
||||||
|
// Its metrics recorder writes to a fresh registry of its own, so
|
||||||
|
// Metrics() works on it and two of them never collide.
|
||||||
|
func NewForTest(
|
||||||
|
log *slog.Logger,
|
||||||
|
cfg *config.Config,
|
||||||
|
sess *session.Session,
|
||||||
|
) *Middleware {
|
||||||
|
return &Middleware{
|
||||||
|
log: log,
|
||||||
|
params: &MiddlewareParams{
|
||||||
|
Config: cfg,
|
||||||
|
},
|
||||||
|
session: sess,
|
||||||
|
metricsRecorder: prommetrics.NewRecorder(
|
||||||
|
prommetrics.Config{Registry: prometheus.NewRegistry()},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -174,7 +174,6 @@ func newServerApp(
|
|||||||
fx.Provide(
|
fx.Provide(
|
||||||
globals.New,
|
globals.New,
|
||||||
logger.New,
|
logger.New,
|
||||||
func(l *logger.Logger) *slog.Logger { return l.Get() },
|
|
||||||
func() *config.Config {
|
func() *config.Config {
|
||||||
return &config.Config{DataDir: dir}
|
return &config.Config{DataDir: dir}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -280,23 +280,6 @@ func click(ctx context.Context, t *testing.T, xpath string) {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
// clickAndLoad clicks the link or button matching an XPath expression
|
|
||||||
// and waits, as loadPage does, for the page the click opens to load and
|
|
||||||
// for Alpine.js to start on it. Reading earlier, a check can find an
|
|
||||||
// element of the page being left, gone by the time its value is read;
|
|
||||||
// and the wait in shown is too short for a page load on a busy host.
|
|
||||||
func clickAndLoad(ctx context.Context, t *testing.T, xpath string) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
_, err := chromedp.RunResponse(
|
|
||||||
ctx, chromedp.Click(xpath, chromedp.BySearch),
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NoError(t, chromedp.Run(
|
|
||||||
ctx, chromedp.WaitNotPresent("[x-cloak]", chromedp.ByQuery),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkAddEntrypoint loads a webhook page and checks that the add
|
// checkAddEntrypoint loads a webhook page and checks that the add
|
||||||
// entrypoint form stays hidden until the Add button beside its heading
|
// entrypoint form stays hidden until the Add button beside its heading
|
||||||
// is clicked. The click looks for a button element there, so it also
|
// is clicked. The click looks for a button element there, so it also
|
||||||
@@ -439,7 +422,7 @@ func checkAddTarget(
|
|||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
clickAndLoad(ctx, t, saveButton)
|
click(ctx, t, saveButton)
|
||||||
assert.Truef(t, shown(ctx, `//span[text()="`+name+
|
assert.Truef(t, shown(ctx, `//span[text()="`+name+
|
||||||
`"]/following-sibling::div/span[text()="`+badge+`"]`),
|
`"]/following-sibling::div/span[text()="`+badge+`"]`),
|
||||||
"%s: the added target is not listed as %s", targetType, badge)
|
"%s: the added target is not listed as %s", targetType, badge)
|
||||||
@@ -502,9 +485,10 @@ func checkArchiveChoices(ctx context.Context, t *testing.T, url string) {
|
|||||||
`/following-sibling::span[text()="daily"]`),
|
`/following-sibling::span[text()="daily"]`),
|
||||||
"a database target added with daily is not listed as daily")
|
"a database target added with daily is not listed as daily")
|
||||||
|
|
||||||
clickAndLoad(ctx, t, row+`//a[text()="Edit"]`)
|
click(ctx, t, row+`//a[text()="Edit"]`)
|
||||||
require.NoError(t, chromedp.Run(
|
require.NoError(t, chromedp.Run(
|
||||||
ctx,
|
ctx,
|
||||||
|
chromedp.WaitReady("#expiry", chromedp.ByQuery),
|
||||||
chromedp.Value("#expiry", &editedExpiry, chromedp.ByQuery),
|
chromedp.Value("#expiry", &editedExpiry, chromedp.ByQuery),
|
||||||
chromedp.Value("#rotation", &editedRotation, chromedp.ByQuery),
|
chromedp.Value("#rotation", &editedRotation, chromedp.ByQuery),
|
||||||
))
|
))
|
||||||
@@ -539,7 +523,7 @@ func checkRefusedTarget(ctx context.Context, t *testing.T, url string) {
|
|||||||
chromedp.Click(forwardQuery, chromedp.ByQuery),
|
chromedp.Click(forwardQuery, chromedp.ByQuery),
|
||||||
))
|
))
|
||||||
|
|
||||||
clickAndLoad(ctx, t, saveButton)
|
click(ctx, t, saveButton)
|
||||||
|
|
||||||
assert.True(t, shown(ctx, reason),
|
assert.True(t, shown(ctx, reason),
|
||||||
"a refused target does not show the reason")
|
"a refused target does not show the reason")
|
||||||
@@ -652,7 +636,7 @@ func checkRefusedEdits(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
clickAndLoad(ctx, t, `//button[text()="Save Changes"]`)
|
click(ctx, t, `//button[text()="Save Changes"]`)
|
||||||
|
|
||||||
assert.Truef(t, shown(ctx, reason),
|
assert.Truef(t, shown(ctx, reason),
|
||||||
"%s: a refused save does not show the reason", edit.url)
|
"%s: a refused save does not show the reason", edit.url)
|
||||||
@@ -776,7 +760,7 @@ func checkEntrypointEdit(
|
|||||||
require.NoError(t, chromedp.Run(
|
require.NoError(t, chromedp.Run(
|
||||||
ctx, chromedp.SendKeys(input, "Billing sender", chromedp.ByQuery),
|
ctx, chromedp.SendKeys(input, "Billing sender", chromedp.ByQuery),
|
||||||
))
|
))
|
||||||
clickAndLoad(ctx, t, saveEdit)
|
click(ctx, t, saveEdit)
|
||||||
|
|
||||||
assert.True(t, shown(ctx, `//span[text()="Billing sender"]`),
|
assert.True(t, shown(ctx, `//span[text()="Billing sender"]`),
|
||||||
"saving the edit form does not change the description")
|
"saving the edit form does not change the description")
|
||||||
@@ -814,7 +798,7 @@ func checkRecentEvents(ctx context.Context, t *testing.T, url string) {
|
|||||||
"clicking the newest event does not collapse it")
|
"clicking the newest event does not collapse it")
|
||||||
|
|
||||||
require.NoError(t, chromedp.Run(ctx, loadPage(url)))
|
require.NoError(t, chromedp.Run(ctx, loadPage(url)))
|
||||||
clickAndLoad(ctx, t, newest+`/ancestor::div[@x-data][1]//a[text()="Open"]`)
|
click(ctx, t, newest+`/ancestor::div[@x-data][1]//a[text()="Open"]`)
|
||||||
|
|
||||||
assert.True(t, shown(ctx, `//h2[text()="Body"]`),
|
assert.True(t, shown(ctx, `//h2[text()="Body"]`),
|
||||||
"Open does not lead to the event's own page")
|
"Open does not lead to the event's own page")
|
||||||
@@ -1215,7 +1199,7 @@ func checkNewWebhookTargets(
|
|||||||
`","rotation":"none"}`
|
`","rotation":"none"}`
|
||||||
}
|
}
|
||||||
|
|
||||||
clickAndLoad(ctx, t, createButton)
|
click(ctx, t, createButton)
|
||||||
require.Truef(t, shown(ctx, `//h1[text()="`+name+`"]`),
|
require.Truef(t, shown(ctx, `//h1[text()="`+name+`"]`),
|
||||||
"%s: the new webhook's page does not open", name)
|
"%s: the new webhook's page does not open", name)
|
||||||
|
|
||||||
@@ -1276,7 +1260,7 @@ func checkRefusedNewWebhook(ctx context.Context, t *testing.T, url string) {
|
|||||||
chromedp.SetValue(pruningChoice, "2160h", chromedp.BySearch),
|
chromedp.SetValue(pruningChoice, "2160h", chromedp.BySearch),
|
||||||
chromedp.SetValue("#archive_rotation", "monthly", chromedp.ByQuery),
|
chromedp.SetValue("#archive_rotation", "monthly", chromedp.ByQuery),
|
||||||
))
|
))
|
||||||
clickAndLoad(ctx, t, createButton)
|
click(ctx, t, createButton)
|
||||||
|
|
||||||
assert.True(t, shown(ctx, `//div[@class="alert-error"]`),
|
assert.True(t, shown(ctx, `//div[@class="alert-error"]`),
|
||||||
"a refused webhook does not show the reason")
|
"a refused webhook does not show the reason")
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package server_test
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"html"
|
"html"
|
||||||
"log/slog"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -131,7 +130,6 @@ func newTestEnvWithConfig(
|
|||||||
fx.Provide(
|
fx.Provide(
|
||||||
globals.New,
|
globals.New,
|
||||||
logger.New,
|
logger.New,
|
||||||
func(l *logger.Logger) *slog.Logger { return l.Get() },
|
|
||||||
func() *config.Config { return cfg },
|
func() *config.Config { return cfg },
|
||||||
database.New,
|
database.New,
|
||||||
database.NewWebhookDBManager,
|
database.NewWebhookDBManager,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"go.uber.org/fx"
|
"go.uber.org/fx"
|
||||||
"sneak.berlin/go/webhooker/internal/config"
|
"sneak.berlin/go/webhooker/internal/config"
|
||||||
"sneak.berlin/go/webhooker/internal/database"
|
"sneak.berlin/go/webhooker/internal/database"
|
||||||
|
"sneak.berlin/go/webhooker/internal/logger"
|
||||||
"sneak.berlin/go/webhooker/internal/reqtls"
|
"sneak.berlin/go/webhooker/internal/reqtls"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -79,7 +80,7 @@ type Params struct {
|
|||||||
|
|
||||||
Config *config.Config
|
Config *config.Config
|
||||||
Database *database.Database
|
Database *database.Database
|
||||||
Logger *slog.Logger
|
Logger *logger.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// Session manages encrypted session storage.
|
// Session manages encrypted session storage.
|
||||||
@@ -179,7 +180,7 @@ func New(
|
|||||||
params Params,
|
params Params,
|
||||||
) (*Session, error) {
|
) (*Session, error) {
|
||||||
s := &Session{
|
s := &Session{
|
||||||
log: params.Logger,
|
log: params.Logger.Get(),
|
||||||
idleTimeout: params.Config.SessionIdleTimeout,
|
idleTimeout: params.Config.SessionIdleTimeout,
|
||||||
now: time.Now,
|
now: time.Now,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ func NewStore(key []byte) *sessions.CookieStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewForTest creates a Session with a pre-configured cookie store for use
|
// NewForTest creates a Session with a pre-configured cookie store for use
|
||||||
// in tests. This bypasses the fx lifecycle and database dependency. The key
|
// in tests. This bypasses the fx lifecycle and database dependency, allowing
|
||||||
|
// middleware and handler tests to use real session functionality. The key
|
||||||
// parameter is the raw 32-byte authentication key used for session encryption
|
// parameter is the raw 32-byte authentication key used for session encryption
|
||||||
// and CSRF cookie signing.
|
// and CSRF cookie signing.
|
||||||
//
|
//
|
||||||
+1
-9
@@ -11,7 +11,6 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
|||||||
|
|
||||||
PKGMGR=""
|
PKGMGR=""
|
||||||
SUDO=""
|
SUDO=""
|
||||||
APT_UPDATED=""
|
|
||||||
|
|
||||||
detect_pkgmgr() {
|
detect_pkgmgr() {
|
||||||
[ -n "$PKGMGR" ] && return 0
|
[ -n "$PKGMGR" ] && return 0
|
||||||
@@ -40,14 +39,7 @@ pkg_install() {
|
|||||||
detect_pkgmgr
|
detect_pkgmgr
|
||||||
case "$PKGMGR" in
|
case "$PKGMGR" in
|
||||||
nix) nix-env -iA "nixpkgs.$1" ;;
|
nix) nix-env -iA "nixpkgs.$1" ;;
|
||||||
apt)
|
apt) $SUDO env DEBIAN_FRONTEND=noninteractive apt-get install -y "$2" ;;
|
||||||
# Package lists may be empty (fresh images); refresh once per run.
|
|
||||||
if [ -z "$APT_UPDATED" ]; then
|
|
||||||
$SUDO env DEBIAN_FRONTEND=noninteractive apt-get update
|
|
||||||
APT_UPDATED=1
|
|
||||||
fi
|
|
||||||
$SUDO env DEBIAN_FRONTEND=noninteractive apt-get install -y "$2"
|
|
||||||
;;
|
|
||||||
brew) brew install "$3" ;;
|
brew) brew install "$3" ;;
|
||||||
apk) apk add --no-cache "$4" ;;
|
apk) apk add --no-cache "$4" ;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
Reference in New Issue
Block a user