156 lines
4.3 KiB
Go
156 lines
4.3 KiB
Go
package delivery_test
|
|
|
|
import (
|
|
"bytes"
|
|
"log"
|
|
"log/slog"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/gorm"
|
|
gormlogger "gorm.io/gorm/logger"
|
|
"sneak.berlin/go/webhooker/internal/delivery"
|
|
"sneak.berlin/go/webhooker/internal/middleware"
|
|
)
|
|
|
|
// archiveGORMTailMarker sits at the far end of the value this file
|
|
// drives into an archive lookup. Its presence in a log line means the
|
|
// whole value reached the log, so nothing truncated it.
|
|
const archiveGORMTailMarker = "ENDOFCLIENTVALUE"
|
|
|
|
// archiveGORMFillBytes is how much text the lookup carries. It is far
|
|
// past every budget in play.
|
|
const archiveGORMFillBytes = 8 << 10
|
|
|
|
// gormDefaultBuf collects what GORM's package-level default logger
|
|
// writes, if anything reaches it.
|
|
type gormDefaultBuf struct {
|
|
mu sync.Mutex
|
|
b bytes.Buffer
|
|
}
|
|
|
|
func (g *gormDefaultBuf) Write(p []byte) (int, error) {
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
|
|
return g.b.Write(p)
|
|
}
|
|
|
|
func (g *gormDefaultBuf) String() string {
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
|
|
return g.b.String()
|
|
}
|
|
|
|
// captureArchiveGORMDefault replaces GORM's package-level default
|
|
// logger with one configured exactly as GORM configures its own,
|
|
// writing to a buffer.
|
|
//
|
|
// This duplicates the detector in internal/handlers rather than
|
|
// sharing it: a test helper cannot cross a package's test boundary
|
|
// without exporting production code to carry it, and a logging
|
|
// detector is not worth a production symbol. What it detects is the
|
|
// third gorm.Open in this service, at
|
|
// internal/delivery/target_database_archive.go — the archive writer,
|
|
// whose type is unexported, so nothing outside this package can drive
|
|
// it.
|
|
func captureArchiveGORMDefault(t *testing.T) *gormDefaultBuf {
|
|
t.Helper()
|
|
|
|
buf := &gormDefaultBuf{}
|
|
orig := gormlogger.Default
|
|
|
|
gormlogger.Default = gormlogger.New(
|
|
log.New(buf, "", log.LstdFlags),
|
|
gormlogger.Config{
|
|
SlowThreshold: 200 * time.Millisecond,
|
|
LogLevel: gormlogger.Warn,
|
|
IgnoreRecordNotFoundError: false,
|
|
Colorful: false,
|
|
},
|
|
)
|
|
|
|
t.Cleanup(func() { gormlogger.Default = orig })
|
|
|
|
return buf
|
|
}
|
|
|
|
// TestArchiveWriter_NeverUsesGORMsDefaultLogger pins the archive
|
|
// writer's gorm.Open to the adapter.
|
|
//
|
|
// Restore a bare &gorm.Config{} at
|
|
// internal/delivery/target_database_archive.go and this fails: the
|
|
// default logger prints the fully interpolated SELECT on every
|
|
// ErrRecordNotFound, so the client-chosen event id below arrives whole
|
|
// and unbounded on stdout, answering to no level the operator set.
|
|
//
|
|
// Not parallel: gormlogger.Default is process-global. Go runs every
|
|
// non-parallel top-level test to completion before it resumes the
|
|
// parallel ones.
|
|
//
|
|
//nolint:paralleltest // Deliberately sequential; see above.
|
|
func TestArchiveWriter_NeverUsesGORMsDefaultLogger(t *testing.T) {
|
|
var captured bytes.Buffer
|
|
|
|
gormDefault := captureArchiveGORMDefault(t)
|
|
|
|
w := delivery.NewExportArchiveWriter(
|
|
filepath.Join(t.TempDir(), "archive.db"),
|
|
slog.New(slog.NewTextHandler(
|
|
&captured, &slog.HandlerOptions{Level: slog.LevelDebug},
|
|
)),
|
|
0,
|
|
)
|
|
|
|
require.NoError(t, w.Open(0))
|
|
|
|
t.Cleanup(w.Evict)
|
|
|
|
// A lookup that misses, carrying a value the size of an inbound
|
|
// event id. Under the default logger this is the line that gets
|
|
// interpolated and printed.
|
|
value := strings.Repeat("\x01", archiveGORMFillBytes) +
|
|
archiveGORMTailMarker
|
|
|
|
var row delivery.ExportArchivedEvent
|
|
|
|
err := w.DB().Where("event_id = ?", value).First(&row).Error
|
|
require.ErrorIs(t, err, gorm.ErrRecordNotFound)
|
|
|
|
got := gormDefault.String()
|
|
assert.Empty(
|
|
t, got,
|
|
"GORM's default logger wrote %d bytes, so the archive "+
|
|
"writer's gorm.Open is back on a bare &gorm.Config{}; "+
|
|
"the first of them: %s",
|
|
len(got), got[:min(len(got), 300)],
|
|
)
|
|
|
|
// The adapter drops a miss, so this should be silent too — and
|
|
// whatever it does write stays inside the stated ceiling.
|
|
out := captured.String()
|
|
|
|
assert.NotContains(
|
|
t, out, archiveGORMTailMarker,
|
|
"the far end of the client-chosen value reached the log",
|
|
)
|
|
|
|
for line := range strings.SplitSeq(strings.TrimRight(out, "\n"), "\n") {
|
|
if line == "" {
|
|
continue
|
|
}
|
|
|
|
assert.LessOrEqual(
|
|
t, len(line), middleware.MaxAccessLogLineBytes,
|
|
"log line exceeded its bound: %s",
|
|
line[:min(len(line), 300)],
|
|
)
|
|
}
|
|
}
|