mirror of
https://github.com/MeshTender/MeshTender.git
synced 2026-09-09 13:15:33 +00:00
184 lines
5.8 KiB
Go
184 lines
5.8 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"log/slog"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// fakePruner records each sweep on a channel, so assertions never race with the
|
|
// janitor goroutine. err, when set, is returned every call.
|
|
type fakePruner struct {
|
|
calls chan context.Context
|
|
err error
|
|
}
|
|
|
|
func newFakePruner(err error) *fakePruner {
|
|
return &fakePruner{calls: make(chan context.Context, 64), err: err}
|
|
}
|
|
|
|
func (f *fakePruner) PruneAuthCodes(ctx context.Context) (int64, error) {
|
|
select {
|
|
case f.calls <- ctx:
|
|
default: // never block the loop under test
|
|
}
|
|
if f.err != nil {
|
|
return 0, f.err
|
|
}
|
|
return 1, nil
|
|
}
|
|
|
|
func discardLogger() *slog.Logger {
|
|
return slog.New(slog.NewTextHandler(io.Discard, nil))
|
|
}
|
|
|
|
// waitForCalls waits for at least n sweeps, failing if they don't arrive.
|
|
func waitForCalls(t *testing.T, f *fakePruner, n int, within time.Duration) {
|
|
t.Helper()
|
|
deadline := time.After(within)
|
|
for i := 0; i < n; i++ {
|
|
select {
|
|
case <-f.calls:
|
|
case <-deadline:
|
|
t.Fatalf("only %d of %d expected sweeps happened within %v", i, n, within)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestJanitorSweepsImmediately: a restart must clean up without waiting out
|
|
// a full interval, so the loop sweeps once on entry. The interval here is long
|
|
// enough that a tick can't be what we observed.
|
|
func TestJanitorSweepsImmediately(t *testing.T) {
|
|
t.Parallel()
|
|
f := newFakePruner(nil)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
go runJanitor(ctx, time.Hour, discardLogger(), janitorSweep{"test", f.PruneAuthCodes})
|
|
|
|
waitForCalls(t, f, 1, 2*time.Second)
|
|
}
|
|
|
|
// TestJanitorRepeats: the ticker keeps firing, so cleanup is ongoing rather
|
|
// than startup-only.
|
|
func TestJanitorRepeats(t *testing.T) {
|
|
t.Parallel()
|
|
f := newFakePruner(nil)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
go runJanitor(ctx, 2*time.Millisecond, discardLogger(), janitorSweep{"test", f.PruneAuthCodes})
|
|
|
|
// One immediate sweep plus several ticks.
|
|
waitForCalls(t, f, 4, 2*time.Second)
|
|
}
|
|
|
|
// TestJanitorStopsOnCancel is the one that matters for shutdown: main waits
|
|
// on <-janitorDone, so a loop that ignored ctx would hang the process forever
|
|
// instead of exiting.
|
|
func TestJanitorStopsOnCancel(t *testing.T) {
|
|
t.Parallel()
|
|
f := newFakePruner(nil)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
runJanitor(ctx, time.Millisecond, discardLogger(), janitorSweep{"test", f.PruneAuthCodes})
|
|
}()
|
|
|
|
waitForCalls(t, f, 1, 2*time.Second) // running
|
|
cancel()
|
|
|
|
select {
|
|
case <-done:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("runJanitor did not return after ctx was cancelled — shutdown would hang")
|
|
}
|
|
}
|
|
|
|
// TestJanitorSurvivesErrors: a transient DB failure must not kill the
|
|
// janitor for the life of the process; it logs and tries again next tick.
|
|
func TestJanitorSurvivesErrors(t *testing.T) {
|
|
t.Parallel()
|
|
f := newFakePruner(errors.New("connection refused"))
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
go runJanitor(ctx, 2*time.Millisecond, discardLogger(), janitorSweep{"test", f.PruneAuthCodes})
|
|
|
|
// Still sweeping well after the first failure.
|
|
waitForCalls(t, f, 4, 2*time.Second)
|
|
}
|
|
|
|
// TestJanitorRunsEverySweep: the janitor drives several cleanups, so each one must
|
|
// run on every pass — and a sweep that fails must not stop the sweeps after it, or a
|
|
// single sick table would silently stall unrelated cleanup for the whole process
|
|
// lifetime.
|
|
func TestJanitorRunsEverySweep(t *testing.T) {
|
|
t.Parallel()
|
|
broken := newFakePruner(errors.New("relation does not exist"))
|
|
healthy := newFakePruner(nil)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
// Broken first, so the healthy one only runs if a failure doesn't abort the pass.
|
|
go runJanitor(ctx, 2*time.Millisecond, discardLogger(),
|
|
janitorSweep{"broken", broken.PruneAuthCodes},
|
|
janitorSweep{"healthy", healthy.PruneAuthCodes},
|
|
)
|
|
|
|
waitForCalls(t, broken, 2, 2*time.Second)
|
|
waitForCalls(t, healthy, 2, 2*time.Second)
|
|
}
|
|
|
|
// TestConnectionTimeoutsAreSet pins audit O1: every phase of a connection is bounded.
|
|
// Zero means "no limit" in net/http, so a single omission silently reopens the hole
|
|
// this closed, and nothing else would notice.
|
|
func TestConnectionTimeoutsAreSet(t *testing.T) {
|
|
t.Parallel()
|
|
for _, c := range []struct {
|
|
name string
|
|
got time.Duration
|
|
}{
|
|
{"readHeaderTimeout", readHeaderTimeout},
|
|
{"readTimeout", readTimeout},
|
|
{"writeTimeout", writeTimeout},
|
|
{"idleTimeout", idleTimeout},
|
|
} {
|
|
if c.got <= 0 {
|
|
t.Errorf("%s = %v; zero means unlimited, so a slow or idle peer can hold a "+
|
|
"connection open forever", c.name, c.got)
|
|
}
|
|
}
|
|
|
|
// Headers must not outlast the whole request they belong to.
|
|
if readHeaderTimeout > readTimeout {
|
|
t.Errorf("readHeaderTimeout (%v) exceeds readTimeout (%v)", readHeaderTimeout, readTimeout)
|
|
}
|
|
// The write budget has to cover a real download on a bad link. The largest asset is
|
|
// a few hundred KB, so anything under ~30s risks truncating legitimate responses for
|
|
// exactly the rural, marginal connections this product targets.
|
|
if writeTimeout < 30*time.Second {
|
|
t.Errorf("writeTimeout = %v; too tight for a large asset over a slow link", writeTimeout)
|
|
}
|
|
}
|
|
|
|
// TestJanitorIntervalOutlivesCodeTTL: the sweep only has to keep the table
|
|
// small, but an interval shorter than the code TTL would mean pointless work, and a
|
|
// wildly long one would defeat the purpose. Pin it to a sane band so a future edit
|
|
// has to think about it.
|
|
func TestJanitorIntervalOutlivesCodeTTL(t *testing.T) {
|
|
t.Parallel()
|
|
if janitorInterval < time.Minute {
|
|
t.Errorf("janitorInterval = %v; shorter than the 60s code TTL means "+
|
|
"sweeping rows that were never expirable", janitorInterval)
|
|
}
|
|
if janitorInterval > time.Hour {
|
|
t.Errorf("janitorInterval = %v; too long to keep the table small", janitorInterval)
|
|
}
|
|
}
|