Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ require (
github.com/charmbracelet/x/ansi v0.11.8
github.com/charmbracelet/x/exp/teatest v0.0.0-20260803091719-3755ebad01b1
github.com/goccy/go-yaml v1.19.2
github.com/gofrs/flock v0.13.0
github.com/google/renameio/v2 v2.0.2
github.com/jedisct1/go-minisign v0.0.0-20260527172527-a09352b57a22
github.com/klauspost/compress v1.19.2
github.com/minio/minio-go/v7 v7.3.0
Expand Down
4 changes: 0 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,6 @@ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
github.com/google/renameio/v2 v2.0.2 h1:qKZs+tfn+arruZZhQ7TKC/ergJunuJicWS6gLDt/dGw=
github.com/google/renameio/v2 v2.0.2/go.mod h1:OX+G6WHHpHq3NVj7cAOleLOwJfcQ1s3uUJQCrr78SWo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
Expand Down
7 changes: 7 additions & 0 deletions internal/adapters/health/export_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package health

import "time"

// WithClock freezes the waiter's notion of now, so a test asserting on a
// deadline does not have to wait for one.
func (w *Waiter) WithClock(clock func() time.Time) *Waiter { return w.withClock(clock) }
8 changes: 6 additions & 2 deletions internal/adapters/health/health.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,8 +255,12 @@ func (w *Waiter) now() time.Time {
return w.clock()
}

// WithClock overrides the waiter's notion of now. Tests only.
func (w *Waiter) WithClock(clock func() time.Time) *Waiter {
// withClock overrides the waiter's notion of now.
//
// Unexported: nothing in production sets it -- now() falls back to time.Now
// when it is nil -- and a clock only tests pass is a clock no test exercises as
// production leaves it.
func (w *Waiter) withClock(clock func() time.Time) *Waiter {
w.clock = clock
return w
}
Expand Down
6 changes: 0 additions & 6 deletions internal/adapters/runtime/compose/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,6 @@ func New(runner exec.Runner, opts ...Option) *Runtime {

type Option func(*Runtime)

// WithDockerBinary overrides the docker executable, for tests and for hosts
// where it is not on the default PATH.
func WithDockerBinary(path string) Option {
return func(r *Runtime) { r.docker = path }
}

// WithOutputSink forwards subprocess output.
func WithOutputSink(fn func(exec.Line)) Option {
return func(r *Runtime) { r.onLine = fn }
Expand Down
7 changes: 4 additions & 3 deletions internal/adapters/runtime/compose/compose_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/morzecrew/morzer/internal/domain"
"github.com/morzecrew/morzer/internal/infra/exec"
"github.com/morzecrew/morzer/internal/ports"
"github.com/morzecrew/morzer/test/fakes"
)

// The acceptance run drives this adapter against real Docker, which proves the
Expand All @@ -21,9 +22,9 @@ import (
// A scripted runner supplies those answers. What is under test is the adapter's
// reading of them -- which is all this adapter is.

func newRuntime() (*compose.Runtime, *exec.Scripted) {
runner := exec.NewScripted()
return compose.New(runner, compose.WithDockerBinary("/usr/bin/docker")), runner
func newRuntime() (*compose.Runtime, *fakes.Scripted) {
runner := fakes.NewScripted()
return compose.New(runner), runner
}

func cfg() ports.RuntimeConfig {
Expand Down
16 changes: 8 additions & 8 deletions internal/adapters/runtime/compose/ingest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"testing"

"github.com/morzecrew/morzer/internal/domain"
"github.com/morzecrew/morzer/internal/infra/exec"
"github.com/morzecrew/morzer/test/fakes"
)

const (
Expand Down Expand Up @@ -52,7 +52,7 @@ func emptyLayout(t *testing.T) string {
// because the loopback reference names a port that stops listening the moment
// this returns.
func TestIngestPullsFromLoopbackAndLeavesTheAliasBehind(t *testing.T) {
runner := exec.NewScripted()
runner := fakes.NewScripted()
// Absent locally, so there is something to do.
runner.OnExit("image inspect", 1, "Error: No such image")

Expand Down Expand Up @@ -112,7 +112,7 @@ func TestIngestPullsFromLoopbackAndLeavesTheAliasBehind(t *testing.T) {
// gigabytes, and the check happens before the layout is opened -- so an ingest
// with nothing to do binds no port and reads no directory.
func TestIngestSkipsWhatIsAlreadyHere(t *testing.T) {
runner := exec.NewScripted() // every command succeeds: the image is present
runner := fakes.NewScripted() // every command succeeds: the image is present

r := New(runner)
// A directory that is not a layout: if this is opened, Start fails and
Expand All @@ -135,7 +135,7 @@ func TestIngestSkipsWhatIsAlreadyHere(t *testing.T) {
// The overwhelmingly common case -- a release that bundles no images at all --
// must not open a layout that is not there.
func TestIngestOfNothingDoesNothing(t *testing.T) {
runner := exec.NewScripted()
runner := fakes.NewScripted()
r := New(runner)

if err := r.IngestImages(context.Background(), "/nonexistent", nil); err != nil {
Expand All @@ -153,7 +153,7 @@ func TestIngestOfNothingDoesNothing(t *testing.T) {
// message about a pull would be looking in the one place the problem cannot
// be.
func TestAFailedPullReportsTheBundleRatherThanTheNetwork(t *testing.T) {
runner := exec.NewScripted()
runner := fakes.NewScripted()
runner.OnExit("image inspect", 1, "Error: No such image")
runner.OnExit("pull", 1, "filesystem layer verification failed for digest sha256:0000")

Expand Down Expand Up @@ -236,7 +236,7 @@ func TestRepositoryPathIsCosmeticButHasToBeLegal(t *testing.T) {
// to leave behind. Unreachable through a validated manifest, and reachable
// through the port, which is where this is asserted.
func TestIngestRefusesAnUnpinnedBundledImage(t *testing.T) {
runner := exec.NewScripted()
runner := fakes.NewScripted()
runner.OnExit("image inspect", 1, "Error: No such image")

r := New(runner)
Expand All @@ -260,7 +260,7 @@ func TestIngestRefusesAnUnpinnedBundledImage(t *testing.T) {
// but not named is an image nothing can use -- and the bytes being safely in
// the store is exactly what would make this easy to ignore.
func TestAFailedTagIsReportedRatherThanSwallowed(t *testing.T) {
runner := exec.NewScripted()
runner := fakes.NewScripted()
runner.OnExit("image inspect", 1, "Error: No such image")
runner.OnExit(" tag ", 1, "Error response from daemon: no such image")

Expand Down Expand Up @@ -298,7 +298,7 @@ func TestARemoteDaemonIsRefusedBeforeAnythingIsServed(t *testing.T) {
t.Run(host, func(t *testing.T) {
t.Setenv("DOCKER_HOST", host)

runner := exec.NewScripted()
runner := fakes.NewScripted()
runner.OnExit("image inspect", 1, "Error: No such image")
r := New(runner)

Expand Down
43 changes: 22 additions & 21 deletions internal/adapters/runtime/compose/volumes_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/morzecrew/morzer/internal/domain"
"github.com/morzecrew/morzer/internal/infra/exec"
"github.com/morzecrew/morzer/internal/ports"
"github.com/morzecrew/morzer/test/fakes"
)

// The fixture is real output. It was taken from `docker compose config
Expand Down Expand Up @@ -205,7 +206,7 @@ func TestTheHelperImageCanBeOverriddenButNotErased(t *testing.T) {
// refusal, because using the default instead would back the deployment up
// through an image the operator did not choose and never hear about it.
func TestAHelperImageThatIsNotPinnedByDigestIsRefused(t *testing.T) {
runner := exec.NewScripted()
runner := fakes.NewScripted()
// The tag is present locally: the case that would otherwise run.
runner.On("image inspect", exec.Result{})
r := New(runner, WithHelperImage("busybox:latest"))
Expand Down Expand Up @@ -253,15 +254,15 @@ func TestAHelperImageThatIsNotPinnedByDigestIsRefused(t *testing.T) {

// helperLab returns a runtime whose docker invocations are recorded rather than
// run, with the helper image reported as already present.
func helperLab(t *testing.T) (*Runtime, *exec.Scripted) {
func helperLab(t *testing.T) (*Runtime, *fakes.Scripted) {
t.Helper()
runner := exec.NewScripted()
runner := fakes.NewScripted()
// `image inspect` succeeding is how HasImage answers "it is here".
runner.On("image inspect", exec.Result{})
return New(runner), runner
}

func helperArgv(t *testing.T, runner *exec.Scripted) string {
func helperArgv(t *testing.T, runner *fakes.Scripted) string {
t.Helper()
for _, c := range runner.Calls() {
line := strings.Join(c.Argv, " ")
Expand Down Expand Up @@ -317,7 +318,7 @@ func TestAVolumeIsRestoredThroughAWritableMount(t *testing.T) {
// air-gapped machine gets the pull command rather than a registry error from
// inside a backup.
func TestAnAbsentHelperImageIsRefusedBeforeAnythingRuns(t *testing.T) {
runner := exec.NewScripted()
runner := fakes.NewScripted()
runner.OnExit("image inspect", 1, "Error: No such image: busybox")
r := New(runner)

Expand All @@ -340,7 +341,7 @@ func TestAnAbsentHelperImageIsRefusedBeforeAnythingRuns(t *testing.T) {
// A failed capture must not leave a zero-length tarball that a checksum would
// happily record as this volume's contents.
func TestAFailedCaptureRemovesTheFileItStarted(t *testing.T) {
runner := exec.NewScripted()
runner := fakes.NewScripted()
runner.On("image inspect", exec.Result{})
runner.OnExit("docker run", 2, "tar: /src: Permission denied")
r := New(runner)
Expand All @@ -358,7 +359,7 @@ func TestAFailedCaptureRemovesTheFileItStarted(t *testing.T) {
// fails: buffered bytes that only reach the disk at close, and a close that
// reports the failure -- a full filesystem, an NFS server that went away.
type closingRunner struct {
*exec.Scripted
*fakes.Scripted
}

func (r closingRunner) Run(ctx context.Context, cmd exec.Command) (exec.Result, error) {
Expand All @@ -374,7 +375,7 @@ func (r closingRunner) Run(ctx context.Context, cmd exec.Command) (exec.Result,
// never encrypt or delete. So the failure that arrives last -- at close, after
// the helper exited zero -- has to clean up like every other one.
func TestACaptureThatCannotBeFinishedLeavesNothingBehind(t *testing.T) {
scripted := exec.NewScripted()
scripted := fakes.NewScripted()
scripted.On("image inspect", exec.Result{})
r := New(closingRunner{scripted})

Expand All @@ -400,7 +401,7 @@ func helperMeasurement(entries, pathBytes int, du string) string {
}

func TestAVolumeIsMeasuredInBytes(t *testing.T) {
runner := exec.NewScripted()
runner := fakes.NewScripted()
runner.On("image inspect", exec.Result{})
// busybox `du -sk` reports KiB and the path it was given.
runner.OnOutput("docker run", helperMeasurement(1, 5, "2048\t/src\n"))
Expand All @@ -413,7 +414,7 @@ func TestAVolumeIsMeasuredInBytes(t *testing.T) {
}

func TestOutputThatIsNotASizeIsAnErrorRatherThanZero(t *testing.T) {
runner := exec.NewScripted()
runner := fakes.NewScripted()
runner.On("image inspect", exec.Result{})
runner.OnOutput("docker run", helperMeasurement(1, 5, "du: unrecognised option\n"))
r := New(runner)
Expand All @@ -440,7 +441,7 @@ func TestASizeCoversTheFramingTarAddsToTheContentsItMeasured(t *testing.T) {
contents = int64(4) << 30
)

runner := exec.NewScripted()
runner := fakes.NewScripted()
runner.On("image inspect", exec.Result{})
runner.OnOutput("docker run",
helperMeasurement(entries, pathBytes, fmt.Sprintf("%d\t/src\n", contents/1024)))
Expand Down Expand Up @@ -479,7 +480,7 @@ func TestAMeasurementWithoutAnEntryCountIsRefused(t *testing.T) {

for name, stdout := range cases {
t.Run(name, func(t *testing.T) {
runner := exec.NewScripted()
runner := fakes.NewScripted()
runner.On("image inspect", exec.Result{})
runner.OnOutput("docker run", stdout)
r := New(runner)
Expand All @@ -496,7 +497,7 @@ func TestAMeasurementWithoutAnEntryCountIsRefused(t *testing.T) {
// and every space check reads a negative as smaller than the free space --
// which is how a refusal became a pass once already.
func TestAFramingThatCannotBeAddedSaturatesRatherThanWrapping(t *testing.T) {
runner := exec.NewScripted()
runner := fakes.NewScripted()
runner.On("image inspect", exec.Result{})
runner.OnOutput("docker run",
helperMeasurement(math.MaxInt64/2, math.MaxInt64/2,
Expand All @@ -514,7 +515,7 @@ func TestAFramingThatCannotBeAddedSaturatesRatherThanWrapping(t *testing.T) {
// read as one. largestSize takes the largest number it is shown, so an unlabelled
// count of ten million files would be budgeted as ten gigabytes of contents.
func TestTheEntryCountIsNotMistakenForASize(t *testing.T) {
runner := exec.NewScripted()
runner := fakes.NewScripted()
runner.On("image inspect", exec.Result{})
runner.OnOutput("docker run", helperMeasurement(10_000_000, 100, "16\t/src\n"))
r := New(runner)
Expand Down Expand Up @@ -546,7 +547,7 @@ func TestAVolumeIsMeasuredByWhicheverReadingIsLarger(t *testing.T) {

for name, tc := range cases {
t.Run(name, func(t *testing.T) {
runner := exec.NewScripted()
runner := fakes.NewScripted()
runner.On("image inspect", exec.Result{})
runner.OnOutput("docker run", tc.stdout)
r := New(runner)
Expand Down Expand Up @@ -586,7 +587,7 @@ func TestAVolumeIsMeasuredByWhicheverReadingIsLarger(t *testing.T) {
// looks plausible. GNU has to win on the first form so it never reaches the
// second. Verified against both implementations.
func TestTheGNUSpellingOfApparentSizeIsTriedFirst(t *testing.T) {
runner := exec.NewScripted()
runner := fakes.NewScripted()
runner.On("image inspect", exec.Result{})
runner.OnOutput("docker run", helperMeasurement(1, 5, "1\t/src\n1\t/src\n"))
r := New(runner)
Expand Down Expand Up @@ -617,7 +618,7 @@ func TestASizeThatCannotBecomeAByteCountIsRefused(t *testing.T) {

for name, tc := range cases {
t.Run(name, func(t *testing.T) {
runner := exec.NewScripted()
runner := fakes.NewScripted()
runner.On("image inspect", exec.Result{})
runner.OnOutput("docker run", tc.stdout)
r := New(runner)
Expand All @@ -641,7 +642,7 @@ func TestASizeThatCannotBecomeAByteCountIsRefused(t *testing.T) {
// refusing every backup of a deployment over it is its own failure.
func TestAMeasurementThatNeverRanIsMarkedApartFromOneThatRanAndCouldNotBeRead(t *testing.T) {
t.Run("the measurement never ran", func(t *testing.T) {
runner := exec.NewScripted()
runner := fakes.NewScripted()
runner.On("image inspect", exec.Result{})
runner.OnExit("docker run", 1, "du: /src: Operation not permitted")
r := New(runner)
Expand All @@ -667,7 +668,7 @@ func TestAMeasurementThatNeverRanIsMarkedApartFromOneThatRanAndCouldNotBeRead(t
}
for name, stdout := range unreadable {
t.Run(name, func(t *testing.T) {
runner := exec.NewScripted()
runner := fakes.NewScripted()
runner.On("image inspect", exec.Result{})
runner.OnOutput("docker run", stdout)
r := New(runner)
Expand All @@ -690,7 +691,7 @@ func TestAMeasurementThatNeverRanIsMarkedApartFromOneThatRanAndCouldNotBeRead(t
// would travel out to `morzer backup` as a runtime failure rather than an
// interruption, which is a different exit code.
func TestACancelledMeasurementIsAnInterruptionRatherThanAnUnmeasuredVolume(t *testing.T) {
runner := exec.NewScripted()
runner := fakes.NewScripted()
runner.On("image inspect", exec.Result{})
runner.OnError("docker run", domain.Interrupted("docker run was cancelled"))
r := New(runner)
Expand All @@ -705,7 +706,7 @@ func TestACancelledMeasurementIsAnInterruptionRatherThanAnUnmeasuredVolume(t *te
// and up reconciles against the declared configuration, so resuming a stack
// after a backup could recreate a container whose definition had drifted.
func TestQuiescingUsesStopAndStartRatherThanDownAndUp(t *testing.T) {
runner := exec.NewScripted()
runner := fakes.NewScripted()
r := New(runner)
cfg := ports.RuntimeConfig{Product: "demo"}

Expand Down
13 changes: 13 additions & 0 deletions internal/adapters/secrets/sopsage/export_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package sopsage

// The two seams the failure tests drive: a sops that is somewhere else, and a
// clock that does not move.
//
// Unexported in the package itself because nothing outside this directory has
// ever set either -- production takes `sops` from PATH and the real time -- and
// an option only tests pass is an option no test exercises as production leaves
// it.
var (
WithSOPSBinary = withSOPSBinary
WithClock = withClock
)
8 changes: 4 additions & 4 deletions internal/adapters/secrets/sopsage/failures_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (

"github.com/morzecrew/morzer/internal/adapters/secrets/sopsage"
"github.com/morzecrew/morzer/internal/domain"
"github.com/morzecrew/morzer/internal/infra/exec"
"github.com/morzecrew/morzer/test/fakes"
)

// What sops does when it fails is the thing an operator meets at the worst
Expand All @@ -26,7 +26,7 @@ import (
// unambiguous parse. The fixtures below are written that way for the same
// reason the adapter asks for it.

func scriptedStore(t *testing.T, encrypted string) (*sopsage.Store, *exec.Scripted, string) {
func scriptedStore(t *testing.T, encrypted string) (*sopsage.Store, *fakes.Scripted, string) {
t.Helper()

dir := t.TempDir()
Expand All @@ -42,7 +42,7 @@ func scriptedStore(t *testing.T, encrypted string) (*sopsage.Store, *exec.Script
t.Fatal(err)
}

runner := exec.NewScripted()
runner := fakes.NewScripted()
return sopsage.New(runner, file, identity,
sopsage.WithClock(func() time.Time {
return time.Date(2026, 8, 4, 12, 0, 0, 0, time.UTC)
Expand Down Expand Up @@ -188,7 +188,7 @@ func TestTheSOPSBinaryCanBeOverridden(t *testing.T) {
t.Fatal(err)
}

runner := exec.NewScripted()
runner := fakes.NewScripted()
runner.OnOutput("decrypt", `{"values":{}}`)

store := sopsage.New(runner, file, identity,
Expand Down
Loading
Loading